diff --git a/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts new file mode 100644 index 0000000000..8b58e89062 --- /dev/null +++ b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AuthProviderFactory, SignInResolver } from './types'; + +/** + * Creates a standardized representation of an integration with a third-party + * auth provider. + * + * The returned object facilitates the creation of provider instances, and + * supplies built-in sign-in resolvers for the specific provider. + */ +export function createAuthProviderIntegration< + TCreateOptions extends unknown[], + TResolvers extends { + [name in string]: (...args: any[]) => SignInResolver; + }, +>(config: { + create: (...args: TCreateOptions) => AuthProviderFactory; + resolvers: TResolvers; +}): Readonly<{ + create: (...args: TCreateOptions) => AuthProviderFactory; + resolvers: Readonly; +}> { + return Object.freeze({ + ...config, + resolvers: Object.freeze(config.resolvers), + }); +} diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 7e5a998785..68c505cc43 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -38,13 +38,10 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { - AuthProviderFactory, - AuthHandler, - RedirectInfo, - SignInResolver, -} from '../types'; +import { AuthHandler, RedirectInfo, SignInResolver } from '../types'; import { Logger } from 'winston'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { commonByEmailLocalPartResolver } from '../resolvers'; type PrivateInfo = { refreshToken: string; @@ -179,28 +176,9 @@ export class GoogleAuthProvider implements OAuthHandlers { } } -export const googleEmailSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Google profile contained no email'); - } - - const entity = await ctx.catalogIdentityClient.findUser({ - annotations: { - 'google.com/email': profile.email, - }, - }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; -}; - +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type GoogleProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -219,53 +197,102 @@ export type GoogleProviderOptions = { }; }; -export const createGoogleProvider = ( - options?: GoogleProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export const google = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, + /** + * 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; + }; + }) { + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + tokenManager, + catalogApi, + logger, + }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenManager, + }); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const provider = new GoogleAuthProvider({ + clientId, + clientSecret, + callbackUrl, + signInResolver: options?.signIn?.resolver, + authHandler, + tokenIssuer, + catalogIdentityClient, + logger, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + callbackUrl, + }); }); + }, + resolvers: { + byEmailLocalPart: () => commonByEmailLocalPartResolver, + lookupEmailAnnotation(): SignInResolver { + return async (info, ctx) => { + const { profile } = info; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + if (!profile.email) { + throw new Error('Google profile contained no email'); + } - const provider = new GoogleAuthProvider({ - clientId, - clientSecret, - callbackUrl, - signInResolver: options?.signIn?.resolver, - authHandler, - tokenIssuer, - catalogIdentityClient, - logger, - }); + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'google.com/email': profile.email, + }, + }); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); + + return { id: entity.metadata.name, entity, token }; + }; + }, + }, +}); + +/** + * @deprecated Use `providers.google.create` instead. + */ +export const createGoogleProvider = google.create; + +/** + * @deprecated Use `google.resolvers.lookupEmailAnnotation` instead. + */ +export const googleEmailSignInResolver = google.resolvers.lookupEmailAnnotation; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 78814ae7e6..9650632622 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -30,6 +30,8 @@ export * from './onelogin'; export * from './saml'; export * from './gcp-iap'; +export { providers } from './providers'; + export { factories as defaultAuthProviderFactories } from './factories'; // Export the minimal interface required for implementing a diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts new file mode 100644 index 0000000000..ffe833ff47 --- /dev/null +++ b/plugins/auth-backend/src/providers/providers.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { google } from './google/provider'; + +export const providers = Object.freeze({ + google, +}); diff --git a/plugins/auth-backend/src/providers/resolvers.ts b/plugins/auth-backend/src/providers/resolvers.ts index ee998c1c90..dd9a12f90a 100644 --- a/plugins/auth-backend/src/providers/resolvers.ts +++ b/plugins/auth-backend/src/providers/resolvers.ts @@ -20,9 +20,10 @@ import { } from '@backstage/catalog-model'; import { SignInResolver } from './types'; -export const commonEmailLocalPartEntityNameSignInResolver: SignInResolver< - unknown -> = async (info, ctx) => { +export const commonByEmailLocalPartResolver: SignInResolver = async ( + info, + ctx, +) => { const { profile } = info; if (!profile.email) {