feat: added OIDC management mutations

This commit is contained in:
Wyatt Johnson
2018-10-24 16:54:07 -06:00
parent 0bb64f1d97
commit 45cbac7972
11 changed files with 524 additions and 69 deletions
@@ -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");
}
+5 -2
View File
@@ -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")
);
+13
View File
@@ -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.
+97 -4
View File
@@ -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;
}
+86 -1
View File
@@ -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;
}