diff --git a/src/core/server/app/middleware/passport/strategies/oidc/discover.ts b/src/core/server/app/middleware/passport/strategies/oidc/discover.ts index ab12fa59c..3fe4679a6 100644 --- a/src/core/server/app/middleware/passport/strategies/oidc/discover.ts +++ b/src/core/server/app/middleware/passport/strategies/oidc/discover.ts @@ -6,9 +6,9 @@ import { URL } from "url"; */ export interface DiscoveryConfiguration { issuer: string; - authorizationURL?: string; + authorizationURL: string; tokenURL?: string; - jwksURI?: string; + jwksURI: string; } /** @@ -18,9 +18,9 @@ export interface DiscoveryConfiguration { */ interface DiscoveryRawConfiguration { issuer: string; - authorization_endpoint?: string; + authorization_endpoint: string; token_endpoint?: string; - jwks_uri?: string; + jwks_uri: string; } /** @@ -29,7 +29,9 @@ interface DiscoveryRawConfiguration { * @param issuer the Issuer URL that should be used to determine the * configuration */ -export async function discover(issuer: URL): Promise { +export async function discover( + issuer: URL +): Promise { // Any provider MUST provide a .well-known url that is JSON parsable based // on the issuer: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig const configurationURL = @@ -38,13 +40,23 @@ export async function discover(issuer: URL): Promise { "/.well-known/openid-configuration"; const res = await fetch(configurationURL); - // Parse the configuration - const meta: DiscoveryRawConfiguration = await res.json(); + // Ensure that it responds correctly. + if (res.status !== 200) { + return null; + } - return { - issuer: meta.issuer, - authorizationURL: meta.authorization_endpoint, - tokenURL: meta.token_endpoint, - jwksURI: meta.jwks_uri, - }; + try { + // Parse the configuration + const meta: DiscoveryRawConfiguration = await res.json(); + + return { + issuer: meta.issuer, + authorizationURL: meta.authorization_endpoint, + tokenURL: meta.token_endpoint, + jwksURI: meta.jwks_uri, + }; + } catch (err) { + // TODO: log the error + return null; + } } diff --git a/src/core/server/graph/tenant/loaders/auth.ts b/src/core/server/graph/tenant/loaders/auth.ts new file mode 100644 index 000000000..9b13a9cda --- /dev/null +++ b/src/core/server/graph/tenant/loaders/auth.ts @@ -0,0 +1,14 @@ +import DataLoader from "dataloader"; + +import TenantContext from "talk-server/graph/tenant/context"; +import { GQLDiscoveredOIDCConfiguration } from "talk-server/graph/tenant/schema/__generated__/types"; +import { discoverOIDCConfiguration } from "talk-server/services/tenant"; + +export default (ctx: TenantContext) => ({ + discoverOIDCConfiguration: new DataLoader< + string, + GQLDiscoveredOIDCConfiguration | null + >(issuers => + Promise.all(issuers.map(issuer => discoverOIDCConfiguration(issuer))) + ), +}); diff --git a/src/core/server/graph/tenant/loaders/index.ts b/src/core/server/graph/tenant/loaders/index.ts index d4a00cc28..1233b54c7 100644 --- a/src/core/server/graph/tenant/loaders/index.ts +++ b/src/core/server/graph/tenant/loaders/index.ts @@ -1,9 +1,11 @@ import Context from "talk-server/graph/tenant/context"; import Assets from "./assets"; +import Auth from "./auth"; import Comments from "./comments"; import Users from "./users"; export default (ctx: Context) => ({ + Auth: Auth(ctx), Assets: Assets(ctx), Comments: Comments(ctx), Users: Users(ctx), diff --git a/src/core/server/graph/tenant/mutators/settings.ts b/src/core/server/graph/tenant/mutators/settings.ts index d1ceaaef3..065e709d3 100644 --- a/src/core/server/graph/tenant/mutators/settings.ts +++ b/src/core/server/graph/tenant/mutators/settings.ts @@ -4,8 +4,6 @@ import TenantContext from "talk-server/graph/tenant/context"; import { GQLCreateOIDCAuthIntegrationInput, GQLDeleteOIDCAuthIntegrationInput, - GQLDiscoverOIDCConfigurationInput, - GQLOIDCConfiguration, GQLSettingsInput, GQLUpdateOIDCAuthIntegrationInput, } from "talk-server/graph/tenant/schema/__generated__/types"; @@ -13,7 +11,6 @@ import { Tenant } from "talk-server/models/tenant"; import { createOIDCAuthIntegration, deleteOIDCAuthIntegration, - discoverOIDCConfiguration, regenerateSSOKey, update, updateOIDCAuthIntegration, @@ -24,9 +21,6 @@ export default ({ mongo, redis, tenantCache, tenant }: TenantContext) => ({ update(mongo, redis, tenantCache, tenant, omitBy(input, isNull)), regenerateSSOKey: (): Promise => regenerateSSOKey(mongo, redis, tenantCache, tenant), - discoverOIDCConfiguration: ( - input: GQLDiscoverOIDCConfigurationInput - ): Promise => discoverOIDCConfiguration(input.issuer), createOIDCAuthIntegration: ( input: GQLCreateOIDCAuthIntegrationInput ): Promise => diff --git a/src/core/server/graph/tenant/resolvers/mutation.ts b/src/core/server/graph/tenant/resolvers/mutation.ts index 7781d7689..cea0de73d 100644 --- a/src/core/server/graph/tenant/resolvers/mutation.ts +++ b/src/core/server/graph/tenant/resolvers/mutation.ts @@ -51,10 +51,6 @@ const Mutation: GQLMutationTypeResolver = { settings: await ctx.mutators.Settings.regenerateSSOKey(), clientMutationId: input.clientMutationId, }), - discoverOIDCConfiguration: async (source, { input }, ctx) => ({ - 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, diff --git a/src/core/server/graph/tenant/resolvers/query.ts b/src/core/server/graph/tenant/resolvers/query.ts index 295b6c459..73ed1d2e5 100644 --- a/src/core/server/graph/tenant/resolvers/query.ts +++ b/src/core/server/graph/tenant/resolvers/query.ts @@ -6,6 +6,8 @@ const Query: GQLQueryTypeResolver = { id ? ctx.loaders.Comments.comment.load(id) : null, settings: (source, args, ctx) => ctx.tenant, me: (source, args, ctx) => ctx.user, + discoverOIDCConfiguration: (source, { issuer }, ctx) => + ctx.loaders.Auth.discoverOIDCConfiguration.load(issuer), }; export default Query; diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index b29921796..2f88ed512 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -312,6 +312,47 @@ type SSOAuthIntegration { ## OIDCAuthIntegration ########################## +""" +DiscoveredOIDCConfiguration contains the discovered Provider Metadata as defined +in: + +https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + +Discovery is not supported on all providers, and is described in the OpenID +Connect Discovery 1.0 incorporating errata set 1: + +https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig +""" +type DiscoveredOIDCConfiguration { + """ + issuer is defined as the `issuer` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ + issuer: String! + + """ + authorizationURL is defined as the `authorization_endpoint` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ + authorizationURL: String! + + """ + tokenURL is defined as the `token_endpoint` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ + tokenURL: String + + """ + jwksURI is defined as the `jwks_uri` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ + jwksURI: String! +} + """ OIDCAuthIntegration provides a way to store Open ID Connect credentials. This will be used in the admin to provide staff logins for users. @@ -335,7 +376,9 @@ type OIDCAuthIntegration { targetFilter: AuthenticationTargetFilter """ - name is the label assigned to reference the provider of the OIDC integration. + name is the label assigned to reference the provider of the OIDC integration, + and will be used in situations where the name of the provider needs to be + displayed, like the login button. """ name: String @@ -346,11 +389,46 @@ type OIDCAuthIntegration { """ callbackURL: String! + """ + clientID is the Client Identifier as defined in: + + https://tools.ietf.org/html/rfc6749#section-2.2 + """ clientID: String! @auth(roles: [ADMIN]) + + """ + clientSecret is the Client Secret as defined in: + + https://tools.ietf.org/html/rfc6749#section-2.3.1 + """ clientSecret: String! @auth(roles: [ADMIN]) + + """ + authorizationURL is defined as the `authorization_endpoint` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ authorizationURL: String! @auth(roles: [ADMIN]) + + """ + tokenURL is defined as the `token_endpoint` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ tokenURL: String! @auth(roles: [ADMIN]) + + """ + jwksURI is defined as the `jwks_uri` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ jwksURI: String! @auth(roles: [ADMIN]) + + """ + issuer is defined as the `issuer` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ issuer: String! @auth(roles: [ADMIN]) """ @@ -1200,7 +1278,8 @@ type Query { asset(id: ID, url: String): Asset """ - me is the current logged in User. + me is the current logged in User. If no user is currently logged in, it will + return null. """ me: User @@ -1208,6 +1287,18 @@ type Query { settings is the Settings for a given Tenant. """ settings: Settings! + + """ + discoverOIDCConfiguration will discover the OpenID Connect configuration based + on the provided issuer. Discovery is not supported on all providers, and is + described in the OpenID Connect Discovery 1.0 incorporating errata set 1: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig + + If the provider does not support discovery, the response will be null. + """ + discoverOIDCConfiguration(issuer: String!): DiscoveredOIDCConfiguration + @auth(roles: [ADMIN]) } ################################################################################ @@ -1365,34 +1456,6 @@ input SettingsSSOAuthIntegrationInput { displayNameEnable: 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 - -# """ -# 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 -# } - input SettingsGoogleAuthIntegrationInput { enabled: Boolean @@ -1915,53 +1978,58 @@ type RegenerateSSOKeyPayload { clientMutationId: String! } -################## -## discoverOIDCConfiguration -################## - -input DiscoverOIDCConfigurationInput { - """ - - """ - issuer: String! - - """ - clientMutationId is required for Relay support. - """ - clientMutationId: String! -} - -type OIDCConfiguration { - issuer: String! - authorizationURL: String - tokenURL: String - jwksURI: String -} - -type DiscoverOIDCConfigurationPayload { - """ - configuration was the discovered configuration for the OpenID Connect server - given the provided issuer. - """ - configuration: OIDCConfiguration - - """ - clientMutationId is required for Relay support. - """ - clientMutationId: String! -} - ################## # createOIDCAuthIntegration ################## input CreateOIDCAuthIntegrationConfigurationInput { + """ + name is the label assigned to reference the provider of the OIDC integration, + and will be used in situations where the name of the provider needs to be + displayed, like the login button. + """ name: String! + + """ + clientID is the Client Identifier as defined in: + + https://tools.ietf.org/html/rfc6749#section-2.2 + """ clientID: String! + + """ + clientSecret is the Client Secret as defined in: + + https://tools.ietf.org/html/rfc6749#section-2.3.1 + """ clientSecret: String! + + """ + authorizationURL is defined as the `authorization_endpoint` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ authorizationURL: String! + + """ + tokenURL is defined as the `token_endpoint` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ tokenURL: String! + + """ + jwksURI is defined as the `jwks_uri` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ jwksURI: String! + + """ + issuer is defined as the `issuer` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ issuer: String! } @@ -1994,13 +2062,65 @@ type CreateOIDCAuthIntegrationPayload { ################## input UpdateOIDCAuthIntegrationConfigurationInput { + """ + enabled, when true, allows the integration to be enabled. + """ 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, + and will be used in situations where the name of the provider needs to be + displayed, like the login button. + """ name: String + + """ + clientID is the Client Identifier as defined in: + + https://tools.ietf.org/html/rfc6749#section-2.2 + """ clientID: String + + """ + clientSecret is the Client Secret as defined in: + + https://tools.ietf.org/html/rfc6749#section-2.3.1 + """ clientSecret: String + + """ + authorizationURL is defined as the `authorization_endpoint` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ authorizationURL: String + + """ + tokenURL is defined as the `token_endpoint` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ tokenURL: String + + """ + jwksURI is defined as the `jwks_uri` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ jwksURI: String + + """ + issuer is defined as the `issuer` in: + + https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + """ issuer: String } @@ -2091,14 +2211,6 @@ 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. """ diff --git a/src/core/server/models/tenant.ts b/src/core/server/models/tenant.ts index e17fa1e5f..e6a12f8df 100644 --- a/src/core/server/models/tenant.ts +++ b/src/core/server/models/tenant.ts @@ -293,7 +293,7 @@ export async function updateTenantOIDCAuthIntegration( // $set: dotize({ // "auth.integrations.oidc.$[oidc]": input, // }), - // FIXME: replace with the above one once the types are updated. + // FIXME: uncomment when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29986 gets merged $set: dotize({ "auth.integrations.oidc.$[]": input, }), @@ -301,7 +301,7 @@ export async function updateTenantOIDCAuthIntegration( { // 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 + // arrayFilters: [{ "oidc.id": oidcID }], // FIXME: uncomment when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29986 gets merged // False to return the updated document instead of the original // document. returnOriginal: false,