From b29d97633061f0bd1a067afdbeda18a651493235 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 28 Dec 2021 14:35:11 +0100 Subject: [PATCH 1/4] auth-backend: refactor auth0 to use sign-in resolver Signed-off-by: Patrik Oldsberg --- .../src/providers/auth0/provider.ts | 153 ++++++++++++++---- plugins/auth-backend/src/providers/index.ts | 7 +- 2 files changed, 122 insertions(+), 38 deletions(-) diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 7aa98c3e65..4293e3e481 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -36,7 +36,15 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { RedirectInfo, AuthProviderFactory } from '../types'; +import { + RedirectInfo, + AuthProviderFactory, + AuthHandler, + SignInResolver, +} from '../types'; +import { CatalogIdentityClient } from '../../lib/catalog'; +import { TokenIssuer } from '../../identity'; +import { Logger } from 'winston'; type PrivateInfo = { refreshToken: string; @@ -44,12 +52,27 @@ type PrivateInfo = { export type Auth0AuthProviderOptions = OAuthProviderOptions & { domain: string; + signInResolver?: SignInResolver; + authHandler: AuthHandler; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger; }; export class Auth0AuthProvider implements OAuthHandlers { private readonly _strategy: Auth0Strategy; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + private readonly tokenIssuer: TokenIssuer; + private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly logger: Logger; constructor(options: Auth0AuthProviderOptions) { + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.tokenIssuer = options.tokenIssuer; + this.catalogIdentityClient = options.catalogIdentityClient; + this.logger = options.logger; this._strategy = new Auth0Strategy( { clientID: options.clientId, @@ -98,18 +121,8 @@ export class Auth0AuthProvider implements OAuthHandlers { PrivateInfo >(req, this._strategy); - const profile = makeProfileInfo(result.fullProfile, result.params.id_token); - return { - response: await this.populateIdentity({ - profile, - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - }), + response: await this.handleResult(result), refreshToken: privateInfo.refreshToken, }; } @@ -125,53 +138,123 @@ export class Auth0AuthProvider implements OAuthHandlers { this._strategy, accessToken, ); - const profile = makeProfileInfo(fullProfile, params.id_token); - return this.populateIdentity({ - providerInfo: { - accessToken, - idToken: params.id_token, - expiresInSeconds: params.expires_in, - scope: params.scope, - }, - profile, + return this.handleResult({ + fullProfile, + params, + accessToken, + refreshToken: req.refreshToken, }); } - // Use this function to grab the user profile info from the token - // Then populate the profile with it - private async populateIdentity( - response: OAuthResponse, - ): Promise { - const { profile } = response; + private async handleResult(result: OAuthResult) { + const { profile } = await this.authHandler(result); - if (!profile.email) { - throw new Error('Profile does not contain an email'); + const response: OAuthResponse = { + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + profile, + }; + + if (this.signInResolver) { + response.backstageIdentity = await this.signInResolver( + { + result, + profile, + }, + { + tokenIssuer: this.tokenIssuer, + catalogIdentityClient: this.catalogIdentityClient, + logger: this.logger, + }, + ); } - const id = profile.email.split('@')[0]; - - return { ...response, backstageIdentity: { id, token: '' } }; + return response; } } -export type Auth0ProviderOptions = {}; +const defaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Profile does not contain an email'); + } + + const id = profile.email.split('@')[0]; + + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: id, ent: [`user:default/${id}`] }, + }); + + return { id, token }; +}; + +export type Auth0ProviderOptions = { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}; export const createAuth0Provider = ( - _options?: Auth0ProviderOptions, + options?: Auth0ProviderOptions, ): AuthProviderFactory => { - return ({ providerId, globalConfig, config, tokenIssuer }) => + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + catalogApi, + logger, + }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const domain = envConfig.getString('domain'); const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver; + const provider = new Auth0AuthProvider({ clientId, clientSecret, callbackUrl, domain, + authHandler, + signInResolver, + tokenIssuer, + catalogIdentityClient, + logger, }); return OAuthAdapter.fromConfig(globalConfig, provider, { diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 9589e97265..aba41bdb73 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +export * from './atlassian'; +export * from './auth0'; +export * from './aws-alb'; +export * from './bitbucket'; export * from './github'; export * from './gitlab'; export * from './google'; @@ -21,9 +25,6 @@ export * from './microsoft'; export * from './oauth2'; export * from './oidc'; export * from './okta'; -export * from './bitbucket'; -export * from './atlassian'; -export * from './aws-alb'; export * from './saml'; export { factories as defaultAuthProviderFactories } from './factories'; From 04b1d4be44ee435dd138835db90a1e0761fb5c2d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 28 Dec 2021 15:26:36 +0100 Subject: [PATCH 2/4] auth-backend: refactor onelogin to use sign-in resolver Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/providers/index.ts | 1 + .../src/providers/onelogin/provider.ts | 150 ++++++++++++++---- 2 files changed, 118 insertions(+), 33 deletions(-) diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index aba41bdb73..3f71dd2c26 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -25,6 +25,7 @@ export * from './microsoft'; export * from './oauth2'; export * from './oidc'; export * from './okta'; +export * from './onelogin'; export * from './saml'; export { factories as defaultAuthProviderFactories } from './factories'; diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 66e8b0bfc5..97aec51a1e 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -36,7 +36,15 @@ import { executeFetchUserProfileStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { RedirectInfo, AuthProviderFactory } from '../types'; +import { + RedirectInfo, + AuthProviderFactory, + AuthHandler, + SignInResolver, +} from '../types'; +import { CatalogIdentityClient } from '../../lib/catalog'; +import { Logger } from 'winston'; +import { TokenIssuer } from '../../identity'; type PrivateInfo = { refreshToken: string; @@ -44,12 +52,27 @@ type PrivateInfo = { export type Options = OAuthProviderOptions & { issuer: string; + signInResolver?: SignInResolver; + authHandler: AuthHandler; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger; }; export class OneLoginProvider implements OAuthHandlers { private readonly _strategy: any; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + private readonly tokenIssuer: TokenIssuer; + private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly logger: Logger; constructor(options: Options) { + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.tokenIssuer = options.tokenIssuer; + this.catalogIdentityClient = options.catalogIdentityClient; + this.logger = options.logger; this._strategy = new OneLoginStrategy( { issuer: options.issuer, @@ -97,18 +120,8 @@ export class OneLoginProvider implements OAuthHandlers { PrivateInfo >(req, this._strategy); - const profile = makeProfileInfo(result.fullProfile, result.params.id_token); - return { - response: await this.populateIdentity({ - profile, - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - }), + response: await this.handleResult(result), refreshToken: privateInfo.refreshToken, }; } @@ -124,51 +137,122 @@ export class OneLoginProvider implements OAuthHandlers { this._strategy, accessToken, ); - const profile = makeProfileInfo(fullProfile, params.id_token); - return this.populateIdentity({ - providerInfo: { - accessToken, - idToken: params.id_token, - expiresInSeconds: params.expires_in, - scope: params.scope, - }, - profile, + return this.handleResult({ + fullProfile, + params, + accessToken, }); } - private async populateIdentity( - response: OAuthResponse, - ): Promise { - const { profile } = response; + private async handleResult(result: OAuthResult) { + const { profile } = await this.authHandler(result); - if (!profile.email) { - throw new Error('OIDC profile contained no email'); + const response: OAuthResponse = { + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + profile, + }; + + if (this.signInResolver) { + response.backstageIdentity = await this.signInResolver( + { + result, + profile, + }, + { + tokenIssuer: this.tokenIssuer, + catalogIdentityClient: this.catalogIdentityClient, + logger: this.logger, + }, + ); } - const id = profile.email.split('@')[0]; - - return { ...response, backstageIdentity: { id, token: '' } }; + return response; } } -export type OneLoginProviderOptions = {}; +const defaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('OIDC profile contained no email'); + } + + const id = profile.email.split('@')[0]; + + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: id, ent: [`user:default/${id}`] }, + }); + + return { id, token }; +}; + +export type OneLoginProviderOptions = { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}; export const createOneLoginProvider = ( - _options?: OneLoginProviderOptions, + options?: OneLoginProviderOptions, ): AuthProviderFactory => { - return ({ providerId, globalConfig, config, tokenIssuer }) => + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + catalogApi, + logger, + }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const issuer = envConfig.getString('issuer'); const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver; + const provider = new OneLoginProvider({ clientId, clientSecret, callbackUrl, issuer, + authHandler, + signInResolver, + tokenIssuer, + catalogIdentityClient, + logger, }); return OAuthAdapter.fromConfig(globalConfig, provider, { From 2f26120a36cb69adcfa8f19d3f1e9cc3b9b6cdc7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 28 Dec 2021 16:01:01 +0100 Subject: [PATCH 3/4] auth-backend: changeset and api report update for auth0 and onelogin Signed-off-by: Patrik Oldsberg --- .changeset/eighty-dancers-heal.md | 5 ++++ plugins/auth-backend/api-report.md | 26 +++++++++++++++++++ .../src/providers/auth0/provider.ts | 2 ++ .../src/providers/onelogin/provider.ts | 2 ++ 4 files changed, 35 insertions(+) create mode 100644 .changeset/eighty-dancers-heal.md diff --git a/.changeset/eighty-dancers-heal.md b/.changeset/eighty-dancers-heal.md new file mode 100644 index 0000000000..9286e75d2c --- /dev/null +++ b/.changeset/eighty-dancers-heal.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Update `auth0` and `onelogin` providers to allow for `authHandler` and `signIn.resolver` configuration. diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 9ed523c678..29de38a1f4 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -47,6 +47,14 @@ export type AtlassianProviderOptions = { }; }; +// @public (undocumented) +export type Auth0ProviderOptions = { + authHandler?: AuthHandler; + signIn?: { + resolver: SignInResolver; + }; +}; + // @public export type AuthHandler = ( input: AuthResult, @@ -219,6 +227,11 @@ export const createAtlassianProvider: ( options?: AtlassianProviderOptions | undefined, ) => AuthProviderFactory; +// @public (undocumented) +export const createAuth0Provider: ( + options?: Auth0ProviderOptions | undefined, +) => AuthProviderFactory; + // Warning: (ae-missing-release-tag) "createAwsAlbProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -282,6 +295,11 @@ export const createOktaProvider: ( _options?: OktaProviderOptions | undefined, ) => AuthProviderFactory; +// @public (undocumented) +export const createOneLoginProvider: ( + options?: OneLoginProviderOptions | undefined, +) => AuthProviderFactory; + // Warning: (ae-missing-release-tag) "createOriginFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -572,6 +590,14 @@ export type OktaProviderOptions = { }; }; +// @public (undocumented) +export type OneLoginProviderOptions = { + authHandler?: AuthHandler; + signIn?: { + resolver: SignInResolver; + }; +}; + // Warning: (ae-missing-release-tag) "postMessageResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 4293e3e481..583f95c621 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -197,6 +197,7 @@ const defaultSignInResolver: SignInResolver = async ( return { id, token }; }; +/** @public */ export type Auth0ProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -215,6 +216,7 @@ export type Auth0ProviderOptions = { }; }; +/** @public */ export const createAuth0Provider = ( options?: Auth0ProviderOptions, ): AuthProviderFactory => { diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 97aec51a1e..8cb06ea7a7 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -195,6 +195,7 @@ const defaultSignInResolver: SignInResolver = async ( return { id, token }; }; +/** @public */ export type OneLoginProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -213,6 +214,7 @@ export type OneLoginProviderOptions = { }; }; +/** @public */ export const createOneLoginProvider = ( options?: OneLoginProviderOptions, ): AuthProviderFactory => { From 77a5d0fb6fce6e388f16b08b54c3bc72e260f476 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 28 Dec 2021 17:37:55 +0100 Subject: [PATCH 4/4] auth-backend: let adapter populate identity token for auth0 and onelogin Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/providers/auth0/provider.ts | 11 ++--------- .../auth-backend/src/providers/onelogin/provider.ts | 11 ++--------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 583f95c621..f14d1b8fd5 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -178,10 +178,7 @@ export class Auth0AuthProvider implements OAuthHandlers { } } -const defaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { +const defaultSignInResolver: SignInResolver = async info => { const { profile } = info; if (!profile.email) { @@ -190,11 +187,7 @@ const defaultSignInResolver: SignInResolver = async ( const id = profile.email.split('@')[0]; - const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: id, ent: [`user:default/${id}`] }, - }); - - return { id, token }; + return { id, token: '' }; }; /** @public */ diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 8cb06ea7a7..4f52e31f5b 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -176,10 +176,7 @@ export class OneLoginProvider implements OAuthHandlers { } } -const defaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { +const defaultSignInResolver: SignInResolver = async info => { const { profile } = info; if (!profile.email) { @@ -188,11 +185,7 @@ const defaultSignInResolver: SignInResolver = async ( const id = profile.email.split('@')[0]; - const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: id, ent: [`user:default/${id}`] }, - }); - - return { id, token }; + return { id, token: '' }; }; /** @public */