mirror of
https://github.com/wassname/talk.git
synced 2026-07-09 08:00:17 +08:00
feat: added initial server sso key rotation semantics (#2696)
This commit is contained in:
committed by
Kim Gardner
parent
59b8dfccda
commit
2dbba52fbd
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user