Working oidc authenticator with passing unit and module tests

Signed-off-by: Ruben Vallejo <rvallejo@vmware.com>
This commit is contained in:
Ruben Vallejo
2023-09-28 10:59:18 -04:00
committed by Jamie Klassen
parent a3c911e636
commit 1964cb7d88
8 changed files with 84 additions and 261 deletions
+1
View File
@@ -27,6 +27,7 @@ export interface Config {
clientSecret: string;
metadataUrl: string;
callbackUrl?: string;
tokenEndpointAuthMethod?: string;
tokenSignedResponseAlg?: string;
scope?: string;
prompt?: string;
@@ -15,12 +15,10 @@
*/
import { createBackend } from '@backstage/backend-defaults';
import { authPlugin } from '@backstage/plugin-auth-backend';
import { authModuleOidcProvider } from '../src';
const backend = createBackend();
backend.add(authPlugin);
backend.add(authModuleOidcProvider);
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('../src'));
backend.start();
@@ -103,7 +103,7 @@ describe('oidcAuthenticator', () => {
id_token: idToken,
refresh_token: 'refreshToken',
scope: 'testScope',
token_type: '',
expires_in: 3600,
})
: ctx.status(401),
);
@@ -119,6 +119,7 @@ describe('oidcAuthenticator', () => {
given_name: 'Alice',
family_name: 'Adams',
email: 'alice@test.com',
picture: 'http://testPictureUrl/photo.jpg',
}),
),
),
@@ -127,7 +128,7 @@ describe('oidcAuthenticator', () => {
implementation = oidcAuthenticator.initialize({
callbackUrl: 'https://backstage.test/callback',
config: new ConfigReader({
metadataUrl: 'https://oidc.test',
metadataUrl: 'https://oidc.test/.well-known/openid-configuration',
clientId: 'clientId',
clientSecret: 'clientSecret',
}),
@@ -219,7 +220,6 @@ describe('oidcAuthenticator', () => {
expect(fakeSession['oidc:oidc.test'].code_verifier).toBeDefined();
});
// without the offline_access scope refresh should not work should we add in the test that is a required scope to test for? curently dont see that by defualt in the provider implementation.
it('requests default scopes if none are provided in config', async () => {
const startResponse = await oidcAuthenticator.start(
startRequest,
@@ -310,6 +310,53 @@ describe('oidcAuthenticator', () => {
expect(responseScope).toEqual('testScope');
});
it('returns a default session.tokentype field', async () => {
const handlerResponse = await oidcAuthenticator.authenticate(
handlerRequest,
implementation,
);
const tokenType = handlerResponse.session.tokenType;
expect(tokenType).toEqual('bearer');
});
it('returns defined fullProfile with picture and email', async () => {
const handlerResponse = await oidcAuthenticator.authenticate(
handlerRequest,
implementation,
);
const displayName = handlerResponse.fullProfile.displayName;
const email = handlerResponse.fullProfile.emails![0].value;
const picture = handlerResponse.fullProfile.photos![0].value;
expect(displayName).toEqual('Alice Adams');
expect(email).toEqual('alice@test.com');
expect(picture).toEqual('http://testPictureUrl/photo.jpg');
});
it('returns defined response with an idToken', async () => {
const handlerResponse = await oidcAuthenticator.authenticate(
handlerRequest,
implementation,
);
expect(handlerResponse).toMatchObject({
fullProfile: {
displayName: 'Alice Adams',
id: 'test',
provider: 'oidc',
},
session: {
accessToken: 'accessToken',
expiresInSeconds: 3600,
idToken,
refreshToken: 'refreshToken',
scope: 'testScope',
tokenType: 'bearer',
},
});
});
it('fails without authorization code', async () => {
handlerRequest.req.url = 'https://test.com';
return expect(
@@ -26,7 +26,6 @@ import {
PassportOAuthAuthenticatorHelper,
PassportOAuthDoneCallback,
} from '@backstage/plugin-auth-node';
// import { BACKSTAGE_SESSION_EXPIRATION } from '@backstage/plugin-auth-backend/src/lib/session';
/** @public */
export const oidcAuthenticator = createOAuthAuthenticator({
@@ -46,9 +45,8 @@ export const oidcAuthenticator = createOAuthAuthenticator({
);
const initializedScope = config.getOptionalString('scope');
const initializedPrompt = config.getOptionalString('prompt');
const issuer = await Issuer.discover(
`${metadataUrl}/.well-known/openid-configuration`,
);
const issuer = await Issuer.discover(metadataUrl);
const client = new issuer.Client({
access_type: 'offline', // this option must be passed to provider to receive a refresh token
client_id: clientId,
@@ -77,9 +75,18 @@ export const oidcAuthenticator = createOAuthAuthenticator({
);
}
const expiresInSeconds = !tokenset.expires_in
? 3600
: Math.min(tokenset.expires_in!, 3600);
const emails = userinfo.email ? [{ value: userinfo.email }] : undefined;
const photos = userinfo.picture
? [{ value: userinfo.picture }]
: undefined;
const name =
userinfo.family_name && userinfo.given_name
? {
familyName: userinfo.family_name,
givenName: userinfo.given_name,
middleName: userinfo.middle_name,
}
: undefined;
done(
undefined,
@@ -88,11 +95,17 @@ export const oidcAuthenticator = createOAuthAuthenticator({
provider: 'oidc',
id: userinfo.sub,
displayName: userinfo.name!,
username: userinfo.preferred_username,
name,
emails,
photos,
},
accessToken: tokenset.access_token!,
params: {
id_token: tokenset.id_token,
scope: tokenset.scope!,
expires_in: expiresInSeconds,
expires_in: tokenset.expires_in!,
token_type: tokenset.token_type,
},
},
{
@@ -139,6 +152,9 @@ export const oidcAuthenticator = createOAuthAuthenticator({
if (!tokenset.access_token) {
throw new Error('Refresh failed');
}
if (!tokenset.scope) {
tokenset.scope = input.scope;
}
const userinfo = await client.userinfo(tokenset.access_token);
return new Promise((resolve, reject) => {
@@ -21,5 +21,5 @@
*/
export { oidcAuthenticator } from './authenticator';
export { authModuleOidcProvider } from './module';
export { authModuleOidcProvider as default } from './module';
export { oidcSignInResolvers } from './resolvers';
@@ -17,7 +17,6 @@
import request from 'supertest';
import {
AuthProviderRouteHandlers,
AuthResolverContext,
createOAuthRouteHandlers,
decodeOAuthState,
} from '@backstage/plugin-auth-node';
@@ -121,6 +120,7 @@ describe('authModuleOidcProvider', () => {
refresh_token: 'refreshToken',
scope: 'testScope',
token_type: '',
expires_in: 3600,
})
: ctx.status(401),
);
@@ -136,6 +136,7 @@ describe('authModuleOidcProvider', () => {
given_name: 'Alice',
family_name: 'Adams',
email: 'alice@test.com',
picture: 'http://testPictureUrl/photo.jpg',
}),
),
),
@@ -172,7 +173,7 @@ describe('authModuleOidcProvider', () => {
isOriginAllowed: _ => true,
providerId: 'oidc',
config: new ConfigReader({
metadataUrl: 'https://oidc.test',
metadataUrl: 'https://oidc.test/.well-known/openid-configuration',
clientId: 'clientId',
clientSecret: 'clientSecret',
}),
@@ -264,9 +265,12 @@ describe('authModuleOidcProvider', () => {
type: 'authorization_response',
response: {
profile: {
email: 'alice@test.com',
picture: 'http://testPictureUrl/photo.jpg',
displayName: 'Alice Adams',
},
providerInfo: {
idToken,
accessToken: 'accessToken',
scope: 'testScope',
expiresInSeconds: 3600,
@@ -15,4 +15,3 @@
*/
export { oidc } from './provider';
export type { OidcAuthResult } from './provider';
@@ -22,255 +22,13 @@ import {
adaptLegacyOAuthHandler,
adaptLegacyOAuthSignInResolver,
} from '../../lib/legacy';
import { oidcAuthenticator } from '@backstage/plugin-auth-backend-module-oidc-provider';
// type PrivateInfo = {
// refreshToken?: string;
// };
// type OidcImpl = {
// strategy: OidcStrategy<UserinfoResponse, Client>;
// client: Client;
// };
// /**
// * authentication result for the OIDC which includes the token set and user information (a profile response sent by OIDC server)
// * @public
// */
// export type OidcAuthResult = {
// tokenset: TokenSet;
// userinfo: UserinfoResponse;
// };
// export type Options = OAuthProviderOptions & {
// metadataUrl: string;
// scope?: string;
// prompt?: string;
// tokenEndpointAuthMethod?: ClientAuthMethod;
// tokenSignedResponseAlg?: string;
// signInResolver?: SignInResolver<OidcAuthResult>;
// authHandler: AuthHandler<OidcAuthResult>;
// resolverContext: AuthResolverContext;
// };
// export class OidcAuthProvider implements OAuthHandlers {
// private readonly implementation: Promise<OidcImpl>;
// private readonly scope?: string;
// private readonly prompt?: string;
// private readonly signInResolver?: SignInResolver<OidcAuthResult>;
// private readonly authHandler: AuthHandler<OidcAuthResult>;
// private readonly resolverContext: AuthResolverContext;
// constructor(options: Options) {
// this.implementation = this.setupStrategy(options);
// this.scope = options.scope;
// this.prompt = options.prompt;
// this.signInResolver = options.signInResolver;
// this.authHandler = options.authHandler;
// this.resolverContext = options.resolverContext;
// }
// async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
// const { strategy } = await this.implementation;
// const options: Record<string, string> = {
// scope: req.scope || this.scope || 'openid profile email',
// state: encodeState(req.state),
// };
// const prompt = this.prompt || 'none';
// if (prompt !== 'auto') {
// options.prompt = prompt;
// }
// return await executeRedirectStrategy(req, strategy, options);
// }
// async handler(req: express.Request) {
// const { strategy } = await this.implementation;
// const { result, privateInfo } = await executeFrameHandlerStrategy<
// OidcAuthResult,
// PrivateInfo
// >(req, strategy);
// async refresh(req: OAuthRefreshRequest) {
// const { client } = await this.implementation;
// const tokenset = await client.refresh(req.refreshToken);
// if (!tokenset.access_token) {
// throw new Error('Refresh failed');
// }
// if (!tokenset.scope) {
// tokenset.scope = req.scope;
// }
// const userinfo = await client.userinfo(tokenset.access_token);
// return {
// response: await this.handleResult(result),
// refreshToken: privateInfo.refreshToken,
// };
// }
// async refresh(req: OAuthRefreshRequest) {
// const { client } = await this.implementation;
// const tokenset = await client.refresh(req.refreshToken);
// if (!tokenset.access_token) {
// throw new Error('Refresh failed');
// }
// const userinfo = await client.userinfo(tokenset.access_token);
// return {
// response: await this.handleResult({ tokenset, userinfo }),
// refreshToken: tokenset.refresh_token,
// };
// }
// private async setupStrategy(options: Options): Promise<OidcImpl> {
// const issuer = await Issuer.discover(options.metadataUrl);
// const client = new issuer.Client({
// access_type: 'offline', // this option must be passed to provider to receive a refresh token
// client_id: options.clientId,
// client_secret: options.clientSecret,
// redirect_uris: [options.callbackUrl],
// response_types: ['code'],
// token_endpoint_auth_method:
// options.tokenEndpointAuthMethod || 'client_secret_basic',
// id_token_signed_response_alg: options.tokenSignedResponseAlg || 'RS256',
// scope: options.scope || '',
// });
// const strategy = new OidcStrategy(
// {
// client,
// passReqToCallback: false,
// },
// (
// tokenset: TokenSet,
// userinfo: UserinfoResponse,
// done: PassportDoneCallback<OidcAuthResult, PrivateInfo>,
// ) => {
// if (typeof done !== 'function') {
// throw new Error(
// 'OIDC IdP must provide a userinfo_endpoint in the metadata response',
// );
// }
// done(
// undefined,
// { tokenset, userinfo },
// {
// refreshToken: tokenset.refresh_token,
// },
// );
// },
// );
// strategy.error = console.error;
// return { strategy, client };
// }
// Use this function to grab the user profile info from the token
// Then populate the profile with it
// private async handleResult(result: OidcAuthResult): Promise<OAuthResponse> {
// const { profile } = await this.authHandler(result, this.resolverContext);
// const expiresInSeconds =
// result.tokenset.expires_in === undefined
// ? BACKSTAGE_SESSION_EXPIRATION
// : Math.min(result.tokenset.expires_in, BACKSTAGE_SESSION_EXPIRATION);
// let backstageIdentity = undefined;
// if (this.signInResolver) {
// backstageIdentity = await this.signInResolver(
// {
// result,
// profile,
// },
// this.resolverContext,
// );
// }
// return {
// backstageIdentity,
// providerInfo: {
// idToken: result.tokenset.id_token,
// accessToken: result.tokenset.access_token!,
// scope: result.tokenset.scope!,
// expiresInSeconds,
// },
// profile,
// };
// }
// }
/**
* Auth provider integration for generic OpenID Connect auth
*
* @public
*/
// export const oidc = createAuthProviderIntegration({
// create(options?: {
// authHandler?: AuthHandler<OidcAuthResult>;
// signIn?: {
// resolver: SignInResolver<OidcAuthResult>;
// };
// }) {
// return ({ providerId, globalConfig, config, resolverContext }) =>
// 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 metadataUrl = envConfig.getString('metadataUrl');
// const tokenEndpointAuthMethod = envConfig.getOptionalString(
// 'tokenEndpointAuthMethod',
// ) as ClientAuthMethod;
// const tokenSignedResponseAlg = envConfig.getOptionalString(
// 'tokenSignedResponseAlg',
// );
// const scope = envConfig.getOptionalString('scope');
// const prompt = envConfig.getOptionalString('prompt');
// const authHandler: AuthHandler<OidcAuthResult> = options?.authHandler
// ? options.authHandler
// : async ({ userinfo }) => ({
// profile: {
// displayName: userinfo.name,
// email: userinfo.email,
// picture: userinfo.picture,
// },
// });
// const provider = new OidcAuthProvider({
// clientId,
// clientSecret,
// callbackUrl,
// tokenEndpointAuthMethod,
// tokenSignedResponseAlg,
// metadataUrl,
// scope,
// prompt,
// signInResolver: options?.signIn?.resolver,
// authHandler,
// resolverContext,
// });
// return OAuthAdapter.fromConfig(globalConfig, provider, {
// providerId,
// callbackUrl,
// });
// });
// },
// resolvers: {
// /**
// * Looks up the user by matching their email local part to the entity name.
// */
// emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver,
// /**
// * Looks up the user by matching their email to the entity email.
// */
// emailMatchingUserEntityProfileEmail: () => commonByEmailResolver,
// },
// });
export const oidc = createAuthProviderIntegration({
create(options?: {
/**