feat: enable backwards compatability and write a simple test
Co-authored-by: Fredrik Adelöw <freben@users.noreply.github.com> Co-authored-by: Johan Haals <johan.haals@gmail.com> Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { GoogleAuthProvider } from './provider';
|
||||
import * as helpers from '../../lib/passport/PassportStrategyHelper';
|
||||
import { OAuthResult } from '../../lib/oauth';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { TokenIssuer } from '../../identity/types';
|
||||
import { CatalogIdentityClient } from '../../lib/catalog';
|
||||
|
||||
const mockFrameHandler = (jest.spyOn(
|
||||
helpers,
|
||||
'executeFrameHandlerStrategy',
|
||||
) as unknown) as jest.MockedFunction<
|
||||
() => Promise<{ result: OAuthResult; privateInfo: any }>
|
||||
>;
|
||||
|
||||
describe('createGoogleProvider', () => {
|
||||
it('should auth', async () => {
|
||||
const tokenIssuer = {
|
||||
issueToken: jest.fn(),
|
||||
listPublicKeys: jest.fn(),
|
||||
};
|
||||
const catalogIdentityClient = {
|
||||
findUser: jest.fn(),
|
||||
};
|
||||
|
||||
const provider = new GoogleAuthProvider({
|
||||
logger: getVoidLogger(),
|
||||
catalogIdentityClient: (catalogIdentityClient as unknown) as CatalogIdentityClient,
|
||||
tokenIssuer: (tokenIssuer as unknown) as TokenIssuer,
|
||||
profileTransform: async ({ fullProfile }) => ({
|
||||
email: fullProfile.emails![0]!.value,
|
||||
displayName: fullProfile.displayName,
|
||||
picture: 'http://google.com/lols',
|
||||
}),
|
||||
clientId: 'mock',
|
||||
clientSecret: 'mock',
|
||||
callbackUrl: 'mock',
|
||||
});
|
||||
|
||||
mockFrameHandler.mockResolvedValueOnce({
|
||||
result: {
|
||||
fullProfile: {
|
||||
emails: [{ value: 'conrad@example.com' }],
|
||||
displayName: 'Conrad',
|
||||
id: 'conrad',
|
||||
provider: 'google',
|
||||
},
|
||||
params: {
|
||||
id_token: 'idToken',
|
||||
scope: 'scope',
|
||||
expires_in: 123,
|
||||
},
|
||||
accessToken: 'accessToken',
|
||||
},
|
||||
privateInfo: {
|
||||
refreshToken: 'wacka',
|
||||
},
|
||||
});
|
||||
const { response } = await provider.handler({} as any);
|
||||
expect(response).toEqual({
|
||||
providerInfo: {
|
||||
accessToken: 'accessToken',
|
||||
expiresInSeconds: 123,
|
||||
idToken: 'idToken',
|
||||
scope: 'scope',
|
||||
},
|
||||
profile: {
|
||||
email: 'conrad@example.com',
|
||||
displayName: 'Conrad',
|
||||
picture: 'http://google.com/lols',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
RedirectInfo,
|
||||
SignInResolver,
|
||||
} from '../types';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -54,6 +55,7 @@ type Options = OAuthProviderOptions & {
|
||||
profileTransform: ProfileTransform<OAuthResult>;
|
||||
tokenIssuer: TokenIssuer;
|
||||
catalogIdentityClient: CatalogIdentityClient;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
export class GoogleAuthProvider implements OAuthHandlers {
|
||||
@@ -62,12 +64,14 @@ export class GoogleAuthProvider implements OAuthHandlers {
|
||||
private readonly profileTransform: ProfileTransform<OAuthResult>;
|
||||
private readonly tokenIssuer: TokenIssuer;
|
||||
private readonly catalogIdentityClient: CatalogIdentityClient;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(options: Options) {
|
||||
this.signInResolver = options.signInResolver;
|
||||
this.profileTransform = options.profileTransform;
|
||||
this.tokenIssuer = options.tokenIssuer;
|
||||
this.catalogIdentityClient = options.catalogIdentityClient;
|
||||
this.logger = options.logger;
|
||||
this._strategy = new GoogleStrategy(
|
||||
{
|
||||
clientID: options.clientId,
|
||||
@@ -163,6 +167,7 @@ export class GoogleAuthProvider implements OAuthHandlers {
|
||||
{
|
||||
tokenIssuer: this.tokenIssuer,
|
||||
catalogIdentityClient: this.catalogIdentityClient,
|
||||
logger: this.logger,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -171,7 +176,10 @@ export class GoogleAuthProvider implements OAuthHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
const emailSignInResolver: SignInResolver<OAuthResult> = async (info, ctx) => {
|
||||
export const googleEmailSignInResolver: SignInResolver<OAuthResult> = async (
|
||||
info,
|
||||
ctx,
|
||||
) => {
|
||||
const { profile } = info;
|
||||
|
||||
if (!profile.email) {
|
||||
@@ -190,6 +198,38 @@ const emailSignInResolver: SignInResolver<OAuthResult> = async (info, ctx) => {
|
||||
return { id: entity.metadata.name, entity, token };
|
||||
};
|
||||
|
||||
export const googleDefaultSignInResolver: SignInResolver<OAuthResult> = async (
|
||||
info,
|
||||
ctx,
|
||||
) => {
|
||||
const { profile } = info;
|
||||
|
||||
if (!profile.email) {
|
||||
throw new Error('Google profile contained no email');
|
||||
}
|
||||
|
||||
let userId: string;
|
||||
try {
|
||||
const entity = await ctx.catalogIdentityClient.findUser({
|
||||
annotations: {
|
||||
'google.com/email': profile.email,
|
||||
},
|
||||
});
|
||||
userId = entity.metadata.name;
|
||||
} catch (error) {
|
||||
ctx.logger.warn(
|
||||
`Failed to look up user, ${error}, falling back to allowing login based on email pattern, this will probably break in the future`,
|
||||
);
|
||||
userId = profile.email.split('@')[0];
|
||||
}
|
||||
|
||||
const token = await ctx.tokenIssuer.issueToken({
|
||||
claims: { sub: userId, ent: [`user:default/${userId}`] },
|
||||
});
|
||||
|
||||
return { id: userId, token };
|
||||
};
|
||||
|
||||
export type GoogleProviderOptions = {
|
||||
/**
|
||||
* The profile transformation function used to verify and convert the auth response
|
||||
@@ -200,21 +240,28 @@ export type GoogleProviderOptions = {
|
||||
/**
|
||||
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
|
||||
*/
|
||||
/**
|
||||
* Maps an auth result to a Backstage identity for the user.
|
||||
*
|
||||
* Set to `'email'` to use the default email-based sign in resolver, which will search
|
||||
* the catalog for a single user entity that has a matching `google.com/email` annotation.
|
||||
*/
|
||||
signIn?: {
|
||||
/**
|
||||
* Maps an auth result to a Backstage identity for the user.
|
||||
*
|
||||
* Set to `'email'` to use the default email-based sign in resolver, which will search
|
||||
* the catalog for a single user entity that has a matching `google.com/email` annotation.
|
||||
*/
|
||||
resolver?: 'email' | SignInResolver<OAuthResult>;
|
||||
resolver?: SignInResolver<OAuthResult>;
|
||||
};
|
||||
};
|
||||
|
||||
export const createGoogleProvider = (
|
||||
options?: GoogleProviderOptions,
|
||||
): AuthProviderFactory => {
|
||||
return ({ providerId, globalConfig, config, tokenIssuer, catalogApi }) =>
|
||||
return ({
|
||||
providerId,
|
||||
globalConfig,
|
||||
config,
|
||||
tokenIssuer,
|
||||
catalogApi,
|
||||
logger,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
@@ -233,17 +280,15 @@ export const createGoogleProvider = (
|
||||
profileTransform = options.profileTransform;
|
||||
}
|
||||
|
||||
let signInResolver: SignInResolver<OAuthResult> | undefined = undefined;
|
||||
const resolver = options?.signIn?.resolver;
|
||||
if (resolver === 'email') {
|
||||
signInResolver = emailSignInResolver;
|
||||
} else if (typeof resolver === 'function') {
|
||||
signInResolver = info =>
|
||||
resolver(info, {
|
||||
catalogIdentityClient,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
const signInResolverFn =
|
||||
options?.signIn?.resolver ?? googleDefaultSignInResolver;
|
||||
|
||||
const signInResolver: SignInResolver<OAuthResult> = info =>
|
||||
signInResolverFn(info, {
|
||||
catalogIdentityClient,
|
||||
tokenIssuer,
|
||||
logger,
|
||||
});
|
||||
|
||||
const provider = new GoogleAuthProvider({
|
||||
clientId,
|
||||
@@ -253,6 +298,7 @@ export const createGoogleProvider = (
|
||||
profileTransform,
|
||||
tokenIssuer,
|
||||
catalogIdentityClient,
|
||||
logger,
|
||||
});
|
||||
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
|
||||
@@ -215,6 +215,7 @@ export type SignInResolver<AuthResult> = (
|
||||
context: {
|
||||
tokenIssuer: TokenIssuer;
|
||||
catalogIdentityClient: CatalogIdentityClient;
|
||||
logger: Logger;
|
||||
},
|
||||
) => Promise<BackstageIdentity>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user