From 04b1d4be44ee435dd138835db90a1e0761fb5c2d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 28 Dec 2021 15:26:36 +0100 Subject: [PATCH] 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, {