mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 18:45:03 +08:00
feat: support new auth methods for Tenants
- New Time scalar type is implemented on the Server - Single Sign-On keys can now be generated - Single Sign-On keys can be regenerated - Single Sign-On keys now store the date they were generated on. - Initial implementation of `AuthenticationTargetFilter`'s
This commit is contained in:
@@ -37,7 +37,7 @@ async function main() {
|
||||
'import { Cursor } from "talk-server/models/connection";',
|
||||
'import TenantContext from "talk-server/graph/tenant/context";',
|
||||
],
|
||||
customScalarType: { Cursor: "Cursor", Time: "string" },
|
||||
customScalarType: { Cursor: "Cursor", Time: "Date" },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -48,6 +48,7 @@ async function main() {
|
||||
importStatements: [
|
||||
'import ManagementContext from "talk-server/graph/management/context";',
|
||||
],
|
||||
customScalarType: { Time: "Date" },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { GraphQLScalarType } from "graphql";
|
||||
import { Kind } from "graphql/language";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
export default new GraphQLScalarType({
|
||||
name: "Time",
|
||||
description: "Time represented as an ISO8601 string.",
|
||||
serialize(value) {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.toISOString();
|
||||
},
|
||||
parseValue(value) {
|
||||
return new Date(value);
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
switch (ast.kind) {
|
||||
case Kind.STRING:
|
||||
// This handles an empty string.
|
||||
if (ast.value && ast.value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = DateTime.fromISO(ast.value, {});
|
||||
if (!date.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return date.toJSDate();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import Time from "talk-server/graph/common/scalars/time";
|
||||
|
||||
import { GQLResolver } from "talk-server/graph/management/schema/__generated__/types";
|
||||
|
||||
const Resolvers: GQLResolver = {};
|
||||
const Resolvers: GQLResolver = {
|
||||
Time,
|
||||
};
|
||||
|
||||
export default Resolvers;
|
||||
|
||||
@@ -3,9 +3,11 @@ import { isNull, omitBy } from "lodash";
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import { GQLSettingsInput } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
import { update } from "talk-server/services/tenant";
|
||||
import { regenerateSSOKey, update } from "talk-server/services/tenant";
|
||||
|
||||
export default ({ mongo, redis, tenantCache, tenant }: TenantContext) => ({
|
||||
update: (input: GQLSettingsInput): Promise<Tenant | null> =>
|
||||
update(mongo, redis, tenantCache, tenant, omitBy(input, isNull)),
|
||||
regenerateSSOKey: (): Promise<Tenant | null> =>
|
||||
regenerateSSOKey(mongo, redis, tenantCache, tenant),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import Cursor from "talk-server/graph/common/scalars/cursor";
|
||||
import Time from "talk-server/graph/common/scalars/time";
|
||||
|
||||
import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import Asset from "./asset";
|
||||
@@ -20,6 +22,7 @@ const Resolvers: GQLResolver = {
|
||||
Profile,
|
||||
Query,
|
||||
User,
|
||||
Time,
|
||||
};
|
||||
|
||||
export default Resolvers;
|
||||
|
||||
@@ -47,6 +47,10 @@ const Mutation: GQLMutationTypeResolver<void> = {
|
||||
comment: await ctx.mutators.Comment.deleteFlag(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
regenerateSSOKey: async (source, { input }, ctx) => ({
|
||||
settings: await ctx.mutators.Settings.regenerateSSOKey(),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
};
|
||||
|
||||
export default Mutation;
|
||||
|
||||
@@ -244,12 +244,32 @@ type Wordlist {
|
||||
## Auth
|
||||
################################################################################
|
||||
|
||||
##########################
|
||||
## AuthenticationTargetFilter
|
||||
##########################
|
||||
|
||||
"""
|
||||
AuthenticationTargetFilter when non-null, will specify the targets that a
|
||||
specific authentication integration will be enabled on.
|
||||
"""
|
||||
type AuthenticationTargetFilter {
|
||||
admin: Boolean!
|
||||
stream: Boolean!
|
||||
}
|
||||
|
||||
##########################
|
||||
## LocalAuthIntegration
|
||||
##########################
|
||||
|
||||
type LocalAuthIntegration {
|
||||
enabled: Boolean!
|
||||
|
||||
"""
|
||||
targetFilter will restrict where the authentication integration should be
|
||||
displayed. If the value of targetFilter is null, then the authentication
|
||||
integration should be displayed in all targets.
|
||||
"""
|
||||
targetFilter: AuthenticationTargetFilter
|
||||
}
|
||||
|
||||
##########################
|
||||
@@ -264,11 +284,23 @@ embed to allow single sign on.
|
||||
type SSOAuthIntegration {
|
||||
enabled: Boolean!
|
||||
|
||||
"""
|
||||
targetFilter will restrict where the authentication integration should be
|
||||
displayed. If the value of targetFilter is null, then the authentication
|
||||
integration should be displayed in all targets.
|
||||
"""
|
||||
targetFilter: AuthenticationTargetFilter
|
||||
|
||||
"""
|
||||
key is the secret that is used to sign tokens.
|
||||
"""
|
||||
key: String @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
keyGeneratedAt is the Time that the key was effective from.
|
||||
"""
|
||||
keyGeneratedAt: Time @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
displayNameEnable when enabled, will allow Users to set and view their
|
||||
displayName's.
|
||||
@@ -287,6 +319,16 @@ will be used in the admin to provide staff logins for users.
|
||||
type OIDCAuthIntegration {
|
||||
enabled: Boolean!
|
||||
|
||||
"""
|
||||
targetFilter will restrict where the authentication integration should be
|
||||
displayed. If the value of targetFilter is null, then the authentication
|
||||
integration should be displayed in all targets.
|
||||
"""
|
||||
targetFilter: AuthenticationTargetFilter
|
||||
|
||||
"""
|
||||
name is the label assigned to reference the provider of the OIDC integration.
|
||||
"""
|
||||
name: String
|
||||
clientID: String @auth(roles: [ADMIN])
|
||||
clientSecret: String @auth(roles: [ADMIN])
|
||||
@@ -308,6 +350,14 @@ type OIDCAuthIntegration {
|
||||
|
||||
type GoogleAuthIntegration {
|
||||
enabled: Boolean!
|
||||
|
||||
"""
|
||||
targetFilter will restrict where the authentication integration should be
|
||||
displayed. If the value of targetFilter is null, then the authentication
|
||||
integration should be displayed in all targets.
|
||||
"""
|
||||
targetFilter: AuthenticationTargetFilter
|
||||
|
||||
clientID: String @auth(roles: [ADMIN])
|
||||
clientSecret: String @auth(roles: [ADMIN])
|
||||
}
|
||||
@@ -318,6 +368,14 @@ type GoogleAuthIntegration {
|
||||
|
||||
type FacebookAuthIntegration {
|
||||
enabled: Boolean!
|
||||
|
||||
"""
|
||||
targetFilter will restrict where the authentication integration should be
|
||||
displayed. If the value of targetFilter is null, then the authentication
|
||||
integration should be displayed in all targets.
|
||||
"""
|
||||
targetFilter: AuthenticationTargetFilter
|
||||
|
||||
clientID: String @auth(roles: [ADMIN])
|
||||
clientSecret: String @auth(roles: [ADMIN])
|
||||
}
|
||||
@@ -1258,17 +1316,31 @@ input SettingsWordlistInput {
|
||||
suspect: [String!]
|
||||
}
|
||||
|
||||
input SettingsAuthenticationTargetFilterInput {
|
||||
admin: Boolean!
|
||||
stream: Boolean!
|
||||
}
|
||||
|
||||
input SettingsLocalAuthIntegrationInput {
|
||||
enabled: Boolean
|
||||
|
||||
"""
|
||||
targetFilter will restrict where the authentication integration should be
|
||||
displayed. If the value of targetFilter is null, then the authentication
|
||||
integration should be displayed in all targets.
|
||||
"""
|
||||
targetFilter: SettingsAuthenticationTargetFilterInput
|
||||
}
|
||||
|
||||
input SettingsSSOAuthIntegrationInput {
|
||||
enabled: Boolean
|
||||
|
||||
"""
|
||||
key is the secret that is used to sign tokens.
|
||||
targetFilter will restrict where the authentication integration should be
|
||||
displayed. If the value of targetFilter is null, then the authentication
|
||||
integration should be displayed in all targets.
|
||||
"""
|
||||
key: String
|
||||
targetFilter: SettingsAuthenticationTargetFilterInput
|
||||
|
||||
"""
|
||||
displayNameEnable when enabled, will allow Users to set and view their
|
||||
@@ -1280,11 +1352,23 @@ input SettingsSSOAuthIntegrationInput {
|
||||
input SettingsOIDCAuthIntegrationInput {
|
||||
enabled: Boolean
|
||||
|
||||
"""
|
||||
targetFilter will restrict where the authentication integration should be
|
||||
displayed. If the value of targetFilter is null, then the authentication
|
||||
integration should be displayed in all targets.
|
||||
"""
|
||||
targetFilter: SettingsAuthenticationTargetFilterInput
|
||||
|
||||
"""
|
||||
name is the label assigned to reference the provider of the OIDC integration.
|
||||
"""
|
||||
name: String
|
||||
clientID: String
|
||||
clientSecret: String
|
||||
authorizationURL: String
|
||||
tokenURL: String
|
||||
jwksURI: String
|
||||
issuer: String
|
||||
|
||||
"""
|
||||
displayNameEnable when enabled, will allow Users to set and view their
|
||||
@@ -1295,12 +1379,28 @@ input SettingsOIDCAuthIntegrationInput {
|
||||
|
||||
input SettingsGoogleAuthIntegrationInput {
|
||||
enabled: Boolean
|
||||
|
||||
"""
|
||||
targetFilter will restrict where the authentication integration should be
|
||||
displayed. If the value of targetFilter is null, then the authentication
|
||||
integration should be displayed in all targets.
|
||||
"""
|
||||
targetFilter: SettingsAuthenticationTargetFilterInput
|
||||
|
||||
clientID: String
|
||||
clientSecret: String
|
||||
}
|
||||
|
||||
input SettingsFacebookAuthIntegrationInput {
|
||||
enabled: Boolean
|
||||
|
||||
"""
|
||||
targetFilter will restrict where the authentication integration should be
|
||||
displayed. If the value of targetFilter is null, then the authentication
|
||||
integration should be displayed in all targets.
|
||||
"""
|
||||
targetFilter: SettingsAuthenticationTargetFilterInput
|
||||
|
||||
clientID: String
|
||||
clientSecret: String
|
||||
}
|
||||
@@ -1777,6 +1877,29 @@ type DeleteCommentFlagPayload {
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## regenerateSSOKey
|
||||
##################
|
||||
|
||||
input RegenerateSSOKeyInput {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type RegenerateSSOKeyPayload {
|
||||
"""
|
||||
settings is the Settings that the SSO key was regenerated on.
|
||||
"""
|
||||
settings: Settings
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## Mutation
|
||||
##################
|
||||
@@ -1799,6 +1922,13 @@ type Mutation {
|
||||
updateSettings(input: UpdateSettingsInput!): UpdateSettingsPayload
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
regenerateSSOKey will regenerate the SSO key used to sign secrets. This will
|
||||
invalidate any existing user sessions.
|
||||
"""
|
||||
regenerateSSOKey(input: RegenerateSSOKeyInput!): RegenerateSSOKeyPayload
|
||||
@auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
createCommentReaction will create a Reaction authored by the current logged in
|
||||
User on a Comment.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import crypto from "crypto";
|
||||
import { Db } from "mongodb";
|
||||
import uuid from "uuid";
|
||||
|
||||
@@ -203,3 +204,44 @@ export async function updateTenant(
|
||||
|
||||
return result.value || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* regenerateTenantSSOKey will regenerate the SSO key used for Single Sing-On
|
||||
* for the specified Tenant. All existing user sessions signed with the old
|
||||
* secret will be invalidated.
|
||||
*/
|
||||
export async function regenerateTenantSSOKey(db: Db, id: string) {
|
||||
// Generate a new key. We generate a key of minimum length 32 up to 37 bytes,
|
||||
// as 16 was the minimum length recommended.
|
||||
//
|
||||
// Reference: https://security.stackexchange.com/a/96176
|
||||
const key = crypto
|
||||
.randomBytes(32 + Math.floor(Math.random() * 5))
|
||||
.toString("hex");
|
||||
|
||||
// Construct the update.
|
||||
const update: DeepPartial<Tenant> = {
|
||||
auth: {
|
||||
integrations: {
|
||||
sso: {
|
||||
key,
|
||||
keyGeneratedAt: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Update the Tenant with this new key.
|
||||
const result = await collection(db).findOneAndUpdate(
|
||||
{ id },
|
||||
// Serialize the deep update into the Tenant.
|
||||
{
|
||||
$set: dotize(update),
|
||||
},
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
{ returnOriginal: false }
|
||||
);
|
||||
|
||||
return result.value || null;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { GQLSettingsInput } from "talk-server/graph/tenant/schema/__generated__/
|
||||
import {
|
||||
createTenant,
|
||||
CreateTenantInput,
|
||||
regenerateTenantSSOKey,
|
||||
Tenant,
|
||||
updateTenant,
|
||||
} from "talk-server/models/tenant";
|
||||
@@ -76,3 +77,24 @@ export async function canInstall(cache: TenantCache) {
|
||||
export async function isInstalled(cache: TenantCache) {
|
||||
return (await cache.count()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* regenerateSSOKey will regenerate the Single Sign-On key for the specified
|
||||
* Tenant and notify all other Tenant's connected that the Tenant was updated.
|
||||
*/
|
||||
export async function regenerateSSOKey(
|
||||
mongo: Db,
|
||||
redis: Redis,
|
||||
cache: TenantCache,
|
||||
tenant: Tenant
|
||||
) {
|
||||
const updatedTenant = await regenerateTenantSSOKey(mongo, tenant.id);
|
||||
if (!updatedTenant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update the tenant cache.
|
||||
await cache.update(redis, updatedTenant);
|
||||
|
||||
return updatedTenant;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user