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:
Wyatt Johnson
2018-10-19 16:05:58 -06:00
parent 9436600e31
commit fa72d5deda
9 changed files with 249 additions and 5 deletions
+2 -1
View File
@@ -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.
+42
View File
@@ -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;
}
+22
View File
@@ -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;
}