diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts deleted file mode 100644 index a61d51b27b..0000000000 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2020 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 express from 'express'; -import { - safelyEncodeURIComponent, - ensuresXRequestedWith, - postMessageResponse, -} from './authFlowHelpers'; -import { WebMessageResponse } from './types'; - -describe('oauth helpers', () => { - describe('safelyEncodeURIComponent', () => { - it('encodes all occurrences of single quotes', () => { - expect(safelyEncodeURIComponent("a'ö'b")).toBe('a%27%C3%B6%27b'); - }); - }); - - describe('postMessageResponse', () => { - const appOrigin = 'http://localhost:3000'; - it('should post a message back with payload success', () => { - const mockResponse = { - end: jest.fn().mockReturnThis(), - setHeader: jest.fn().mockReturnThis(), - } as unknown as express.Response; - - const data: WebMessageResponse = { - type: 'authorization_response', - response: { - providerInfo: { - accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', - expiresInSeconds: 10, - scope: 'email', - }, - profile: { - email: 'foo@bar.com', - }, - backstageIdentity: { - token: 'a.b.c', - identity: { - type: 'user', - ownershipEntityRefs: [], - userEntityRef: 'a', - }, - }, - }, - }; - const encoded = safelyEncodeURIComponent(JSON.stringify(data)); - - postMessageResponse(mockResponse, appOrigin, data); - expect(mockResponse.setHeader).toHaveBeenCalledTimes(3); - expect(mockResponse.end).toHaveBeenCalledTimes(1); - expect(mockResponse.end).toHaveBeenCalledWith( - expect.stringContaining(encoded), - ); - }); - - it('should post a message back with payload error', () => { - const mockResponse = { - end: jest.fn().mockReturnThis(), - setHeader: jest.fn().mockReturnThis(), - } as unknown as express.Response; - - const data: WebMessageResponse = { - type: 'authorization_response', - error: new Error('Unknown error occurred'), - }; - const encoded = safelyEncodeURIComponent(JSON.stringify(data)); - - postMessageResponse(mockResponse, appOrigin, data); - expect(mockResponse.setHeader).toHaveBeenCalledTimes(3); - expect(mockResponse.end).toHaveBeenCalledTimes(1); - expect(mockResponse.end).toHaveBeenCalledWith( - expect.stringContaining(encoded), - ); - }); - - it('should call postMessage twice but only one of them with target *', () => { - let responseBody = ''; - - const mockResponse = { - end: jest.fn(body => { - responseBody = body; - return this; - }), - setHeader: jest.fn().mockReturnThis(), - } as unknown as express.Response; - - const data: WebMessageResponse = { - type: 'authorization_response', - response: { - providerInfo: { - accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', - expiresInSeconds: 10, - scope: 'email', - }, - profile: { - email: 'foo@bar.com', - }, - backstageIdentity: { - token: 'a.b.c', - identity: { - type: 'user', - ownershipEntityRefs: [], - userEntityRef: 'a', - }, - }, - }, - }; - postMessageResponse(mockResponse, appOrigin, data); - expect(responseBody.match(/.postMessage\(/g)).toHaveLength(2); - expect( - responseBody.match(/.postMessage\([a-zA-Z.()]*, \'\*\'\)/g), - ).toHaveLength(1); - - const errData: WebMessageResponse = { - type: 'authorization_response', - error: new Error('Unknown error occurred'), - }; - postMessageResponse(mockResponse, appOrigin, errData); - expect(responseBody.match(/.postMessage\(/g)).toHaveLength(2); - expect( - responseBody.match(/.postMessage\([a-zA-Z.()]*, \'\*\'\)/g), - ).toHaveLength(1); - }); - - it('handles single quotes and unicode chars safely', () => { - const mockResponse = { - end: jest.fn().mockReturnThis(), - setHeader: jest.fn().mockReturnThis(), - } as unknown as express.Response; - - const data: WebMessageResponse = { - type: 'authorization_response', - response: { - providerInfo: { - accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', - expiresInSeconds: 10, - scope: 'email', - }, - profile: { - email: 'foo@bar.com', - displayName: "Adam l'Hôpital", - }, - backstageIdentity: { - token: 'a.b.c', - identity: { - type: 'user', - ownershipEntityRefs: [], - userEntityRef: 'a', - }, - }, - }, - }; - - postMessageResponse(mockResponse, appOrigin, data); - expect(mockResponse.setHeader).toHaveBeenCalledTimes(3); - expect(mockResponse.end).toHaveBeenCalledTimes(1); - expect(mockResponse.end).toHaveBeenCalledWith( - expect.stringContaining('Adam%20l%27H%C3%B4pital'), - ); - }); - }); - - describe('ensuresXRequestedWith', () => { - it('should return false if no header present', () => { - const mockRequest = { - header: () => jest.fn(), - } as unknown as express.Request; - expect(ensuresXRequestedWith(mockRequest)).toBe(false); - }); - - it('should return false if header present with incorrect value', () => { - const mockRequest = { - header: () => 'INVALID', - } as unknown as express.Request; - expect(ensuresXRequestedWith(mockRequest)).toBe(false); - }); - - it('should return true if header present with correct value', () => { - const mockRequest = { - header: () => 'XMLHttpRequest', - } as unknown as express.Request; - expect(ensuresXRequestedWith(mockRequest)).toBe(true); - }); - }); -}); diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts deleted file mode 100644 index 2f1770a3d4..0000000000 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2020 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 express from 'express'; -import crypto from 'crypto'; -import { WebMessageResponse } from './types'; - -export const safelyEncodeURIComponent = (value: string) => { - // Note the g at the end of the regex; all occurrences of single quotes must - // be replaced, which encodeURIComponent does not do itself by default - return encodeURIComponent(value).replace(/'/g, '%27'); -}; - -/** - * @public - * @deprecated Use `sendWebMessageResponse` from `@backstage/plugin-auth-node` instead - */ -export const postMessageResponse = ( - res: express.Response, - appOrigin: string, - response: WebMessageResponse, -) => { - const jsonData = JSON.stringify(response); - const base64Data = safelyEncodeURIComponent(jsonData); - const base64Origin = safelyEncodeURIComponent(appOrigin); - - // NOTE: It is absolutely imperative that we use the safe encoder above, to - // be sure that the js code below does not allow the injection of malicious - // data. - - // TODO: Make target app origin configurable globally - - // - // postMessage fails silently if the targetOrigin is disallowed. - // So 2 postMessages are sent from the popup to the parent window. - // First, the origin being used to post the actual authorization response is - // shared with the parent window with a postMessage with targetOrigin '*'. - // Second, the actual authorization response is sent with the app origin - // as the targetOrigin. - // If the first message was received but the actual auth response was - // never received, the event listener can conclude that targetOrigin - // was disallowed, indicating potential misconfiguration. - // - const script = ` - var authResponse = decodeURIComponent('${base64Data}'); - var origin = decodeURIComponent('${base64Origin}'); - var originInfo = {'type': 'config_info', 'targetOrigin': origin}; - (window.opener || window.parent).postMessage(originInfo, '*'); - (window.opener || window.parent).postMessage(JSON.parse(authResponse), origin); - setTimeout(() => { - window.close(); - }, 100); // same as the interval of the core-app-api lib/loginPopup.ts (to address race conditions) - `; - const hash = crypto.createHash('sha256').update(script).digest('base64'); - - res.setHeader('Content-Type', 'text/html'); - res.setHeader('X-Frame-Options', 'sameorigin'); - res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`); - res.end(``); -}; - -/** - * @public - * @deprecated Use inline logic to check that the `X-Requested-With` header is set to `'XMLHttpRequest'` instead. - */ -export const ensuresXRequestedWith = (req: express.Request) => { - const requiredHeader = req.header('X-Requested-With'); - if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') { - return false; - } - return true; -}; diff --git a/plugins/auth-backend/src/lib/flow/index.ts b/plugins/auth-backend/src/lib/flow/index.ts deleted file mode 100644 index f7b4491edb..0000000000 --- a/plugins/auth-backend/src/lib/flow/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2020 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. - */ - -export { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers'; - -export type { WebMessageResponse } from './types'; diff --git a/plugins/auth-backend/src/lib/flow/types.ts b/plugins/auth-backend/src/lib/flow/types.ts deleted file mode 100644 index 36f3033b21..0000000000 --- a/plugins/auth-backend/src/lib/flow/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 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 { WebMessageResponse as _WebMessageResponse } from '@backstage/plugin-auth-node'; - -/** - * @public - * @deprecated import from `@backstage/plugin-auth-node` instead - */ -export type WebMessageResponse = _WebMessageResponse; diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts deleted file mode 100644 index d9e80263ee..0000000000 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright 2020 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 express from 'express'; -import { UnsecuredJWT } from 'jose'; -import passport from 'passport'; -import { InternalOAuthError } from 'passport-oauth2'; -import { - executeRedirectStrategy, - executeFrameHandlerStrategy, - executeRefreshTokenStrategy, - makeProfileInfo, -} from './PassportStrategyHelper'; -import { PassportProfile } from './types'; - -const mockRequest = {} as unknown as express.Request; - -describe('PassportStrategyHelper', () => { - describe('makeProfileInfo', () => { - it('retrieves email from passport profile', () => { - const profile: PassportProfile = { - emails: [{ value: 'email' }], - provider: '', - id: '', - displayName: '', - }; - - const profileInfo = makeProfileInfo(profile); - - expect(profileInfo.email).toEqual('email'); - }); - - it('retrieves picture from passport profile avatarUrl', () => { - const profile: PassportProfile = { - avatarUrl: 'avatarUrl', - provider: '', - id: '', - displayName: '', - }; - - const profileInfo = makeProfileInfo(profile); - - expect(profileInfo.picture).toEqual('avatarUrl'); - }); - - it('falls back to picture from passport profile photos field', () => { - const profile: PassportProfile = { - photos: [{ value: 'picture' }], - provider: '', - id: '', - displayName: '', - }; - - const profileInfo = makeProfileInfo(profile); - - expect(profileInfo.picture).toEqual('picture'); - }); - - it('falls back to email from ID token', async () => { - const profile: PassportProfile = { - provider: '', - id: '', - displayName: '', - }; - - const profileInfo = makeProfileInfo( - profile, - await new UnsecuredJWT({ email: 'email' }).encode(), - ); - - expect(profileInfo.email).toEqual('email'); - }); - - it('falls back to picture from ID token', async () => { - const profile: PassportProfile = { - provider: '', - id: '', - displayName: '', - }; - - const profileInfo = makeProfileInfo( - profile, - await new UnsecuredJWT({ picture: 'picture' }).encode(), - ); - - expect(profileInfo.picture).toEqual('picture'); - }); - - it('falls back to name from ID token', async () => { - const profile: PassportProfile = { - provider: '', - id: '', - displayName: '', - }; - - const profileInfo = makeProfileInfo( - profile, - await new UnsecuredJWT({ name: 'name' }).encode(), - ); - - expect(profileInfo.displayName).toEqual('name'); - }); - - it('fails when attempting to fall back to invalid JWT', () => { - const profile: PassportProfile = { - provider: '', - id: '', - displayName: '', - }; - - expect(() => makeProfileInfo(profile, 'invalid JWT')).toThrow( - 'Failed to parse id token and get profile info', - ); - }); - }); - - class MyCustomRedirectStrategy extends passport.Strategy { - authenticate() { - this.redirect('a', 302); - } - } - - describe('executeRedirectStrategy', () => { - it('should call authenticate and resolve with OAuthStartResponse', async () => { - const mockStrategy = new MyCustomRedirectStrategy(); - const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate'); - const redirectStrategyPromise = executeRedirectStrategy( - mockRequest, - mockStrategy, - {}, - ); - expect(spyAuthenticate).toHaveBeenCalledTimes(1); - await expect(redirectStrategyPromise).resolves.toStrictEqual( - expect.objectContaining({ url: 'a', status: 302 }), - ); - }); - }); - - describe('executeFrameHandlerStrategy', () => { - class MyCustomAuthSuccessStrategy extends passport.Strategy { - authenticate() { - this.success( - { accessToken: 'ACCESS_TOKEN' }, - { refreshToken: 'REFRESH_TOKEN' }, - ); - } - } - class MyCustomAuthErrorStrategy extends passport.Strategy { - authenticate() { - this.error( - new InternalOAuthError('MyCustomAuth error', { - data: '{ "message": "Custom message" }', - }), - ); - } - } - class MyCustomAuthRedirectStrategy extends passport.Strategy { - authenticate() { - this.redirect('URL', 302); - } - } - class MyCustomAuthFailStrategy extends passport.Strategy { - authenticate() { - this.fail('challenge', 302); - } - } - - it('should resolve with user and info on success', async () => { - const mockStrategy = new MyCustomAuthSuccessStrategy(); - const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate'); - const frameHandlerStrategyPromise = executeFrameHandlerStrategy( - mockRequest, - mockStrategy, - ); - expect(spyAuthenticate).toHaveBeenCalledTimes(1); - await expect(frameHandlerStrategyPromise).resolves.toStrictEqual( - expect.objectContaining({ - result: { accessToken: 'ACCESS_TOKEN' }, - privateInfo: { refreshToken: 'REFRESH_TOKEN' }, - }), - ); - }); - - it('should reject on error', async () => { - const mockStrategy = new MyCustomAuthErrorStrategy(); - const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate'); - const frameHandlerStrategyPromise = executeFrameHandlerStrategy( - mockRequest, - mockStrategy, - ); - expect(spyAuthenticate).toHaveBeenCalledTimes(1); - await expect(frameHandlerStrategyPromise).rejects.toThrow( - 'Authentication failed, MyCustomAuth error - Custom message', - ); - }); - - it('should reject on redirect', async () => { - const mockStrategy = new MyCustomAuthRedirectStrategy(); - const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate'); - const frameHandlerStrategyPromise = executeFrameHandlerStrategy( - mockRequest, - mockStrategy, - ); - expect(spyAuthenticate).toHaveBeenCalledTimes(1); - await expect(frameHandlerStrategyPromise).rejects.toThrow( - 'Unexpected redirect', - ); - }); - - it('should reject on fail', async () => { - const mockStrategy = new MyCustomAuthFailStrategy(); - const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate'); - const frameHandlerStrategyPromise = executeFrameHandlerStrategy( - mockRequest, - mockStrategy, - ); - expect(spyAuthenticate).toHaveBeenCalledTimes(1); - await expect(frameHandlerStrategyPromise).rejects.toThrow(); - }); - }); - - describe('executeRefreshTokenStrategy', () => { - it('should resolve with a new access token, scope and expiry', async () => { - class MyCustomOAuth2Success { - getOAuthAccessToken( - _refreshToken: string, - _options: any, - callback: Function, - ) { - callback(null, 'ACCESS_TOKEN', 'REFRESH_TOKEN', { - scope: 'a', - expires_in: 10, - }); - } - } - class MyCustomRefreshTokenSuccess extends passport.Strategy { - _oauth2 = new MyCustomOAuth2Success(); - userProfile(_accessToken: string, callback: Function) { - callback(null, { - provider: 'a', - email: 'b', - name: 'c', - picture: 'd', - }); - } - } - - const mockStrategy = new MyCustomRefreshTokenSuccess(); - const refreshTokenPromise = executeRefreshTokenStrategy( - mockStrategy, - 'REFRESH_TOKEN', - 'a', - ); - await expect(refreshTokenPromise).resolves.toStrictEqual( - expect.objectContaining({ - accessToken: 'ACCESS_TOKEN', - params: expect.objectContaining({ scope: 'a', expires_in: 10 }), - }), - ); - }); - - it('should forward simple errors', async () => { - class MyCustomRefreshTokenSuccess extends passport.Strategy { - _oauth2 = new (class { - getOAuthAccessToken(_r: string, _o: any, cb: Function) { - cb(new Error('Unknown error')); - } - })(); - } - - await expect( - executeRefreshTokenStrategy( - new MyCustomRefreshTokenSuccess(), - 'REFRESH_TOKEN', - 'a', - ), - ).rejects.toThrow( - 'Failed to refresh access token; caused by Error: Unknown error', - ); - }); - - it('should forward string errors', async () => { - class MyCustomRefreshTokenSuccess extends passport.Strategy { - _oauth2 = new (class { - getOAuthAccessToken(_r: string, _o: any, cb: Function) { - cb('some silly string error'); - } - })(); - } - - await expect( - executeRefreshTokenStrategy( - new MyCustomRefreshTokenSuccess(), - 'REFRESH_TOKEN', - 'a', - ), - ).rejects.toThrow( - "Failed to refresh access token; caused by unknown error 'some silly string error'", - ); - }); - - it('should forward object errors', async () => { - class MyCustomRefreshTokenSuccess extends passport.Strategy { - _oauth2 = new (class { - getOAuthAccessToken(_r: string, _o: any, cb: Function) { - cb({ name: 'SomeError', message: 'some message' }); - } - })(); - } - - await expect( - executeRefreshTokenStrategy( - new MyCustomRefreshTokenSuccess(), - 'REFRESH_TOKEN', - 'a', - ), - ).rejects.toThrow( - 'Failed to refresh access token; caused by SomeError: some message', - ); - }); - - it('should reject with an error if access token missing in refresh callback', async () => { - class MyCustomOAuth2AccessTokenMissing { - getOAuthAccessToken( - _refreshToken: string, - _options: any, - callback: Function, - ) { - callback(null, ''); - } - } - class MyCustomRefreshTokenSuccess extends passport.Strategy { - _oauth2 = new MyCustomOAuth2AccessTokenMissing(); - } - - const mockStrategy = new MyCustomRefreshTokenSuccess(); - const refreshTokenPromise = executeRefreshTokenStrategy( - mockStrategy, - 'REFRESH_TOKEN', - 'a', - ); - await expect(refreshTokenPromise).rejects.toThrow( - 'Failed to refresh access token, no access token received', - ); - }); - }); -}); diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts deleted file mode 100644 index ef9284c1fb..0000000000 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright 2020 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 express from 'express'; -import passport from 'passport'; -import { decodeJwt } from 'jose'; -import { InternalOAuthError } from 'passport-oauth2'; -import { ProfileInfo } from '@backstage/plugin-auth-node'; -import { PassportProfile } from './types'; -import { OAuthStartResponse } from '../../providers/types'; -import { ForwardedError } from '@backstage/errors'; - -export type PassportDoneCallback = ( - err?: Error, - response?: Res, - privateInfo?: Private, -) => void; - -export const makeProfileInfo = ( - profile: PassportProfile, - idToken?: string, -): ProfileInfo => { - let email: string | undefined = undefined; - if (profile.emails && profile.emails.length > 0) { - const [firstEmail] = profile.emails; - email = firstEmail.value; - } - - let picture: string | undefined = undefined; - if (profile.avatarUrl) { - picture = profile.avatarUrl; - } else if (profile.photos && profile.photos.length > 0) { - const [firstPhoto] = profile.photos; - picture = firstPhoto.value; - } - - let displayName: string | undefined = - profile.displayName ?? profile.username ?? profile.id; - - if ((!email || !picture || !displayName) && idToken) { - try { - const decoded = decodeJwt(idToken) as { - email?: string; - name?: string; - picture?: string; - }; - if (!email && decoded.email) { - email = decoded.email; - } - if (!picture && decoded.picture) { - picture = decoded.picture; - } - if (!displayName && decoded.name) { - displayName = decoded.name; - } - } catch (e) { - throw new ForwardedError( - `Failed to parse id token and get profile info`, - e, - ); - } - } - - return { - email, - picture, - displayName, - }; -}; - -export const executeRedirectStrategy = async ( - req: express.Request, - providerStrategy: passport.Strategy, - options: Record, -): Promise => { - return new Promise(resolve => { - const strategy = Object.create(providerStrategy); - strategy.redirect = (url: string, status?: number) => { - resolve({ url, status: status ?? undefined }); - }; - - strategy.authenticate(req, { ...options }); - }); -}; - -export const executeFrameHandlerStrategy = async ( - req: express.Request, - providerStrategy: passport.Strategy, - options?: Record, -) => { - return new Promise<{ result: Result; privateInfo: PrivateInfo }>( - (resolve, reject) => { - const strategy = Object.create(providerStrategy); - strategy.success = (result: any, privateInfo: any) => { - resolve({ result, privateInfo }); - }; - strategy.fail = ( - info: { type: 'success' | 'error'; message?: string }, - // _status: number, - ) => { - reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); - }; - strategy.error = (error: InternalOAuthError) => { - let message = `Authentication failed, ${error.message}`; - - if (error.oauthError?.data) { - try { - const errorData = JSON.parse(error.oauthError.data); - - if (errorData.message) { - message += ` - ${errorData.message}`; - } - } catch (parseError) { - message += ` - ${error.oauthError}`; - } - } - - reject(new Error(message)); - }; - strategy.redirect = () => { - reject(new Error('Unexpected redirect')); - }; - strategy.authenticate(req, { ...(options ?? {}) }); - }, - ); -}; - -type RefreshTokenResponse = { - /** - * An access token issued for the signed in user. - */ - accessToken: string; - /** - * Optionally, the server can issue a new Refresh Token for the user - */ - refreshToken?: string; - params: any; -}; - -export const executeRefreshTokenStrategy = async ( - providerStrategy: passport.Strategy, - refreshToken: string, - scope: string, -): Promise => { - return new Promise((resolve, reject) => { - const anyStrategy = providerStrategy as any; - const OAuth2 = anyStrategy._oauth2.constructor; - const oauth2 = new OAuth2( - anyStrategy._oauth2._clientId, - anyStrategy._oauth2._clientSecret, - anyStrategy._oauth2._baseSite, - anyStrategy._oauth2._authorizeUrl, - anyStrategy._refreshURL || anyStrategy._oauth2._accessTokenUrl, - anyStrategy._oauth2._customHeaders, - ); - - oauth2.getOAuthAccessToken( - refreshToken, - { - scope, - grant_type: 'refresh_token', - }, - ( - err: Error | null, - accessToken: string, - newRefreshToken: string, - params: any, - ) => { - if (err) { - reject(new ForwardedError(`Failed to refresh access token`, err)); - } - if (!accessToken) { - reject( - new Error( - `Failed to refresh access token, no access token received`, - ), - ); - } - - resolve({ - accessToken, - refreshToken: newRefreshToken, - params, - }); - }, - ); - }); -}; - -type ProviderStrategy = { - userProfile(accessToken: string, callback: Function): void; -}; - -export const executeFetchUserProfileStrategy = async ( - providerStrategy: passport.Strategy, - accessToken: string, -): Promise => { - return new Promise((resolve, reject) => { - const anyStrategy = providerStrategy as unknown as ProviderStrategy; - anyStrategy.userProfile( - accessToken, - (error: Error, rawProfile: PassportProfile) => { - if (error) { - reject(error); - } else { - resolve(rawProfile); - } - }, - ); - }); -}; diff --git a/plugins/auth-backend/src/lib/passport/index.ts b/plugins/auth-backend/src/lib/passport/index.ts deleted file mode 100644 index 17ab71f51f..0000000000 --- a/plugins/auth-backend/src/lib/passport/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2020 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. - */ - -export { - executeFetchUserProfileStrategy, - executeFrameHandlerStrategy, - executeRedirectStrategy, - executeRefreshTokenStrategy, - makeProfileInfo, -} from './PassportStrategyHelper'; -export type { PassportDoneCallback } from './PassportStrategyHelper'; diff --git a/plugins/auth-backend/src/lib/passport/types.ts b/plugins/auth-backend/src/lib/passport/types.ts deleted file mode 100644 index 55fe4543fc..0000000000 --- a/plugins/auth-backend/src/lib/passport/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 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 passport from 'passport'; - -export type PassportProfile = passport.Profile & { - avatarUrl?: string; -}; diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index 7f66179d81..0917d916c7 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -39,16 +39,7 @@ import { } from '@backstage/plugin-auth-node'; import { CatalogIdentityClient } from '../catalog'; -/** - * Uses the default ownership resolution logic to return an array - * of entity refs that the provided entity claims ownership through. - * - * A reference to the entity itself will also be included in the returned array. - * - * @public - * @deprecated use `ctx.resolveOwnershipEntityRefs(entity)` from the provided `AuthResolverContext` instead. - */ -export function getDefaultOwnershipEntityRefs(entity: Entity) { +function getDefaultOwnershipEntityRefs(entity: Entity) { const membershipRefs = entity.relations ?.filter( @@ -59,9 +50,6 @@ export function getDefaultOwnershipEntityRefs(entity: Entity) { return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs])); } -/** - * @internal - */ export class CatalogAuthResolverContext implements AuthResolverContext { static create(options: { logger: LoggerService; diff --git a/plugins/auth-backend/src/lib/resolvers/index.ts b/plugins/auth-backend/src/lib/resolvers/index.ts index c1ca59cb25..fc3ea8521c 100644 --- a/plugins/auth-backend/src/lib/resolvers/index.ts +++ b/plugins/auth-backend/src/lib/resolvers/index.ts @@ -14,7 +14,4 @@ * limitations under the License. */ -export { - CatalogAuthResolverContext, - getDefaultOwnershipEntityRefs, -} from './CatalogAuthResolverContext'; +export { CatalogAuthResolverContext } from './CatalogAuthResolverContext'; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 9f301893cd..abe3e77736 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -15,9 +15,3 @@ */ export { createOriginFilter, type ProviderFactories } from './router'; - -export type { - AuthHandler, - AuthHandlerResult, - OAuthStartResponse, -} from './types'; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts deleted file mode 100644 index f28995f15e..0000000000 --- a/plugins/auth-backend/src/providers/types.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020 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 { - AuthResolverContext as _AuthResolverContext, - ProfileInfo as _ProfileInfo, -} from '@backstage/plugin-auth-node'; - -/** - * @public - * @deprecated Use `createOAuthAuthenticator` from `@backstage/plugin-auth-node` instead - */ -export type OAuthStartResponse = { - /** - * URL to redirect to - */ - url: string; - /** - * Status code to use for the redirect - */ - status?: number; -}; - -/** - * The return type of an authentication handler. Must contain valid profile - * information. - * - * @public - * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead - */ -export type AuthHandlerResult = { profile: _ProfileInfo }; - -/** - * The AuthHandler function is called every time the user authenticates using - * the provider. - * - * The handler should return a profile that represents the session for the user - * in the frontend. - * - * Throwing an error in the function will cause the authentication to fail, - * making it possible to use this function as a way to limit access to a certain - * group of users. - * - * @public - * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead - */ -export type AuthHandler = ( - input: TAuthResult, - context: _AuthResolverContext, -) => Promise;