mirror of
https://github.com/wassname/talk.git
synced 2026-07-29 11:28:24 +08:00
feat: added OIDC management mutations
This commit is contained in:
@@ -5,6 +5,7 @@ import { URL } from "url";
|
||||
* Configuration that Talk is expecting.
|
||||
*/
|
||||
export interface DiscoveryConfiguration {
|
||||
issuer: string;
|
||||
authorizationURL?: string;
|
||||
tokenURL?: string;
|
||||
jwksURI?: string;
|
||||
@@ -16,6 +17,7 @@ export interface DiscoveryConfiguration {
|
||||
* https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
*/
|
||||
interface DiscoveryRawConfiguration {
|
||||
issuer: string;
|
||||
authorization_endpoint?: string;
|
||||
token_endpoint?: string;
|
||||
jwks_uri?: string;
|
||||
@@ -40,6 +42,7 @@ export async function discover(issuer: URL): Promise<DiscoveryConfiguration> {
|
||||
const meta: DiscoveryRawConfiguration = await res.json();
|
||||
|
||||
return {
|
||||
issuer: meta.issuer,
|
||||
authorizationURL: meta.authorization_endpoint,
|
||||
tokenURL: meta.token_endpoint,
|
||||
jwksURI: meta.jwks_uri,
|
||||
|
||||
@@ -32,23 +32,26 @@ export interface OIDCIDToken {
|
||||
iss: string;
|
||||
sub: string;
|
||||
exp: number; // TODO: use this as the source for how long an OIDC user can be logged in for
|
||||
email?: string;
|
||||
email: string;
|
||||
email_verified?: boolean;
|
||||
picture?: string;
|
||||
name?: string;
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
export interface StrategyItem {
|
||||
strategy: OAuth2Strategy;
|
||||
jwksClient?: JwksClient;
|
||||
}
|
||||
export type StrategyItem = Record<
|
||||
string,
|
||||
{
|
||||
strategy: OAuth2Strategy;
|
||||
jwksClient?: JwksClient;
|
||||
}
|
||||
>;
|
||||
|
||||
export function isOIDCToken(token: OIDCIDToken | object): token is OIDCIDToken {
|
||||
if (
|
||||
(token as OIDCIDToken).iss &&
|
||||
(token as OIDCIDToken).sub &&
|
||||
(token as OIDCIDToken).email
|
||||
(token as OIDCIDToken).aud
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -85,9 +88,18 @@ const signingKeyFactory = (client: jwks.JwksClient): jwt.KeyFunction => (
|
||||
};
|
||||
|
||||
function getEnabledIntegration(
|
||||
tenant: Tenant
|
||||
tenant: Tenant,
|
||||
oidcID: string
|
||||
): Required<GQLOIDCAuthIntegration> {
|
||||
const integration = tenant.auth.integrations.oidc;
|
||||
if (!tenant.auth.integrations.oidc) {
|
||||
// TODO: return a better error.
|
||||
throw new Error("integration not found");
|
||||
}
|
||||
|
||||
// Grab the OIDC Integration from the list of integrations.
|
||||
const integration = tenant.auth.integrations.oidc.find(
|
||||
({ id }) => id === oidcID
|
||||
);
|
||||
if (!integration) {
|
||||
// TODO: return a better error.
|
||||
throw new Error("integration not found");
|
||||
@@ -135,6 +147,7 @@ export const OIDCDisplayNameIDTokenSchema = OIDCIDTokenSchema.keys({
|
||||
export async function findOrCreateOIDCUser(
|
||||
db: Db,
|
||||
tenant: Tenant,
|
||||
integration: GQLOIDCAuthIntegration,
|
||||
token: OIDCIDToken
|
||||
) {
|
||||
// Unpack/validate the token content.
|
||||
@@ -148,7 +161,7 @@ export async function findOrCreateOIDCUser(
|
||||
name,
|
||||
nickname,
|
||||
}: OIDCIDToken = validate(
|
||||
tenant.auth.integrations.oidc!.displayNameEnable
|
||||
integration.displayNameEnable
|
||||
? OIDCDisplayNameIDTokenSchema
|
||||
: OIDCIDTokenSchema,
|
||||
token
|
||||
@@ -215,34 +228,45 @@ export default class OIDCStrategy extends Strategy {
|
||||
req: Request,
|
||||
tenantID: string,
|
||||
oidc: Required<GQLOIDCAuthIntegration>
|
||||
) {
|
||||
let entry = this.cache.get(tenantID);
|
||||
if (!entry) {
|
||||
): jwks.JwksClient {
|
||||
let tenantIntegrations = this.cache.get(tenantID);
|
||||
if (!tenantIntegrations || !tenantIntegrations[oidc.id]) {
|
||||
const strategy = this.createStrategy(req, oidc);
|
||||
|
||||
// Create the entry.
|
||||
entry = {
|
||||
strategy,
|
||||
tenantIntegrations = {
|
||||
[oidc.id]: {
|
||||
strategy,
|
||||
},
|
||||
...(tenantIntegrations || {}),
|
||||
};
|
||||
|
||||
// We don't reset the entry in the cache here because if we just created
|
||||
// it, we'll be creating the jwksClient anyways, so we'll update it there.
|
||||
}
|
||||
|
||||
if (!entry.jwksClient) {
|
||||
const tenantIntegration = tenantIntegrations[oidc.id];
|
||||
|
||||
if (!tenantIntegration.jwksClient) {
|
||||
// Create the new JWKS client.
|
||||
const jwksClient = jwks({
|
||||
jwksUri: oidc.jwksURI,
|
||||
});
|
||||
|
||||
// Set the jwksClient on the entry.
|
||||
entry.jwksClient = jwksClient;
|
||||
tenantIntegration.jwksClient = jwksClient;
|
||||
|
||||
// Update the cached entry.
|
||||
this.cache.set(tenantID, entry);
|
||||
this.cache.set(tenantID, {
|
||||
[oidc.id]: {
|
||||
...tenantIntegration,
|
||||
jwksClient,
|
||||
},
|
||||
...tenantIntegrations,
|
||||
});
|
||||
}
|
||||
|
||||
return entry.jwksClient;
|
||||
return tenantIntegration.jwksClient;
|
||||
}
|
||||
|
||||
private userAuthenticatedCallback = (
|
||||
@@ -268,11 +292,14 @@ export default class OIDCStrategy extends Strategy {
|
||||
return done(new Error("tenant not found"));
|
||||
}
|
||||
|
||||
// Grab the OIDC ID from the request.
|
||||
const { oidcID }: { oidcID: string } = req.params;
|
||||
|
||||
// Get the integration from the tenant. If needed, it will be used to create
|
||||
// a new strategy.
|
||||
let integration: Required<GQLOIDCAuthIntegration>;
|
||||
try {
|
||||
integration = getEnabledIntegration(tenant);
|
||||
integration = getEnabledIntegration(tenant, oidcID);
|
||||
} catch (err) {
|
||||
// TODO: wrap error?
|
||||
return done(err);
|
||||
@@ -298,6 +325,7 @@ export default class OIDCStrategy extends Strategy {
|
||||
const user = await findOrCreateOIDCUser(
|
||||
this.mongo,
|
||||
tenant,
|
||||
integration,
|
||||
decoded as OIDCIDToken
|
||||
);
|
||||
return done(null, user);
|
||||
@@ -332,39 +360,45 @@ export default class OIDCStrategy extends Strategy {
|
||||
);
|
||||
}
|
||||
|
||||
private async lookupStrategy(req: Request) {
|
||||
private lookupStrategy(req: Request): OAuth2Strategy {
|
||||
const { tenant } = req.talk!;
|
||||
if (!tenant) {
|
||||
// TODO: return a better error.
|
||||
throw new Error("tenant not found");
|
||||
}
|
||||
|
||||
// Get the OIDC ID.
|
||||
const { oidcID }: { oidcID: string } = req.params;
|
||||
|
||||
// Get the integration from the tenant. If needed, it will be used to create
|
||||
// a new strategy.
|
||||
const integration = getEnabledIntegration(tenant);
|
||||
const integration = getEnabledIntegration(tenant, oidcID);
|
||||
|
||||
// Try to get the Tenant's cached integrations.
|
||||
let entry = this.cache.get(tenant.id);
|
||||
if (!entry) {
|
||||
let tenantIntegrations = this.cache.get(tenant.id);
|
||||
if (!tenantIntegrations || !tenantIntegrations[oidcID]) {
|
||||
// Create the strategy.
|
||||
const strategy = this.createStrategy(req, integration);
|
||||
|
||||
// Reset the entry.
|
||||
entry = {
|
||||
strategy,
|
||||
tenantIntegrations = {
|
||||
[oidcID]: {
|
||||
strategy,
|
||||
},
|
||||
...(tenantIntegrations || {}),
|
||||
};
|
||||
|
||||
// Update the cached integrations value.
|
||||
this.cache.set(tenant.id, entry);
|
||||
this.cache.set(tenant.id, tenantIntegrations);
|
||||
}
|
||||
|
||||
return entry.strategy;
|
||||
return tenantIntegrations[oidcID].strategy;
|
||||
}
|
||||
|
||||
public async authenticate(req: Request) {
|
||||
public authenticate(req: Request) {
|
||||
try {
|
||||
// Lookup the strategy.
|
||||
const strategy = await this.lookupStrategy(req);
|
||||
const strategy = this.lookupStrategy(req);
|
||||
if (!strategy) {
|
||||
throw new Error("strategy not found");
|
||||
}
|
||||
|
||||
@@ -29,9 +29,12 @@ export function createNewAuthRouter(app: AppOptions, options: RouterOptions) {
|
||||
signupHandler({ db: app.mongo, signingConfig: app.signingConfig })
|
||||
);
|
||||
|
||||
router.get("/oidc", wrapAuthn(options.passport, app.signingConfig, "oidc"));
|
||||
router.get(
|
||||
"/oidc/callback",
|
||||
"/oidc/:oidcID",
|
||||
wrapAuthn(options.passport, app.signingConfig, "oidc")
|
||||
);
|
||||
router.get(
|
||||
"/oidc/:oidcID/callback",
|
||||
wrapAuthn(options.passport, app.signingConfig, "oidc")
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
import { Request } from "talk-server/types/express";
|
||||
import { URL } from "url";
|
||||
|
||||
@@ -11,6 +12,18 @@ export function reconstructURL(req: Request, path: string = "/"): string {
|
||||
return url.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* reconstructTenantURL will reconstruct a URL based off of the Tenant's domain.
|
||||
*/
|
||||
export function reconstructTenantURL(
|
||||
tenant: Pick<Tenant, "domain">,
|
||||
path: string = "/"
|
||||
): string {
|
||||
const url = new URL(path, `https://${tenant.domain}`);
|
||||
|
||||
return url.href;
|
||||
}
|
||||
|
||||
export function doesRequireSchemePrefixing(url: string) {
|
||||
return !url.startsWith("http");
|
||||
}
|
||||
|
||||
@@ -2,15 +2,21 @@ import { isNull, omitBy } from "lodash";
|
||||
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import {
|
||||
GQLCreateOIDCAuthIntegrationInput,
|
||||
GQLDeleteOIDCAuthIntegrationInput,
|
||||
GQLDiscoverOIDCConfigurationInput,
|
||||
GQLOIDCConfiguration,
|
||||
GQLSettingsInput,
|
||||
GQLUpdateOIDCAuthIntegrationInput,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
import {
|
||||
createOIDCAuthIntegration,
|
||||
deleteOIDCAuthIntegration,
|
||||
discoverOIDCConfiguration,
|
||||
regenerateSSOKey,
|
||||
update,
|
||||
updateOIDCAuthIntegration,
|
||||
} from "talk-server/services/tenant";
|
||||
|
||||
export default ({ mongo, redis, tenantCache, tenant }: TenantContext) => ({
|
||||
@@ -21,4 +27,29 @@ export default ({ mongo, redis, tenantCache, tenant }: TenantContext) => ({
|
||||
discoverOIDCConfiguration: (
|
||||
input: GQLDiscoverOIDCConfigurationInput
|
||||
): Promise<GQLOIDCConfiguration> => discoverOIDCConfiguration(input.issuer),
|
||||
createOIDCAuthIntegration: (
|
||||
input: GQLCreateOIDCAuthIntegrationInput
|
||||
): Promise<Tenant | null> =>
|
||||
createOIDCAuthIntegration(
|
||||
mongo,
|
||||
redis,
|
||||
tenantCache,
|
||||
tenant,
|
||||
input.configuration
|
||||
),
|
||||
updateOIDCAuthIntegration: (
|
||||
input: GQLUpdateOIDCAuthIntegrationInput
|
||||
): Promise<Tenant | null> =>
|
||||
updateOIDCAuthIntegration(
|
||||
mongo,
|
||||
redis,
|
||||
tenantCache,
|
||||
tenant,
|
||||
input.id,
|
||||
input.configuration
|
||||
),
|
||||
deleteOIDCAuthIntegration: (
|
||||
input: GQLDeleteOIDCAuthIntegrationInput
|
||||
): Promise<Tenant | null> =>
|
||||
deleteOIDCAuthIntegration(mongo, redis, tenantCache, tenant, input.id),
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import AuthIntegrations from "./auth_integrations";
|
||||
import Comment from "./comment";
|
||||
import CommentCounts from "./comment_counts";
|
||||
import Mutation from "./mutation";
|
||||
import OIDCAuthIntegration from "./oidc_auth_integration";
|
||||
import Profile from "./profile";
|
||||
import Query from "./query";
|
||||
import User from "./user";
|
||||
@@ -19,10 +20,11 @@ const Resolvers: GQLResolver = {
|
||||
CommentCounts,
|
||||
Cursor,
|
||||
Mutation,
|
||||
OIDCAuthIntegration,
|
||||
Profile,
|
||||
Query,
|
||||
User,
|
||||
Time,
|
||||
User,
|
||||
};
|
||||
|
||||
export default Resolvers;
|
||||
|
||||
@@ -55,6 +55,18 @@ const Mutation: GQLMutationTypeResolver<void> = {
|
||||
configuration: await ctx.mutators.Settings.discoverOIDCConfiguration(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createOIDCAuthIntegration: async (source, { input }, ctx) => ({
|
||||
settings: await ctx.mutators.Settings.createOIDCAuthIntegration(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
updateOIDCAuthIntegration: async (source, { input }, ctx) => ({
|
||||
settings: await ctx.mutators.Settings.updateOIDCAuthIntegration(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
deleteOIDCAuthIntegration: async (source, { input }, ctx) => ({
|
||||
settings: await ctx.mutators.Settings.deleteOIDCAuthIntegration(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
};
|
||||
|
||||
export default Mutation;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { reconstructTenantURL, reconstructURL } from "talk-server/app/url";
|
||||
import {
|
||||
GQLOIDCAuthIntegration,
|
||||
GQLOIDCAuthIntegrationTypeResolver,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<
|
||||
GQLOIDCAuthIntegration
|
||||
> = {
|
||||
callbackURL: (integration, args, ctx) => {
|
||||
const path = `/api/tenant/auth/oidc/${integration.id}`;
|
||||
|
||||
// If the request is available, then prefer it over building from the tenant
|
||||
// as the tenant does not include the port number. This should only really
|
||||
// be a problem if the graph API is called internally.
|
||||
if (ctx.req) {
|
||||
return reconstructURL(ctx.req, path);
|
||||
}
|
||||
|
||||
// Note that when constructing the callback url with the tenant, the port
|
||||
// information is lost.
|
||||
return reconstructTenantURL(ctx.tenant, path);
|
||||
},
|
||||
};
|
||||
|
||||
export default OIDCAuthIntegration;
|
||||
@@ -317,6 +317,14 @@ OIDCAuthIntegration provides a way to store Open ID Connect credentials. This
|
||||
will be used in the admin to provide staff logins for users.
|
||||
"""
|
||||
type OIDCAuthIntegration {
|
||||
"""
|
||||
id is the identifier of the OIDCAuthIntegration.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
enabled, when true, allows the integration to be enabled.
|
||||
"""
|
||||
enabled: Boolean!
|
||||
|
||||
"""
|
||||
@@ -330,12 +338,20 @@ type OIDCAuthIntegration {
|
||||
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])
|
||||
authorizationURL: String @auth(roles: [ADMIN])
|
||||
tokenURL: String @auth(roles: [ADMIN])
|
||||
jwksURI: String @auth(roles: [ADMIN])
|
||||
issuer: String @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
callbackURL is the URL that the user should be redirected to in order to start
|
||||
an authentication flow with the given integration. This field is not stored,
|
||||
and is instead computed from the Tenant.
|
||||
"""
|
||||
callbackURL: String!
|
||||
|
||||
clientID: String! @auth(roles: [ADMIN])
|
||||
clientSecret: String! @auth(roles: [ADMIN])
|
||||
authorizationURL: String! @auth(roles: [ADMIN])
|
||||
tokenURL: String! @auth(roles: [ADMIN])
|
||||
jwksURI: String! @auth(roles: [ADMIN])
|
||||
issuer: String! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
displayNameEnable when enabled, will allow Users to set and view their
|
||||
@@ -383,7 +399,7 @@ type FacebookAuthIntegration {
|
||||
type AuthIntegrations {
|
||||
local: LocalAuthIntegration!
|
||||
sso: SSOAuthIntegration!
|
||||
oidc: OIDCAuthIntegration!
|
||||
oidc: [OIDCAuthIntegration!]!
|
||||
google: GoogleAuthIntegration!
|
||||
facebook: FacebookAuthIntegration!
|
||||
}
|
||||
@@ -1349,33 +1365,33 @@ input SettingsSSOAuthIntegrationInput {
|
||||
displayNameEnable: Boolean
|
||||
}
|
||||
|
||||
input SettingsOIDCAuthIntegrationInput {
|
||||
enabled: Boolean
|
||||
# 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
|
||||
# """
|
||||
# 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
|
||||
# """
|
||||
# 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
|
||||
displayName's.
|
||||
"""
|
||||
displayNameEnable: Boolean
|
||||
}
|
||||
# """
|
||||
# displayNameEnable when enabled, will allow Users to set and view their
|
||||
# displayName's.
|
||||
# """
|
||||
# displayNameEnable: Boolean
|
||||
# }
|
||||
|
||||
input SettingsGoogleAuthIntegrationInput {
|
||||
enabled: Boolean
|
||||
@@ -1408,7 +1424,6 @@ input SettingsFacebookAuthIntegrationInput {
|
||||
input SettingsAuthIntegrationsInput {
|
||||
local: SettingsLocalAuthIntegrationInput
|
||||
sso: SettingsSSOAuthIntegrationInput
|
||||
oidc: SettingsOIDCAuthIntegrationInput
|
||||
google: SettingsGoogleAuthIntegrationInput
|
||||
facebook: SettingsFacebookAuthIntegrationInput
|
||||
}
|
||||
@@ -1917,6 +1932,7 @@ input DiscoverOIDCConfigurationInput {
|
||||
}
|
||||
|
||||
type OIDCConfiguration {
|
||||
issuer: String!
|
||||
authorizationURL: String
|
||||
tokenURL: String
|
||||
jwksURI: String
|
||||
@@ -1935,6 +1951,117 @@ type DiscoverOIDCConfigurationPayload {
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# createOIDCAuthIntegration
|
||||
##################
|
||||
|
||||
input CreateOIDCAuthIntegrationConfigurationInput {
|
||||
name: String!
|
||||
clientID: String!
|
||||
clientSecret: String!
|
||||
authorizationURL: String!
|
||||
tokenURL: String!
|
||||
jwksURI: String!
|
||||
issuer: String!
|
||||
}
|
||||
|
||||
input CreateOIDCAuthIntegrationInput {
|
||||
"""
|
||||
configuration contains the configuration to be used to create the auth integration.
|
||||
"""
|
||||
configuration: CreateOIDCAuthIntegrationConfigurationInput!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type CreateOIDCAuthIntegrationPayload {
|
||||
"""
|
||||
settings is the Settings that the OIDCAuthIntegration was created on.
|
||||
"""
|
||||
settings: Settings
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# updateOIDCAuthIntegration
|
||||
##################
|
||||
|
||||
input UpdateOIDCAuthIntegrationConfigurationInput {
|
||||
enabled: Boolean
|
||||
name: String
|
||||
clientID: String
|
||||
clientSecret: String
|
||||
authorizationURL: String
|
||||
tokenURL: String
|
||||
jwksURI: String
|
||||
issuer: String
|
||||
}
|
||||
|
||||
input UpdateOIDCAuthIntegrationInput {
|
||||
"""
|
||||
id is the ID of the specific OpenID Connect integration that we are updating.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
configuration contains the configuration to be used to update the OpenID
|
||||
Connect integration.
|
||||
"""
|
||||
configuration: UpdateOIDCAuthIntegrationConfigurationInput!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type UpdateOIDCAuthIntegrationPayload {
|
||||
"""
|
||||
settings is the Settings that the OIDCAuthIntegration was updated on.
|
||||
"""
|
||||
settings: Settings
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# deleteOIDCAuthIntegration
|
||||
##################
|
||||
|
||||
input DeleteOIDCAuthIntegrationInput {
|
||||
"""
|
||||
id is the ID of the specific OpenID Connect integration that we are deleting.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type DeleteOIDCAuthIntegrationPayload {
|
||||
"""
|
||||
settings is the Settings that the OIDCAuthIntegration was deleted on.
|
||||
"""
|
||||
settings: Settings
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## Mutation
|
||||
##################
|
||||
@@ -1964,10 +2091,36 @@ type Mutation {
|
||||
regenerateSSOKey(input: RegenerateSSOKeyInput!): RegenerateSSOKeyPayload
|
||||
@auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
discoverOIDCConfiguration will discover the OpenID Connect configuration based
|
||||
on the provided input.
|
||||
"""
|
||||
discoverOIDCConfiguration(
|
||||
input: DiscoverOIDCConfigurationInput!
|
||||
): DiscoverOIDCConfigurationPayload @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
createOIDCAuthIntegration will create a OpenID Connect auth integration.
|
||||
"""
|
||||
createOIDCAuthIntegration(
|
||||
input: CreateOIDCAuthIntegrationInput!
|
||||
): CreateOIDCAuthIntegrationPayload @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
updateOIDCAuthIntegration will update a given OpenID Connect auth integration.
|
||||
"""
|
||||
updateOIDCAuthIntegration(
|
||||
input: UpdateOIDCAuthIntegrationInput!
|
||||
): UpdateOIDCAuthIntegrationPayload @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
deleteOIDCAuthIntegration will delete the specified OpenID Connect auth
|
||||
integration.
|
||||
"""
|
||||
deleteOIDCAuthIntegration(
|
||||
input: DeleteOIDCAuthIntegrationInput!
|
||||
): DeleteOIDCAuthIntegrationPayload @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
createCommentReaction will create a Reaction authored by the current logged in
|
||||
User on a Comment.
|
||||
|
||||
@@ -4,7 +4,10 @@ import uuid from "uuid";
|
||||
|
||||
import { DeepPartial, Omit, Sub } from "talk-common/types";
|
||||
import { dotize } from "talk-common/utils/dotize";
|
||||
import { GQLMODERATION_MODE } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
GQLMODERATION_MODE,
|
||||
GQLOIDCAuthIntegration,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Settings } from "talk-server/models/settings";
|
||||
|
||||
function collection(db: Db) {
|
||||
@@ -87,9 +90,7 @@ export async function createTenant(mongo: Db, input: CreateTenantInput) {
|
||||
sso: {
|
||||
enabled: false,
|
||||
},
|
||||
oidc: {
|
||||
enabled: false,
|
||||
},
|
||||
oidc: [],
|
||||
google: {
|
||||
enabled: false,
|
||||
},
|
||||
@@ -245,3 +246,95 @@ export async function regenerateTenantSSOKey(db: Db, id: string) {
|
||||
|
||||
return result.value || null;
|
||||
}
|
||||
|
||||
export type CreateTenantOIDCAuthIntegrationInput = Omit<
|
||||
GQLOIDCAuthIntegration,
|
||||
"id" | "callbackURL"
|
||||
>;
|
||||
|
||||
export async function createTenantOIDCAuthIntegration(
|
||||
mongo: Db,
|
||||
id: string,
|
||||
input: CreateTenantOIDCAuthIntegrationInput
|
||||
) {
|
||||
// Add the ID to the integration.
|
||||
const integration = {
|
||||
id: uuid.v4(),
|
||||
...input,
|
||||
};
|
||||
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{ id },
|
||||
// Serialize the deep update into the Tenant.
|
||||
{
|
||||
$push: { "auth.integrations.oidc": integration },
|
||||
},
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
{ returnOriginal: false }
|
||||
);
|
||||
|
||||
return result.value || null;
|
||||
}
|
||||
|
||||
export type UpdateTenantOIDCAuthIntegrationInput = Partial<
|
||||
Omit<GQLOIDCAuthIntegration, "id">
|
||||
>;
|
||||
|
||||
export async function updateTenantOIDCAuthIntegration(
|
||||
mongo: Db,
|
||||
id: string,
|
||||
oidcID: string,
|
||||
input: UpdateTenantOIDCAuthIntegrationInput
|
||||
) {
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{ id },
|
||||
{
|
||||
// $set: dotize({
|
||||
// "auth.integrations.oidc.$[oidc]": input,
|
||||
// }),
|
||||
// FIXME: replace with the above one once the types are updated.
|
||||
$set: dotize({
|
||||
"auth.integrations.oidc.$[]": input,
|
||||
}),
|
||||
},
|
||||
{
|
||||
// Add an ArrayFilter to only update one of the OpenID Connect
|
||||
// integrations.
|
||||
// arrayFilters: [{ "oidc.id": oidcID }], // FIXME: add back when we got the mongo fixes in place
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
returnOriginal: false,
|
||||
}
|
||||
);
|
||||
|
||||
return result.value || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* deleteTenantOIDCAuthIntegration will delete the specific OpenID Connect Auth
|
||||
* Integration on the Tenant.
|
||||
*
|
||||
* @param mongo MongoDB Database handle
|
||||
* @param id the id of the Tenant
|
||||
* @param oidcID the id of the OpenID Connect Auth Integration we're deleting
|
||||
*/
|
||||
export async function deleteTenantOIDCAuthIntegration(
|
||||
mongo: Db,
|
||||
id: string,
|
||||
oidcID: string
|
||||
) {
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{ id },
|
||||
{
|
||||
$pull: { "auth.integrations.oidc": { id: oidcID } },
|
||||
},
|
||||
{
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
returnOriginal: false,
|
||||
}
|
||||
);
|
||||
|
||||
return result.value || null;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,20 @@ import { Redis } from "ioredis";
|
||||
import { Db } from "mongodb";
|
||||
import { URL } from "url";
|
||||
|
||||
import { GQLSettingsInput } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
GQLCreateOIDCAuthIntegrationConfigurationInput,
|
||||
GQLSettingsInput,
|
||||
GQLUpdateOIDCAuthIntegrationConfigurationInput,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
createTenant,
|
||||
CreateTenantInput,
|
||||
createTenantOIDCAuthIntegration,
|
||||
deleteTenantOIDCAuthIntegration,
|
||||
regenerateTenantSSOKey,
|
||||
Tenant,
|
||||
updateTenant,
|
||||
updateTenantOIDCAuthIntegration,
|
||||
} from "talk-server/models/tenant";
|
||||
|
||||
import { discover } from "talk-server/app/middleware/passport/strategies/oidc/discover";
|
||||
@@ -116,3 +123,81 @@ export async function discoverOIDCConfiguration(issuerString: string) {
|
||||
// Discover the configuration.
|
||||
return discover(issuer);
|
||||
}
|
||||
|
||||
export type CreateOIDCAuthIntegration = GQLCreateOIDCAuthIntegrationConfigurationInput;
|
||||
|
||||
export async function createOIDCAuthIntegration(
|
||||
mongo: Db,
|
||||
redis: Redis,
|
||||
cache: TenantCache,
|
||||
tenant: Tenant,
|
||||
input: CreateOIDCAuthIntegration
|
||||
) {
|
||||
// Create the integration. By default, the integration is disabled.
|
||||
const updatedTenant = await createTenantOIDCAuthIntegration(
|
||||
mongo,
|
||||
tenant.id,
|
||||
{
|
||||
enabled: false,
|
||||
...input,
|
||||
}
|
||||
);
|
||||
if (!updatedTenant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update the tenant cache.
|
||||
await cache.update(redis, updatedTenant);
|
||||
|
||||
return updatedTenant;
|
||||
}
|
||||
|
||||
export type UpdateOIDCAuthIntegration = GQLUpdateOIDCAuthIntegrationConfigurationInput;
|
||||
|
||||
export async function updateOIDCAuthIntegration(
|
||||
mongo: Db,
|
||||
redis: Redis,
|
||||
cache: TenantCache,
|
||||
tenant: Tenant,
|
||||
oidcID: string,
|
||||
input: UpdateOIDCAuthIntegration
|
||||
) {
|
||||
// Update the integration. By default, the integration is disabled.
|
||||
const updatedTenant = await updateTenantOIDCAuthIntegration(
|
||||
mongo,
|
||||
tenant.id,
|
||||
oidcID,
|
||||
input
|
||||
);
|
||||
if (!updatedTenant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update the tenant cache.
|
||||
await cache.update(redis, updatedTenant);
|
||||
|
||||
return updatedTenant;
|
||||
}
|
||||
|
||||
export async function deleteOIDCAuthIntegration(
|
||||
mongo: Db,
|
||||
redis: Redis,
|
||||
cache: TenantCache,
|
||||
tenant: Tenant,
|
||||
oidcID: string
|
||||
) {
|
||||
// Delete the integration. By default, the integration is disabled.
|
||||
const updatedTenant = await deleteTenantOIDCAuthIntegration(
|
||||
mongo,
|
||||
tenant.id,
|
||||
oidcID
|
||||
);
|
||||
if (!updatedTenant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update the tenant cache.
|
||||
await cache.update(redis, updatedTenant);
|
||||
|
||||
return updatedTenant;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user