feat: added initial server sso key rotation semantics (#2696)

This commit is contained in:
Wyatt Johnson
2019-11-07 21:53:28 +00:00
committed by Kim Gardner
parent 59b8dfccda
commit 2dbba52fbd
14 changed files with 441 additions and 88 deletions
@@ -5,10 +5,9 @@ import OAuth2Strategy, {
} from "coral-server/app/middleware/passport/strategies/oauth2";
import { constructTenantURL } from "coral-server/app/url";
import {
GQLAuthIntegrations,
GQLFacebookAuthIntegration,
GQLUSER_ROLE,
} from "coral-server/graph/tenant/schema/__generated__/types";
AuthIntegrations,
FacebookAuthIntegration,
} from "coral-server/models/settings";
import { Tenant } from "coral-server/models/tenant";
import {
FacebookProfile,
@@ -16,10 +15,12 @@ import {
} from "coral-server/models/user";
import { findOrCreate } from "coral-server/services/users";
import { GQLUSER_ROLE } from "coral-server/graph/tenant/schema/__generated__/types";
export type FacebookStrategyOptions = OAuth2StrategyOptions;
export default class FacebookStrategy extends OAuth2Strategy<
GQLFacebookAuthIntegration,
FacebookAuthIntegration,
Strategy
> {
public name = "facebook";
@@ -34,12 +35,12 @@ export default class FacebookStrategy extends OAuth2Strategy<
});
}
protected getIntegration = (integrations: GQLAuthIntegrations) =>
protected getIntegration = (integrations: AuthIntegrations) =>
integrations.facebook;
protected async findOrCreateUser(
tenant: Tenant,
integration: Required<GQLFacebookAuthIntegration>,
integration: Required<FacebookAuthIntegration>,
{ id, photos, emails }: Profile,
now = new Date()
) {
@@ -94,7 +95,7 @@ export default class FacebookStrategy extends OAuth2Strategy<
protected createStrategy(
tenant: Tenant,
integration: Required<GQLFacebookAuthIntegration>
integration: Required<FacebookAuthIntegration>
) {
return new Strategy(
{
@@ -5,10 +5,9 @@ import OAuth2Strategy, {
} from "coral-server/app/middleware/passport/strategies/oauth2";
import { constructTenantURL } from "coral-server/app/url";
import {
GQLAuthIntegrations,
GQLGoogleAuthIntegration,
GQLUSER_ROLE,
} from "coral-server/graph/tenant/schema/__generated__/types";
AuthIntegrations,
GoogleAuthIntegration,
} from "coral-server/models/settings";
import { Tenant } from "coral-server/models/tenant";
import {
GoogleProfile,
@@ -16,10 +15,12 @@ import {
} from "coral-server/models/user";
import { findOrCreate } from "coral-server/services/users";
import { GQLUSER_ROLE } from "coral-server/graph/tenant/schema/__generated__/types";
export type GoogleStrategyOptions = OAuth2StrategyOptions;
export default class GoogleStrategy extends OAuth2Strategy<
GQLGoogleAuthIntegration,
GoogleAuthIntegration,
Strategy
> {
public name = "google";
@@ -33,12 +34,12 @@ export default class GoogleStrategy extends OAuth2Strategy<
});
}
protected getIntegration = (integrations: GQLAuthIntegrations) =>
protected getIntegration = (integrations: AuthIntegrations) =>
integrations.google;
protected async findOrCreateUser(
tenant: Tenant,
integration: Required<GQLGoogleAuthIntegration>,
integration: Required<GoogleAuthIntegration>,
{ id, photos, emails }: Profile,
now = new Date()
) {
@@ -93,7 +94,7 @@ export default class GoogleStrategy extends OAuth2Strategy<
protected createStrategy(
tenant: Tenant,
integration: Required<GQLGoogleAuthIntegration>
integration: Required<GoogleAuthIntegration>
) {
return new Strategy(
{
@@ -9,7 +9,10 @@ import {
} from "coral-server/errors";
import { Tenant } from "coral-server/models/tenant";
import { User } from "coral-server/models/user";
import { extractTokenFromRequest } from "coral-server/services/jwt";
import {
extractTokenFromRequest,
StandardHeader,
} from "coral-server/services/jwt";
import { Request } from "coral-server/types/express";
import { JWTToken, JWTVerifier } from "./verifiers/jwt";
@@ -38,14 +41,15 @@ export interface Verifier<T = Token> {
tokenString: string,
token: T,
tenant: Tenant,
now: Date
now: Date,
kid?: string
) => Promise<Readonly<User> | null>;
/**
* supports will perform type checking and ensure that the given Tenant
* supports the requested verification type.
*/
supports: (token: T | object, tenant: Tenant) => token is T;
supports: (token: T | object, tenant: Tenant, kid?: string) => token is T;
}
export function createVerifiers(
@@ -64,16 +68,29 @@ export async function verifyAndRetrieveUser(
tokenString: string,
now = new Date()
) {
const token: Token = jwt.decode(tokenString);
if (!token || typeof token === "string") {
// Decode the token into header and payload parts.
const decoded = jwt.decode(tokenString, {
complete: true,
});
if (!decoded || typeof decoded === "string") {
throw new TokenInvalidError(tokenString, "token could not be decoded");
}
// Pull the parts of the token apart.
const header: StandardHeader = decoded.header;
const token: Token = decoded.payload;
try {
// Try to verify the token.
for (const verifier of verifiers) {
if (verifier.supports(token, tenant)) {
return await verifier.verify(tokenString, token, tenant, now);
return await verifier.verify(
tokenString,
token,
tenant,
now,
header.kid
);
}
}
} catch (err) {
@@ -1,21 +1,20 @@
import Joi from "joi";
import { isNil } from "lodash";
import { DateTime } from "luxon";
import { Db } from "mongodb";
import { validate } from "coral-server/app/request/body";
import { IntegrationDisabled } from "coral-server/errors";
import { IntegrationDisabled, TokenInvalidError } from "coral-server/errors";
import {
GQLSSOAuthIntegration,
GQLUSER_ROLE,
} from "coral-server/graph/tenant/schema/__generated__/types";
RequiredSSOKey,
SSOAuthIntegration,
} from "coral-server/models/settings";
import { Tenant } from "coral-server/models/tenant";
import {
retrieveUserWithProfile,
SSOProfile,
updateUserFromSSO,
} from "coral-server/models/user";
import { findOrCreate } from "coral-server/services/users";
import {
getSSOProfile,
needsSSOUpdate,
@@ -26,7 +25,13 @@ import {
verifyJWT,
} from "coral-server/services/jwt";
import { AugmentedRedis } from "coral-server/services/redis";
import { DateTime } from "luxon";
import { findOrCreate } from "coral-server/services/users";
import {
GQLSSOAuthIntegration,
GQLUSER_ROLE,
} from "coral-server/graph/tenant/schema/__generated__/types";
import { Verifier } from "../jwt";
export interface SSOStrategyOptions {
@@ -167,6 +172,57 @@ export interface SSOVerifierOptions {
redis: AugmentedRedis;
}
export function getRelevantSSOKeys(
integration: SSOAuthIntegration,
tokenString: string,
now: Date,
kid?: string
): RequiredSSOKey[] {
// Collect all the current valid keys.
const keys = integration.keys.filter(k => {
if (!k.secret) {
return false;
}
if (k.deletedAt) {
return false;
}
if (k.deprecateAt && now >= k.deprecateAt) {
return false;
}
return k;
}) as RequiredSSOKey[];
// If there is only one key, that's all we can use!
if (keys.length === 1) {
return keys;
}
// There is more than one key that could work, lets see if the token has a
// kid.
if (kid) {
// The token has a kid, so if we have a matching token, we should use it. If
// we don't have a matching kid, we can't possibly verify it, so throw an
// error.
const key = keys.find(k => k.kid === kid);
if (!key) {
throw new TokenInvalidError(
tokenString,
"kid was specified but no matching keys were found"
);
}
return [key];
}
// Because no matching kid's were found, we should now use all valid secrets
// instead.
// TODO: [CORL-755] (wyattjoh) remove when we are able to make a breaking change to require signing with a kid
return keys;
}
export class SSOVerifier implements Verifier<SSOToken> {
private mongo: Db;
private redis: AugmentedRedis;
@@ -176,7 +232,13 @@ export class SSOVerifier implements Verifier<SSOToken> {
this.redis = redis;
}
public supports(token: SSOToken | object, tenant: Tenant): token is SSOToken {
public supports(
token: SSOToken | object,
tenant: Tenant,
kid?: string
): token is SSOToken {
// TODO: [CORL-755] (wyattjoh) check that the `kid` it provided and matches a given kid in a future release
return tenant.auth.integrations.sso.enabled && isSSOToken(token);
}
@@ -184,28 +246,68 @@ export class SSOVerifier implements Verifier<SSOToken> {
tokenString: string,
token: SSOToken,
tenant: Tenant,
now: Date
now: Date,
kid?: string
) {
const integration = tenant.auth.integrations.sso;
if (!integration.enabled) {
throw new IntegrationDisabled("sso");
}
if (!integration.key) {
// check to see if there is at least one key associated with this
// integration.
if (integration.keys.length === 0) {
throw new Error("integration key does not exist");
}
verifyJWT(
tokenString,
{
// Force the use of the HS256 algorithm. We can explore switching this
// out in the future..
// TODO: (wyattjoh) investigate replacing algorithm.
algorithm: SymmetricSigningAlgorithm.HS256,
secret: integration.key,
},
now
);
// Get the valid configurations for the given token and integration pair.
const keys = getRelevantSSOKeys(integration, tokenString, now, kid);
if (keys.length === 0) {
throw new TokenInvalidError(
tokenString,
"no verification configuration can be matched"
);
}
// TODO: [CORL-755] (wyattjoh) remove once we've added the requirement for `kid`.
// While there are configs left to test...
while (keys.length > 0) {
// Grab the next config to test. We know that the shift operation will
// return a config because we validated it's length in the loop predicate.
const key = keys.shift()!;
try {
verifyJWT(
tokenString,
{
// TODO: (wyattjoh) investigate replacing algorithm.
algorithm: SymmetricSigningAlgorithm.HS256,
secret: key.secret,
},
now
);
} catch (err) {
// If this is the last config to test, we need to rethrow the error
// here.
if (keys.length === 0) {
throw err;
}
// There are still configs to test, continue.
continue;
}
// TODO: [CORL-754] (wyattjoh) reintroduce when we amend the front-end to display the kid
// // The verification did not throw an error, which means the verification
// // succeeded! Break out now.
// if (!kid) {
// logger.warn(
// { tenantID: tenant.id, kid: config.kid },
// "token without a `kid` matched key with known `kid`"
// );
// }
break;
}
return findOrCreateSSOUser(
this.mongo,
@@ -9,9 +9,10 @@ export const Settings = ({
tenantCache,
tenant,
config,
now,
}: TenantContext) => ({
update: (input: GQLUpdateSettingsInput): Promise<Tenant | null> =>
update(mongo, redis, tenantCache, config, tenant, input.settings),
regenerateSSOKey: (): Promise<Tenant | null> =>
regenerateSSOKey(mongo, redis, tenantCache, tenant),
regenerateSSOKey(mongo, redis, tenantCache, tenant, now),
});
@@ -0,0 +1,30 @@
import * as settings from "coral-server/models/settings";
import { GQLSSOAuthIntegrationTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types";
function getActiveSSOKey(keys: settings.SSOKey[]) {
return keys.find(
key => Boolean(key.secret) && !key.deletedAt && !key.deprecateAt
);
}
export const SSOAuthIntegration: GQLSSOAuthIntegrationTypeResolver<
settings.SSOAuthIntegration
> = {
key: ({ keys }) => {
const key = getActiveSSOKey(keys);
if (key) {
return key.secret;
}
return null;
},
keyGeneratedAt: ({ keys }) => {
const key = getActiveSSOKey(keys);
if (key) {
return key.createdAt;
}
return null;
},
};
@@ -1,6 +1,7 @@
import Cursor from "coral-server/graph/common/scalars/cursor";
import Locale from "coral-server/graph/common/scalars/locale";
import Time from "coral-server/graph/common/scalars/time";
import { GQLResolver } from "coral-server/graph/tenant/schema/__generated__/types";
import { ApproveCommentPayload } from "./ApproveCommentPayload";
@@ -36,6 +37,7 @@ import { Profile } from "./Profile";
import { Query } from "./Query";
import { RecentCommentHistory } from "./RecentCommentHistory";
import { RejectCommentPayload } from "./RejectCommentPayload";
import { SSOAuthIntegration } from "./SSOAuthIntegration";
import { Story } from "./Story";
import { StorySettings } from "./StorySettings";
import { Subscription } from "./Subscription";
@@ -56,10 +58,10 @@ const Resolvers: GQLResolver = {
Comment,
CommentCounts,
CommentCreatedPayload,
CommentReleasedPayload,
CommentEnteredModerationQueuePayload,
CommentLeftModerationQueuePayload,
CommentModerationAction,
CommentReleasedPayload,
CommentReplyCreatedPayload,
CommentRevision,
CommentStatusUpdatedPayload,
@@ -71,8 +73,10 @@ const Resolvers: GQLResolver = {
GoogleAuthIntegration,
Invite,
LiveConfiguration,
Locale,
ModerationQueue,
ModerationQueues,
ModeratorNote,
Mutation,
OIDCAuthIntegration,
PremodStatus,
@@ -81,19 +85,18 @@ const Resolvers: GQLResolver = {
Query,
RecentCommentHistory,
RejectCommentPayload,
SSOAuthIntegration,
Story,
StorySettings,
Subscription,
SuspensionStatus,
SuspensionStatusHistory,
UsernameHistory,
Tag,
Time,
Locale,
User,
UserStatus,
UsernameHistory,
UsernameStatus,
ModeratorNote,
UserStatus,
};
export default Resolvers;
@@ -15,7 +15,7 @@ import {
} from "subscriptions-transport-ws";
import { ACCESS_TOKEN_PARAM, CLIENT_ID_PARAM } from "coral-common/constants";
import { Omit } from "coral-common/types";
import { Omit, RequireProperty } from "coral-common/types";
import { AppOptions } from "coral-server/app";
import { getHostname } from "coral-server/app/helpers/hostname";
import {
@@ -36,12 +36,13 @@ import {
} from "coral-server/graph/common/extensions";
import { getOperationMetadata } from "coral-server/graph/common/extensions/helpers";
import { getPersistedQuery } from "coral-server/graph/common/persisted";
import { GQLUSER_ROLE } from "coral-server/graph/tenant/schema/__generated__/types";
import logger from "coral-server/logger";
import { PersistedQuery } from "coral-server/models/queries";
import { hasStaffRole } from "coral-server/models/user/helpers";
import { extractTokenFromRequest } from "coral-server/services/jwt";
import { GQLUSER_ROLE } from "coral-server/graph/tenant/schema/__generated__/types";
import TenantContext, { TenantContextOptions } from "../context";
type OnConnectFn = (
@@ -77,11 +78,10 @@ export function extractClientID(connectionParams: OperationMessagePayload) {
return null;
}
export type OnConnectOptions = Omit<
TenantContextOptions,
"tenant" | "signingConfig" | "disableCaching"
> &
Required<Pick<TenantContextOptions, "signingConfig">>;
export type OnConnectOptions = RequireProperty<
Omit<TenantContextOptions, "tenant" | "disableCaching">,
"signingConfig"
>;
export function onConnect(options: OnConnectOptions): OnConnectFn {
// Create the JWT verifiers that will be used to verify all the requests
+43 -3
View File
@@ -1,6 +1,8 @@
import { Omit } from "coral-common/types";
import { Omit, RequireProperty } from "coral-common/types";
import {
GQLAuth,
GQLAuthenticationTargetFilter,
GQLEmailConfiguration,
GQLFacebookAuthIntegration,
GQLGoogleAuthIntegration,
@@ -9,7 +11,6 @@ import {
GQLMODERATION_MODE,
GQLOIDCAuthIntegration,
GQLSettings,
GQLSSOAuthIntegration,
} from "coral-server/graph/tenant/schema/__generated__/types";
export type LiveConfiguration = Omit<GQLLiveConfiguration, "configurable">;
@@ -37,13 +38,52 @@ export type FacebookAuthIntegration = Omit<
"callbackURL" | "redirectURL"
>;
export interface SSOKey {
/**
* kid is the identifier for the key used when verifying tokens issued by the
* provider.
*/
kid: string;
/**
* secret is the actual underlying secret used to verify the tokens with. When
* this is not available, it indicates that the token secret was deleted.
*/
secret?: string;
/**
* createdAt is the time that this key was created at.
*/
createdAt: Date;
/**
* deprecateAt when provided is the time that the token should no longer be
* valid at.
*/
deprecateAt?: Date;
/**
* deletedAt is the timestamp that the token was revoked.
*/
deletedAt?: Date;
}
export type RequiredSSOKey = RequireProperty<SSOKey, "secret">;
export interface SSOAuthIntegration {
enabled: boolean;
allowRegistration: boolean;
targetFilter: GQLAuthenticationTargetFilter;
keys: SSOKey[];
}
/**
* AuthIntegrations are the set of configurations for the variations of
* authentication solutions.
*/
export interface AuthIntegrations {
local: GQLLocalAuthIntegration;
sso: GQLSSOAuthIntegration;
sso: SSOAuthIntegration;
oidc: OIDCAuthIntegration;
google: GoogleAuthIntegration;
facebook: FacebookAuthIntegration;
+13 -2
View File
@@ -7,6 +7,8 @@ import {
} from "coral-server/graph/tenant/schema/__generated__/types";
import { translate } from "coral-server/services/i18n";
import { SSOKey } from "../settings";
export const getDefaultReactionConfiguration = (
bundle: FluentBundle
): GQLReactionConfiguration => ({
@@ -28,10 +30,19 @@ export const getDefaultStaffConfiguration = (
label: translate(bundle, "Staff", "staff-label"),
});
export function generateSSOKey() {
export function generateRandomString(size: number, drift = 5) {
return crypto
.randomBytes(size + Math.floor(Math.random() * drift))
.toString("hex");
}
export function generateSSOKey(createdAt: Date): SSOKey {
// 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
return crypto.randomBytes(32 + Math.floor(Math.random() * 5)).toString("hex");
const secret = generateRandomString(32, 5);
const kid = generateRandomString(8, 3);
return { kid, secret, createdAt };
}
+34 -16
View File
@@ -125,8 +125,8 @@ export async function createTenant(
admin: true,
stream: true,
},
key: generateSSOKey(),
keyGeneratedAt: now,
// TODO: [CORL-754] (wyattjoh) remove this in favor of generating this when needed
keys: [generateSSOKey(now)],
},
oidc: {
enabled: false,
@@ -277,25 +277,17 @@ export async function updateTenant(
* for the specified Tenant. All existing user sessions signed with the old
* secret will be invalidated.
*/
export async function regenerateTenantSSOKey(mongo: Db, id: string) {
// Construct the update.
const update: DeepPartial<Tenant> = {
auth: {
integrations: {
sso: {
key: generateSSOKey(),
keyGeneratedAt: new Date(),
},
},
},
};
export async function createTenantSSOKey(mongo: Db, id: string, now: Date) {
// Construct the new key.
const key = generateSSOKey(now);
// Update the Tenant with this new key.
const result = await collection(mongo).findOneAndUpdate(
{ id },
// Serialize the deep update into the Tenant.
{
$set: dotize(update),
$push: {
"auth.integrations.sso.keys": key,
},
},
// False to return the updated document instead of the original
// document.
@@ -304,3 +296,29 @@ export async function regenerateTenantSSOKey(mongo: Db, id: string) {
return result.value || null;
}
export async function deprecateTenantSSOKey(
mongo: Db,
id: string,
kid: string,
deprecateAt: Date
) {
// Update the tenant.
const result = await collection(mongo).findOneAndUpdate(
{ id },
{
$set: {
"auth.integrations.sso.keys.$[keys].deprecateAt": deprecateAt,
},
},
{
// False to return the updated document instead of the original
// document.
returnOriginal: false,
// Add an ArrayFilter to only update one of the keys.
arrayFilters: [{ "keys.kid": kid }],
}
);
return result.value || null;
}
+38 -4
View File
@@ -1,13 +1,13 @@
import cookie from "cookie";
import { DEFAULT_SESSION_LENGTH } from "coral-common/constants";
import { IncomingMessage } from "http";
import { Redis } from "ioredis";
import Joi from "joi";
import jwt, { SignOptions, VerifyOptions } from "jsonwebtoken";
import jwt, { KeyFunction, SignOptions, VerifyOptions } from "jsonwebtoken";
import { DateTime } from "luxon";
import { Bearer, BearerOptions } from "permit";
import uuid from "uuid/v4";
import { DEFAULT_SESSION_LENGTH } from "coral-common/constants";
import { Omit } from "coral-common/types";
import { Config } from "coral-server/config";
import {
@@ -20,7 +20,36 @@ import { User } from "coral-server/models/user";
import { Request } from "coral-server/types/express";
/**
* The following Claim Names are registered in the IANA "JSON Web Token
* The following Header Parameter names for use in JWSs are registered
* in the IANA "JSON Web Signature and Encryption Header Parameters"
* registry established by Section 9.1, with meanings as defined in the
* subsections below.
*
* As indicated by the common registry, JWSs and JWEs share a common
* Header Parameter space; when a parameter is used by both
* specifications, its usage must be compatible between the
* specifications.
*
* https://tools.ietf.org/html/rfc7515#section-4.1
*/
export interface StandardHeader {
/**
* The "kid" (key ID) Header Parameter is a hint indicating which key
* was used to secure the JWS. This parameter allows originators to
* explicitly signal a change of key to recipients. The structure of
* the "kid" value is unspecified. Its value MUST be a case-sensitive
* string. Use of this Header Parameter is OPTIONAL.
*
* When used with a JWK, the "kid" value is used to match a JWK "kid"
* parameter value.
*
* https://tools.ietf.org/html/rfc7515#section-4.1.4
*/
kid?: string;
}
/**
* The following Claim Names are registered in the IANA "JSON Web Token
* Claims" registry established by Section 10.1. None of the claims
* defined below are intended to be mandatory to use or implement in all
* cases, but rather they provide a starting point for a set of useful,
@@ -157,6 +186,11 @@ export interface JWTSigningConfig {
algorithm: JWTSigningAlgorithm;
}
export interface JWTVerifyingConfig {
secret: Buffer | string | KeyFunction;
algorithm: JWTSigningAlgorithm;
}
export function dateToSeconds(date: Date): number {
return Math.round(DateTime.fromJSDate(date).toSeconds());
}
@@ -408,7 +442,7 @@ export async function checkJWTRevoked(redis: Redis, jti: string) {
export function verifyJWT(
tokenString: string,
{ algorithm, secret }: JWTSigningConfig,
{ algorithm, secret }: JWTVerifyingConfig,
now: Date,
options: Omit<VerifyOptions, "algorithms" | "clockTimestamp"> = {}
) {
@@ -0,0 +1,67 @@
import { Db } from "mongodb";
import { SSOKey } from "coral-server/models/settings";
import { generateSSOKey } from "coral-server/models/tenant";
import Migration from "coral-server/services/migrate/migration";
import collections from "coral-server/services/mongodb/collections";
import { GQLTime } from "coral-server/graph/tenant/schema/__generated__/types";
import { MigrationError } from "../error";
interface Tenant {
auth: {
integrations: {
sso: {
key?: string;
keyGeneratedAt?: GQLTime;
};
};
};
}
export default class extends Migration {
public async up(mongo: Db, tenantID: string) {
// Get the Tenant so we can update it. We don't have to worry about two
// migration operations conflicting here because we will lock one instance
// to perform the operations.
const tenant = await collections
.tenants<Tenant>(mongo)
.findOne({ id: tenantID });
if (!tenant) {
throw new MigrationError(tenantID, "could not find tenant", "tenants", [
tenantID,
]);
}
// Store the keys in an array.
const keys: SSOKey[] = [];
// Check to see if a key is set.
const sso = tenant.auth.integrations.sso;
if (sso.key && sso.keyGeneratedAt) {
// Create the new SSOKey based on this data.
const key = generateSSOKey(sso.keyGeneratedAt);
// Set the secret of the sso key to the secret of the current set key.
key.secret = sso.key;
// Add this key to the set of keys.
keys.push(key);
}
// Update the tenant with the new sso keys.
await collections.tenants(mongo).updateOne(
{ id: tenantID },
{
$set: {
"auth.integrations.sso.keys": keys,
},
$unset: {
"auth.integrations.sso.key": "",
"auth.integrations.sso.keyGeneratedAt": "",
},
}
);
}
}
+35 -7
View File
@@ -1,25 +1,28 @@
import { Redis } from "ioredis";
import { isUndefined } from "lodash";
import { DateTime } from "luxon";
import { Db } from "mongodb";
import { URL } from "url";
import { discover } from "coral-server/app/middleware/passport/strategies/oidc/discover";
import { Config } from "coral-server/config";
import { TenantInstalledAlreadyError } from "coral-server/errors";
import {
GQLSettingsInput,
GQLSettingsWordListInput,
} from "coral-server/graph/tenant/schema/__generated__/types";
import logger from "coral-server/logger";
import {
createTenant,
CreateTenantInput,
regenerateTenantSSOKey,
createTenantSSOKey,
deprecateTenantSSOKey,
Tenant,
updateTenant,
} from "coral-server/models/tenant";
import { I18n } from "coral-server/services/i18n";
import {
GQLSettingsInput,
GQLSettingsWordListInput,
} from "coral-server/graph/tenant/schema/__generated__/types";
import TenantCache from "./cache";
export type UpdateTenant = GQLSettingsInput;
@@ -124,9 +127,34 @@ export async function regenerateSSOKey(
mongo: Db,
redis: Redis,
cache: TenantCache,
tenant: Tenant
tenant: Tenant,
now: Date
) {
const updatedTenant = await regenerateTenantSSOKey(mongo, tenant.id);
// Deprecate the old Tenant SSO key if it exists.
if (tenant.auth.integrations.sso.keys.length > 0) {
// Get the old keys that are not deprecated.
const keysToDeprecate = tenant.auth.integrations.sso.keys.filter(key => {
return !key.deletedAt && !key.deprecateAt && key.secret;
});
// Check to see if there are keys to deprecate.
if (keysToDeprecate.length > 0) {
// All the keys will be deprecated a month from now.
// TODO: [CORL-754] (wyattjoh) take input for the deprecation duration later.
const deprecateAt = DateTime.fromJSDate(now)
.plus({ month: 1 })
.toJSDate();
// Deprecate all the keys that are associated on the tenant that haven't
// been done.
for (const key of keysToDeprecate) {
await deprecateTenantSSOKey(mongo, tenant.id, key.kid, deprecateAt);
}
}
}
// Create the new Tenant.
const updatedTenant = await createTenantSSOKey(mongo, tenant.id, now);
if (!updatedTenant) {
return null;
}