auth-backend: new pattern for exporting auth providers and resolvers, applied to google provider

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-03-17 21:59:32 +01:00
parent d1b3a1e805
commit a020f4a720
5 changed files with 169 additions and 76 deletions
@@ -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<any>;
},
>(config: {
create: (...args: TCreateOptions) => AuthProviderFactory;
resolvers: TResolvers;
}): Readonly<{
create: (...args: TCreateOptions) => AuthProviderFactory;
resolvers: Readonly<TResolvers>;
}> {
return Object.freeze({
...config,
resolvers: Object.freeze(config.resolvers),
});
}
@@ -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<OAuthResult> = 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<OAuthResult>;
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<OAuthResult>;
};
}) {
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<OAuthResult> = 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<OAuthResult> {
return async (info, ctx) => {
const { profile } = info;
const authHandler: AuthHandler<OAuthResult> = 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;
@@ -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
@@ -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,
});
@@ -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<unknown> = async (
info,
ctx,
) => {
const { profile } = info;
if (!profile.email) {