From 78baf7f932583fb73c0fe9a9f151e495fa29bad5 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Mon, 22 Jun 2020 18:02:19 +0200 Subject: [PATCH] backend - fix types + refactoring --- .../src/lib/OAuthProvider.test.ts | 64 +++++++++----- plugins/auth-backend/src/lib/OAuthProvider.ts | 69 ++++++++------- .../src/lib/PassportStrategyHelper.test.ts | 4 +- .../src/lib/PassportStrategyHelper.ts | 86 +++++++++++-------- .../src/providers/github/provider.ts | 40 ++++++--- .../src/providers/google/provider.ts | 68 +++++++++------ .../src/providers/saml/provider.ts | 76 ++++++++++------ plugins/auth-backend/src/providers/types.ts | 79 ++++++++++++----- 8 files changed, 314 insertions(+), 172 deletions(-) diff --git a/plugins/auth-backend/src/lib/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/OAuthProvider.test.ts index 48952e6dad..d84226214d 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/OAuthProvider.test.ts @@ -23,7 +23,23 @@ import { verifyNonce, OAuthProvider, } from './OAuthProvider'; -import { AuthResponse, OAuthProviderHandlers } from '../providers/types'; +import { + WebMessageResponse, + OAuthProviderHandlers, + OAuthResponse, +} from '../providers/types'; + +const mockResponseData: OAuthResponse = { + providerInfo: { + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 10, + scope: 'email', + }, + profile: { + email: 'foo@bar.com', + }, +}; describe('OAuthProvider Utils', () => { describe('verifyNonce', () => { @@ -38,6 +54,7 @@ describe('OAuthProvider Utils', () => { verifyNonce(mockRequest, 'providera'); }).toThrowError('Missing nonce'); }); + it('should throw error if state nonce missing', () => { const mockRequest = ({ cookies: { @@ -49,6 +66,7 @@ describe('OAuthProvider Utils', () => { verifyNonce(mockRequest, 'providera'); }).toThrowError('Missing nonce'); }); + it('should throw error if nonce mismatch', () => { const mockRequest = ({ cookies: { @@ -62,6 +80,7 @@ describe('OAuthProvider Utils', () => { verifyNonce(mockRequest, 'providera'); }).toThrowError('Invalid nonce'); }); + it('should not throw any error if nonce matches', () => { const mockRequest = ({ cookies: { @@ -85,13 +104,19 @@ describe('OAuthProvider Utils', () => { setHeader: jest.fn().mockReturnThis(), } as unknown) as express.Response; - const data: AuthResponse = { - type: 'auth-result', - payload: { - accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', - expiresInSeconds: 10, - scope: 'email', + const data: WebMessageResponse = { + type: 'authorization_response', + response: { + providerInfo: { + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 10, + scope: 'email', + }, + profile: { + email: 'foo@bar.com', + }, + userIdToken: 'a.b.c', }, }; const jsonData = JSON.stringify(data); @@ -111,8 +136,8 @@ describe('OAuthProvider Utils', () => { setHeader: jest.fn().mockReturnThis(), } as unknown) as express.Response; - const data: AuthResponse = { - type: 'auth-result', + const data: WebMessageResponse = { + type: 'authorization_response', error: new Error('Unknown error occured'), }; const jsonData = JSON.stringify(data); @@ -161,16 +186,12 @@ describe('OAuthProvider', () => { } async handler() { return { - user: {}, - info: { - refreshToken: 'token', - }, + response: mockResponseData, + refreshToken: 'token', }; } async refresh() { - return { - accessToken: 'token', - }; + return mockResponseData; } } const providerInstance = new MyAuthProvider(); @@ -323,11 +344,10 @@ describe('OAuthProvider', () => { await oauthProvider.refresh(mockRequest, mockResponse); expect(mockResponse.send).toHaveBeenCalledTimes(1); - expect(mockResponse.send).toHaveBeenCalledWith( - expect.objectContaining({ - accessToken: 'token', - }), - ); + expect(mockResponse.send).toHaveBeenCalledWith({ + ...mockResponseData, + userIdToken: 'my-id-token', + }); }); it('handles refresh without capabilities', async () => { diff --git a/plugins/auth-backend/src/lib/OAuthProvider.ts b/plugins/auth-backend/src/lib/OAuthProvider.ts index 38065c3380..1c88dae693 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/OAuthProvider.ts @@ -21,6 +21,7 @@ import { AuthResponse, AuthProviderRouteHandlers, OAuthProviderHandlers, + WebMessageResponse, } from '../providers/types'; import { InputError } from '@backstage/backend-common'; import { TokenIssuer } from '../identity'; @@ -53,9 +54,9 @@ export const verifyNonce = (req: express.Request, providerId: string) => { export const postMessageResponse = ( res: express.Response, appOrigin: string, - data: AuthResponse, + response: WebMessageResponse, ) => { - const jsonData = JSON.stringify(data); + const jsonData = JSON.stringify(response); const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); res.setHeader('Content-Type', 'text/html'); @@ -96,7 +97,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { this.basePath = url.pathname; } - async start(req: express.Request, res: express.Response): Promise { + async start(req: express.Request, res: express.Response): Promise { // retrieve scopes from request const scope = req.query.scope?.toString() ?? ''; @@ -108,14 +109,15 @@ export class OAuthProvider implements AuthProviderRouteHandlers { // set a nonce cookie before redirecting to oauth provider this.setNonceCookie(res, nonce); - const options = { + const queryParameters = { scope, - accessType: 'offline', - prompt: 'consent', state: nonce, }; - const { url, status } = await this.providerHandlers.start(req, options); + const { url, status } = await this.providerHandlers.start( + req, + queryParameters, + ); res.statusCode = status || 302; res.setHeader('Location', url); @@ -126,16 +128,17 @@ export class OAuthProvider implements AuthProviderRouteHandlers { async frameHandler( req: express.Request, res: express.Response, - ): Promise { + ): Promise { try { // verify nonce cookie and state cookie on callback verifyNonce(req, this.options.providerId); - const { user, info } = await this.providerHandlers.handler(req); + const { response, refreshToken } = await this.providerHandlers.handler( + req, + ); if (!this.options.disableRefresh) { // throw error if missing refresh token - const { refreshToken } = info; if (!refreshToken) { throw new Error('Missing refresh token'); } @@ -144,19 +147,23 @@ export class OAuthProvider implements AuthProviderRouteHandlers { this.setRefreshTokenCookie(res, refreshToken); } - user.userIdToken = await this.options.tokenIssuer.issueToken({ - claims: { sub: user.profile.email }, + const userIdToken = await this.options.tokenIssuer.issueToken({ + claims: { sub: response.profile.email }, }); + const fullResponse: AuthResponse = { + ...response, + userIdToken, + }; // post message back to popup if successful return postMessageResponse(res, this.options.appOrigin, { - type: 'auth-result', - payload: user, + type: 'authorization_response', + response: fullResponse, }); } catch (error) { // post error message back to popup if failure return postMessageResponse(res, this.options.appOrigin, { - type: 'auth-result', + type: 'authorization_response', error: { name: error.name, message: error.message, @@ -165,27 +172,30 @@ export class OAuthProvider implements AuthProviderRouteHandlers { } } - async logout(req: express.Request, res: express.Response): Promise { + async logout(req: express.Request, res: express.Response): Promise { if (!ensuresXRequestedWith(req)) { - return res.status(401).send('Invalid X-Requested-With header'); + res.status(401).send('Invalid X-Requested-With header'); + return; } if (!this.options.disableRefresh) { // remove refresh token cookie before logout this.removeRefreshTokenCookie(res); } - return res.send('logout!'); + res.send('logout!'); } - async refresh(req: express.Request, res: express.Response): Promise { + async refresh(req: express.Request, res: express.Response): Promise { if (!ensuresXRequestedWith(req)) { - return res.status(401).send('Invalid X-Requested-With header'); + res.status(401).send('Invalid X-Requested-With header'); + return; } if (!this.providerHandlers.refresh || this.options.disableRefresh) { - return res.send( + res.send( `Refresh token not supported for provider: ${this.options.providerId}`, ); + return; } try { @@ -200,18 +210,19 @@ export class OAuthProvider implements AuthProviderRouteHandlers { const scope = req.query.scope?.toString() ?? ''; // get new access_token - const refreshInfo = await this.providerHandlers.refresh( - refreshToken, - scope, - ); + const response = await this.providerHandlers.refresh(refreshToken, scope); - refreshInfo.userIdToken = await this.options.tokenIssuer.issueToken({ - claims: { sub: refreshInfo.profile?.email }, + const userIdToken = await this.options.tokenIssuer.issueToken({ + claims: { sub: response.profile.email }, }); + const fullResponse: AuthResponse = { + ...response, + userIdToken, + }; - return res.send(refreshInfo); + res.send(fullResponse); } catch (error) { - return res.status(401).send(`${error.message}`); + res.status(401).send(`${error.message}`); } } diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts index 071c949a21..d8f6d25d0f 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts @@ -82,8 +82,8 @@ describe('PassportStrategyHelper', () => { expect(spyAuthenticate).toBeCalledTimes(1); await expect(frameHandlerStrategyPromise).resolves.toStrictEqual( expect.objectContaining({ - user: { accessToken: 'ACCESS_TOKEN' }, - info: { refreshToken: 'REFRESH_TOKEN' }, + response: { accessToken: 'ACCESS_TOKEN' }, + privateInfo: { refreshToken: 'REFRESH_TOKEN' }, }), ); }); diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 1e8dfca5ed..bb6d1b5d42 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -26,42 +26,52 @@ import { export const makeProfileInfo = ( profile: passport.Profile, - params: any, + idToken?: string, ): ProfileInfo => { - const { displayName: name } = profile; + const { displayName } = profile; - let email = ''; + let email: string | undefined = undefined; if (profile.emails) { const [firstEmail] = profile.emails; email = firstEmail.value; } - if (!email && params.id_token) { - try { - const decoded: { email: string } = jwtDecoder(params.id_token); - email = decoded.email; - } catch (e) { - console.error('Failed to parse id token and get profile info'); - } - } - - let picture = ''; + let picture: string | undefined = undefined; if (profile.photos) { const [firstPhoto] = profile.photos; picture = firstPhoto.value; } + if ((!email || !picture) && idToken) { + try { + const decoded: Record = jwtDecoder(idToken); + + if (!email && decoded.email) { + email = decoded.email; + } + if (!picture && decoded.picture) { + picture = decoded.picture; + } + } catch (e) { + throw new Error(`Failed to parse id token and get profile info, ${e}`); + } + } + + if (!email) { + throw new Error('No email received in profile info'); + } + return { - name, email, picture, + displayName, }; }; export const executeRedirectStrategy = async ( req: express.Request, providerStrategy: passport.Strategy, - options: any, + options: Record, ): Promise => { return new Promise(resolve => { const strategy = Object.create(providerStrategy); @@ -73,30 +83,32 @@ export const executeRedirectStrategy = async ( }); }; -export const executeFrameHandlerStrategy = async ( +export const executeFrameHandlerStrategy = async ( req: express.Request, providerStrategy: passport.Strategy, ) => { - return new Promise<{ user: any; info: any }>((resolve, reject) => { - const strategy = Object.create(providerStrategy); - strategy.success = (user: any, info: any) => { - resolve({ user, info }); - }; - strategy.fail = ( - info: { type: 'success' | 'error'; message?: string }, - // _status: number, - ) => { - reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); - }; - strategy.error = (error: Error) => { - reject(new Error(`Authentication failed, ${error}`)); - }; - strategy.redirect = () => { - reject(new Error('Unexpected redirect')); - }; + return new Promise<{ response: T; privateInfo: PrivateInfo }>( + (resolve, reject) => { + const strategy = Object.create(providerStrategy); + strategy.success = (response: any, privateInfo: any) => { + resolve({ response, privateInfo }); + }; + strategy.fail = ( + info: { type: 'success' | 'error'; message?: string }, + // _status: number, + ) => { + reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); + }; + strategy.error = (error: Error) => { + reject(new Error(`Authentication failed, ${error}`)); + }; + strategy.redirect = () => { + reject(new Error('Unexpected redirect')); + }; - strategy.authenticate(req, {}); - }); + strategy.authenticate(req, {}); + }, + ); }; export const executeRefreshTokenStrategy = async ( @@ -151,7 +163,7 @@ export const executeRefreshTokenStrategy = async ( export const executeFetchUserProfileStrategy = async ( providerStrategy: passport.Strategy, accessToken: string, - params: any, + idToken?: string, ): Promise => { return new Promise((resolve, reject) => { const anyStrategy = (providerStrategy as unknown) as ProviderStrategy; @@ -162,7 +174,7 @@ export const executeFetchUserProfileStrategy = async ( reject(error); } - const profile = makeProfileInfo(passportProfile, params); + const profile = makeProfileInfo(passportProfile, idToken); resolve(profile); }, ); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index b5f9d7e46b..3ae9858bb1 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -19,16 +19,17 @@ import { Strategy as GithubStrategy } from 'passport-github2'; import { executeFrameHandlerStrategy, executeRedirectStrategy, + makeProfileInfo, } from '../../lib/PassportStrategyHelper'; import { OAuthProviderHandlers, AuthProviderConfig, RedirectInfo, - AuthInfoBase, - AuthInfoPrivate, EnvironmentProviderConfig, OAuthProviderOptions, OAuthProviderConfig, + OAuthResponse, + PassportDoneCallback, } from '../types'; import { OAuthProvider } from '../../lib/OAuthProvider'; import { @@ -44,25 +45,42 @@ export class GithubAuthProvider implements OAuthProviderHandlers { constructor(options: OAuthProviderOptions) { this._strategy = new GithubStrategy( { ...options }, - (accessToken: any, _: any, params: any, profile: any, done: any) => { + ( + accessToken: any, + _: any, + params: any, + rawProfile: any, + done: PassportDoneCallback, + ) => { + const profile = makeProfileInfo(rawProfile); done(undefined, { + providerInfo: { + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }, profile, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, }); }, ); } - async start(req: express.Request, options: any): Promise { + async start( + req: express.Request, + options: Record, + ): Promise { return await executeRedirectStrategy(req, this._strategy, options); } - async handler( - req: express.Request, - ): Promise<{ user: AuthInfoBase; info: AuthInfoPrivate }> { - return await executeFrameHandlerStrategy(req, this._strategy); + async handler(req: express.Request): Promise<{ response: OAuthResponse }> { + const result = await executeFrameHandlerStrategy( + req, + this._strategy, + ); + + return { + response: result.response, + }; } } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index f41fbfe787..f12b248021 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -25,14 +25,13 @@ import { } from '../../lib/PassportStrategyHelper'; import { OAuthProviderHandlers, - AuthInfoBase, - AuthInfoPrivate, RedirectInfo, AuthProviderConfig, - AuthInfoWithProfile, EnvironmentProviderConfig, OAuthProviderOptions, OAuthProviderConfig, + OAuthResponse, + PassportDoneCallback, } from '../types'; import { OAuthProvider } from '../../lib/OAuthProvider'; import passport from 'passport'; @@ -43,6 +42,10 @@ import { import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; +type PrivateInfo = { + refreshToken: string; +}; + export class GoogleAuthProvider implements OAuthProviderHandlers { private readonly _strategy: GoogleStrategy; @@ -56,18 +59,20 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { accessToken: any, refreshToken: any, params: any, - profile: passport.Profile, - done: any, + rawProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profileInfo = makeProfileInfo(profile, params); + const profile = makeProfileInfo(rawProfile, params.id_token); done( undefined, { - profile: profileInfo, - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, + providerInfo: { + idToken: params.id_token, + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }, + profile, }, { refreshToken, @@ -77,20 +82,33 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { ); } - async start(req: express.Request, options: any): Promise { - return await executeRedirectStrategy(req, this._strategy, options); + async start( + req: express.Request, + options: Record, + ): Promise { + const providerOptions = { + ...options, + accessType: 'offline', + prompt: 'consent', + }; + return await executeRedirectStrategy(req, this._strategy, providerOptions); } async handler( req: express.Request, - ): Promise<{ user: AuthInfoBase; info: AuthInfoPrivate }> { - return await executeFrameHandlerStrategy(req, this._strategy); + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const result = await executeFrameHandlerStrategy< + OAuthResponse, + PrivateInfo + >(req, this._strategy); + + return { + response: result.response, + refreshToken: result.privateInfo.refreshToken, + }; } - async refresh( - refreshToken: string, - scope: string, - ): Promise { + async refresh(refreshToken: string, scope: string): Promise { const { accessToken, params } = await executeRefreshTokenStrategy( this._strategy, refreshToken, @@ -100,14 +118,16 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { const profile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params, + params.id_token, ); return { - accessToken, - idToken: params.id_token, - expiresInSeconds: params.expires_in, - scope: params.scope, + providerInfo: { + accessToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }, profile, }; } diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 621b75078c..867bd280a2 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -15,7 +15,11 @@ */ import express from 'express'; -import { Strategy as SamlStrategy } from 'passport-saml'; +import { + Strategy as SamlStrategy, + Profile as SamlProfile, + VerifyWithoutRequest, +} from 'passport-saml'; import { executeFrameHandlerStrategy, executeRedirectStrategy, @@ -25,6 +29,8 @@ import { AuthProviderRouteHandlers, EnvironmentProviderConfig, SAMLProviderConfig, + PassportDoneCallback, + ProfileInfo, } from '../types'; import { postMessageResponse } from '../../lib/OAuthProvider'; import { @@ -32,30 +38,39 @@ import { EnvironmentHandler, } from '../../lib/EnvironmentHandler'; import { Logger } from 'winston'; +import { TokenIssuer } from '../../identity'; + +type SamlInfo = { + userId: string; + profile: ProfileInfo; +}; export class SamlAuthProvider implements AuthProviderRouteHandlers { private readonly strategy: SamlStrategy; + private readonly tokenIssuer: TokenIssuer; constructor(options: SAMLProviderOptions) { - this.strategy = new SamlStrategy( - { ...options }, - (profile: any, done: any) => { - // TODO: There's plenty more validation and profile handling to do here, - // this provider is currently only intended to validate the provider pattern - // for non-oauth auth flows. - // TODO: This flow doesn't issue an identity token that can be used to validate - // the identity of the user in other backends, which we need in some form. - done(undefined, { - email: profile.email, - firstName: profile.firstName, - lastName: profile.lastName, - displayName: profile.displayName, - }); - }, - ); + this.tokenIssuer = options.tokenIssuer; + this.strategy = new SamlStrategy({ ...options }, (( + profile: SamlProfile, + done: PassportDoneCallback, + ) => { + // TODO: There's plenty more validation and profile handling to do here, + // this provider is currently only intended to validate the provider pattern + // for non-oauth auth flows. + // TODO: This flow doesn't issue an identity token that can be used to validate + // the identity of the user in other backends, which we need in some form. + done(undefined, { + userId: profile.ID!, + profile: { + email: profile.email!, + displayName: profile.displayName as string, + }, + }); + }) as VerifyWithoutRequest); } - async start(req: express.Request, res: express.Response): Promise { + async start(req: express.Request, res: express.Response): Promise { const { url } = await executeRedirectStrategy(req, this.strategy, {}); res.redirect(url); } @@ -63,17 +78,27 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { async frameHandler( req: express.Request, res: express.Response, - ): Promise { + ): Promise { try { - const { user } = await executeFrameHandlerStrategy(req, this.strategy); + const { + response: { userId, profile }, + } = await executeFrameHandlerStrategy(req, this.strategy); + + const userIdToken = await this.tokenIssuer.issueToken({ + claims: { sub: userId }, + }); return postMessageResponse(res, 'http://localhost:3000', { - type: 'auth-result', - payload: user, + type: 'authorization_response', + response: { + providerInfo: {}, + profile, + userIdToken, + }, }); } catch (error) { return postMessageResponse(res, 'http://localhost:3000', { - type: 'auth-result', + type: 'authorization_response', error: { name: error.name, message: error.message, @@ -82,7 +107,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { } } - async logout(_req: express.Request, res: express.Response): Promise { + async logout(_req: express.Request, res: express.Response): Promise { res.send('noop'); } } @@ -91,12 +116,14 @@ type SAMLProviderOptions = { entryPoint: string; issuer: string; path: string; + tokenIssuer: TokenIssuer; }; export function createSamlProvider( _authProviderConfig: AuthProviderConfig, providerConfig: EnvironmentProviderConfig, logger: Logger, + tokenIssuer: TokenIssuer, ) { const envProviders: EnvironmentHandlers = {}; @@ -106,6 +133,7 @@ export function createSamlProvider( entryPoint: config.entryPoint, issuer: config.issuer, path: '/auth/saml/handler/frame', + tokenIssuer, }; if (!opts.entryPoint || !opts.issuer) { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 3660751c1c..2101fc19bb 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -89,25 +89,30 @@ export interface OAuthProviderHandlers { * @param {express.Request} req * @param options */ - start(req: express.Request, options: any): Promise; + start( + req: express.Request, + options: Record, + ): Promise; /** * Handles the redirect from the auth provider when the user has signed in. * @param {express.Request} req */ - handler(req: express.Request): Promise; + handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken?: string }>; /** * (Optional) Given a refresh token and scope fetches a new access token from the auth provider. * @param {string} refreshToken * @param {string} scope */ - refresh?(refreshToken: string, scope: string): Promise; + refresh?(refreshToken: string, scope: string): Promise; /** * (Optional) Sign out of the auth provider. */ - logout?(): Promise; + logout?(): Promise; } /** @@ -134,7 +139,7 @@ export interface AuthProviderRouteHandlers { * @param {express.Request} req * @param {express.Response} res */ - start(req: express.Request, res: express.Response): Promise; + start(req: express.Request, res: express.Response): Promise; /** * Once the user signs in or consents in the OAuth screen, the auth provider redirects to the @@ -149,7 +154,7 @@ export interface AuthProviderRouteHandlers { * @param {express.Request} req * @param {express.Response} res */ - frameHandler(req: express.Request, res: express.Response): Promise; + frameHandler(req: express.Request, res: express.Response): Promise; /** * (Optional) If the auth provider supports refresh tokens then this method handles @@ -163,7 +168,7 @@ export interface AuthProviderRouteHandlers { * @param {express.Request} req * @param {express.Response} res */ - refresh?(req: express.Request, res: express.Response): Promise; + refresh?(req: express.Request, res: express.Response): Promise; /** * (Optional) Handles sign out requests @@ -174,7 +179,7 @@ export interface AuthProviderRouteHandlers { * @param {express.Request} req * @param {express.Response} res */ - logout?(req: express.Request, res: express.Response): Promise; + logout?(req: express.Request, res: express.Response): Promise; } export type AuthProviderFactory = ( @@ -184,7 +189,18 @@ export type AuthProviderFactory = ( issuer: TokenIssuer, ) => AuthProviderRouteHandlers; -export type AuthInfoBase = { +export type AuthResponse = { + providerInfo: ProviderInfo; + profile: ProfileInfo; + userIdToken: string; +}; + +export type OAuthResponse = Omit< + AuthResponse, + 'userIdToken' +>; + +export type OAuthProviderInfo = { /** * An access token issued for the signed in user. */ @@ -203,34 +219,35 @@ export type AuthInfoBase = { scope: string; }; -export type AuthInfoWithProfile = AuthInfoBase & { - /** - * Profile information of the signed in user. - */ - profile: ProfileInfo | undefined; -}; - -export type AuthInfoPrivate = { +export type OAuthPrivateInfo = { /** * A refresh token issued for the signed in user. */ refreshToken: string; }; +// {type: 'authorization_response', response: {...}} + /** * Payload sent as a post message after the auth request is complete. * If successful then has a valid payload with Auth information else contains an error. */ -export type AuthResponse = +export type WebMessageResponse = | { - type: 'auth-result'; - payload: AuthInfoBase | AuthInfoWithProfile; + type: 'authorization_response'; + response: AuthResponse; } | { - type: 'auth-result'; + type: 'authorization_response'; error: Error; }; +export type PassportDoneCallback = ( + err?: Error, + response?: Res, + privateInfo?: Private, +) => void; + export type RedirectInfo = { /** * URL to redirect to @@ -242,6 +259,22 @@ export type RedirectInfo = { status?: number; }; +/* +metadata: + name: john +spec: + profile: + email: john.doe@example.com + displayName: John Doe + picture: https://example.com/avatars/john.doe + + identities: + - type: google + email: john.doe@gmail.com + */ + +// 1. Link to identity in catalog / within backstage +// 2. Display login information to user, i.e. sidebar popup export type ProfileInfo = { /** * Email ID of the signed in user. @@ -250,12 +283,12 @@ export type ProfileInfo = { /** * Display name that can be presented to the signed in user. */ - name: string; + displayName?: string; /** * URL to an image that can be used as the display image or avatar of the * signed in user. */ - picture: string; + picture?: string; }; export type RefreshTokenResponse = {