diff --git a/plugins/auth-backend/src/providers/GoogleAuthProvider.ts b/plugins/auth-backend/src/providers/GoogleAuthProvider.ts deleted file mode 100644 index 604a765906..0000000000 --- a/plugins/auth-backend/src/providers/GoogleAuthProvider.ts +++ /dev/null @@ -1,76 +0,0 @@ -import express from 'express'; -import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -import { - executeFrameHandlerStrategy, - executeRedirectStrategy, - executeRefreshTokenStrategy, -} from './PassportStrategyHelper'; -import { - OAuthProviderHandlers, - AuthInfoBase, - AuthInfoPrivate, - RedirectInfo, - AuthProviderConfig, -} from './types'; - -export class GoogleAuthProvider implements OAuthProviderHandlers { - private readonly provider: string; - private readonly providerConfig: AuthProviderConfig; - private readonly _strategy: GoogleStrategy; - - constructor(providerConfig: AuthProviderConfig) { - this.provider = providerConfig.provider; - this.providerConfig = providerConfig; - // TODO: throw error if env variables not set? - this._strategy = new GoogleStrategy( - { ...this.providerConfig.options }, - ( - accessToken: any, - refreshToken: any, - params: any, - profile: any, - done: any, - ) => { - done( - undefined, - { - profile, - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - { - refreshToken, - }, - ); - }, - ); - } - - async start(req: express.Request, options: any): 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 refresh(refreshToken: string, scope: string): Promise { - return await executeRefreshTokenStrategy( - this._strategy, - refreshToken, - scope, - ); - } - - logout(): Promise { - throw new Error('Method not implemented.'); - } - - getProvider(): string { - return this.provider; - } -} diff --git a/plugins/auth-backend/src/providers/OAuthHelper.ts b/plugins/auth-backend/src/providers/OAuthHelper.ts deleted file mode 100644 index 7f930530a9..0000000000 --- a/plugins/auth-backend/src/providers/OAuthHelper.ts +++ /dev/null @@ -1,70 +0,0 @@ -import express, { CookieOptions } from 'express'; -import crypto from 'crypto'; - -export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; -export const TEN_MINUTES_MS = 600 * 1000; - -// TODO: move all of these methods to OAuthProvider - -export const verifyNonce = (req: express.Request, provider: string) => { - const cookieNonce = req.cookies[`${provider}-nonce`]; - const stateNonce = req.query.state; - - if (!cookieNonce || !stateNonce) { - throw new Error('Missing nonce'); - } - - if (cookieNonce !== stateNonce) { - throw new Error('Invalid nonce'); - } -}; - -export const setNonceCookie = (res: express.Response, provider: string) => { - const nonce = crypto.randomBytes(16).toString('base64'); - - const options: CookieOptions = { - maxAge: TEN_MINUTES_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}/handler`, - httpOnly: true, - }; - - res.cookie(`${provider}-nonce`, nonce, options); - - return nonce; -}; - -export const setRefreshTokenCookie = ( - res: express.Response, - provider: string, - refreshToken: string, -) => { - const options: CookieOptions = { - maxAge: THOUSAND_DAYS_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}`, - httpOnly: true, - }; - - res.cookie(`${provider}-refresh-token`, refreshToken, options); -}; - -export const removeRefreshTokenCookie = ( - res: express.Response, - provider: string, -) => { - const options: CookieOptions = { - maxAge: 0, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}`, - httpOnly: true, - }; - - res.cookie(`${provider}-refresh-token`, '', options); -}; diff --git a/plugins/auth-backend/src/providers/OAuthProvider.ts b/plugins/auth-backend/src/providers/OAuthProvider.ts index d294654453..d02f7da110 100644 --- a/plugins/auth-backend/src/providers/OAuthProvider.ts +++ b/plugins/auth-backend/src/providers/OAuthProvider.ts @@ -1,14 +1,12 @@ +import express, { CookieOptions } from 'express'; +import crypto from 'crypto'; import { AuthProviderRouteHandlers, OAuthProviderHandlers } from './types'; -import express from 'express'; import { InputError } from '@backstage/backend-common'; -import { - setNonceCookie, - verifyNonce, - setRefreshTokenCookie, - removeRefreshTokenCookie, -} from './OAuthHelper'; import { postMessageResponse, ensuresXRequestedWith } from './utils'; +export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; +export const TEN_MINUTES_MS = 600 * 1000; + export class OAuthProvider implements AuthProviderRouteHandlers { private readonly provider: string; private readonly providerHandlers: OAuthProviderHandlers; @@ -114,3 +112,66 @@ export class OAuthProvider implements AuthProviderRouteHandlers { } } } + +export const verifyNonce = (req: express.Request, provider: string) => { + const cookieNonce = req.cookies[`${provider}-nonce`]; + const stateNonce = req.query.state; + + if (!cookieNonce || !stateNonce) { + throw new Error('Missing nonce'); + } + + if (cookieNonce !== stateNonce) { + throw new Error('Invalid nonce'); + } +}; + +export const setNonceCookie = (res: express.Response, provider: string) => { + const nonce = crypto.randomBytes(16).toString('base64'); + + const options: CookieOptions = { + maxAge: TEN_MINUTES_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}/handler`, + httpOnly: true, + }; + + res.cookie(`${provider}-nonce`, nonce, options); + + return nonce; +}; + +export const setRefreshTokenCookie = ( + res: express.Response, + provider: string, + refreshToken: string, +) => { + const options: CookieOptions = { + maxAge: THOUSAND_DAYS_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, refreshToken, options); +}; + +export const removeRefreshTokenCookie = ( + res: express.Response, + provider: string, +) => { + const options: CookieOptions = { + maxAge: 0, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, '', options); +}; diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts index e33c183381..515a91f25c 100644 --- a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts @@ -85,7 +85,7 @@ export const executeRefreshTokenStrategy = async ( resolve({ accessToken, idToken: params.id_token, - expiresInSeconds: params.expires_in, + expiresInSeconds: 10, scope: params.scope, }); }, diff --git a/plugins/auth-backend/src/providers/factories.test.ts b/plugins/auth-backend/src/providers/factories.test.ts deleted file mode 100644 index cc31834889..0000000000 --- a/plugins/auth-backend/src/providers/factories.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -// /* -// * 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 express from 'express'; -// import passport from 'passport'; -// import { OAuthProviderHandlers } from './types'; -// import { ProviderFactories } from './factories'; - -// class MyAuthProvider implements OAuthProviderHandlers { -// async start(_: express.Request, res: express.Response): Promise { -// res.send('start'); -// } -// async logout(_: express.Request, res: express.Response): Promise { -// res.send('logout'); -// } -// async handler(): Promise { -// throw new Error('Method not implemented.'); -// } -// async refresh(): Promise { -// throw new Error('Method not implemented.'); -// } -// } - -// describe('getProviderFactory', () => { -// it('makes a provider for MyAuthProvider', () => { -// jest -// .spyOn(ProviderFactories, 'getProviderFactory') -// .mockReturnValueOnce(MyAuthProvider); -// const provider = ProviderFactories.getProviderFactory('a'); -// expect(provider).toBeDefined(); -// }); - -// it('throws an error when provider implementation does not exist', () => { -// expect(() => { -// ProviderFactories.getProviderFactory('b'); -// }).toThrow('Provider Implementation missing for : b auth provider'); -// }); -// }); diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 76c3bd3dde..0a1e639082 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -15,8 +15,7 @@ */ import { AuthProviderFactories, AuthProviderFactory } from './types'; -// import { GoogleAuthProvider } from './google/provider'; -import { GoogleAuthProvider } from './GoogleAuthProvider'; +import { GoogleAuthProvider } from './google'; export class ProviderFactories { private static readonly providerFactories: AuthProviderFactories = { diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts index d79c9e34e9..5a94d88ada 100644 --- a/plugins/auth-backend/src/providers/google/index.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -1 +1 @@ -// export { GoogleAuthProvider } from './provider'; +export { GoogleAuthProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts deleted file mode 100644 index 03f3da1eda..0000000000 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ /dev/null @@ -1,531 +0,0 @@ -// /* -// * 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, -// THOUSAND_DAYS_MS, -// TEN_MINUTES_MS, -// } from './provider'; -// import passport from 'passport'; -// import express from 'express'; -// import * as utils from './../utils'; -// import refresh from 'passport-oauth2-refresh'; - -// const googleAuthProviderConfig = { -// provider: 'google', -// options: { -// clientID: 'a', -// clientSecret: 'b', -// callbackURL: 'c', -// }, -// }; - -// const googleAuthProviderConfigInvalidOptions = { -// provider: 'google', -// options: {}, -// }; - -// describe('GoogleAuthProvider', () => { -// afterEach(() => { -// jest.clearAllMocks(); -// }); -// describe('create a new provider', () => { -// it('should succeed with valid config', () => { -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); -// expect(googleAuthProvider).toBeDefined(); -// expect(googleAuthProvider.start).toBeDefined(); -// expect(googleAuthProvider.logout).toBeDefined(); -// expect(googleAuthProvider.frameHandler).toBeDefined(); -// expect(googleAuthProvider.strategy).toBeDefined(); -// }); -// }); - -// describe('start authentication handler', () => { -// const mockResponse = ({ -// send: jest.fn().mockReturnThis(), -// status: jest.fn().mockReturnThis(), -// cookie: jest.fn().mockReturnThis(), -// } as unknown) as express.Response; - -// fit('should initiate authenticate request with provided scopes', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// query: { -// scope: 'a,b', -// }, -// } as unknown) as express.Request; - -// // const spyPassport = jest -// // .spyOn(passport, 'authenticate') -// // .mockImplementation(() => jest.fn()); -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// const googleAuthProviderStrategy = googleAuthProvider.strategy(); -// const spyAuthenticate = jest -// .spyOn(googleAuthProviderStrategy, 'authenticate') -// .mockImplementation(() => jest.fn()); - -// // const spyRedirect = jest -// // .spyOn(googleAuthProviderStrategy, 'redirect') -// // .mockImplementation(() => jest.fn()); - -// googleAuthProvider.start(mockRequest, mockResponse); -// expect(spyAuthenticate).toBeCalledTimes(1); -// expect(spyAuthenticate).toBeCalledWith(mockRequest, { -// scope: 'a,b', -// accessType: 'offline', -// prompt: 'consent', -// state: expect.any(String), -// }); -// // expect(spyRedirect).toBeCalledTimes(1); -// // expect(spyPassport).toBeCalledTimes(1); -// }); - -// it('should set a nonce cookie', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// query: { -// scope: 'a,b', -// }, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); -// googleAuthProvider.start(mockRequest, mockResponse); -// expect(mockResponse.cookie).toBeCalledTimes(1); -// expect(mockResponse.cookie).toBeCalledWith( -// 'google-nonce', -// expect.any(String), -// expect.objectContaining({ -// maxAge: TEN_MINUTES_MS, -// path: `/auth/${googleAuthProviderConfig.provider}/handler`, -// }), -// ); -// }); - -// it('should throw error if no scopes provided', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// query: {}, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); -// expect(() => { -// googleAuthProvider.start(mockRequest, mockResponse); -// }).toThrowError('missing scope parameter'); -// }); -// }); - -// describe('logout handler', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// } as unknown) as express.Request; - -// it('should perform logout and respond with 200', () => { -// const mockResponse: any = ({ -// send: jest.fn(), -// cookie: jest.fn(), -// } as unknown) as express.Response; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// const spyResponse = jest -// .spyOn(mockResponse, 'send') -// .mockImplementation(() => jest.fn()); - -// googleAuthProvider.logout(mockRequest, mockResponse); -// expect(spyResponse).toBeCalledTimes(1); -// expect(spyResponse).toBeCalledWith('logout!'); -// expect(mockResponse.cookie).toBeCalledTimes(1); -// expect(mockResponse.cookie).toBeCalledWith( -// 'google-refresh-token', -// '', -// expect.objectContaining({ maxAge: 0 }), -// ); -// }); -// }); - -// describe('redirect frame handler', () => { -// const mockResponse: any = ({ -// status: jest.fn().mockReturnThis(), -// send: jest.fn().mockReturnThis(), -// cookie: jest.fn().mockReturnThis(), -// } as unknown) as express.Response; - -// it('should call authenticate and post a response', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-nonce': 'NONCE' }, -// query: { -// state: 'NONCE', -// }, -// } as unknown) as express.Request; - -// const spyPostMessage = jest -// .spyOn(utils, 'postMessageResponse') -// .mockImplementation(() => jest.fn()); - -// const spyPassport = jest -// .spyOn(passport, 'authenticate') -// .mockImplementation((_x, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(null, { refreshToken: 'REFRESH_TOKEN' }); -// return jest.fn(); -// }); - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(spyPassport).toBeCalledTimes(1); -// expect(spyPostMessage).toBeCalledTimes(1); -// expect(mockResponse.cookie).toBeCalledTimes(1); -// expect(mockResponse.cookie).toBeCalledWith( -// 'google-refresh-token', -// 'REFRESH_TOKEN', -// expect.objectContaining({ -// path: '/auth/google', -// sameSite: 'none', -// httpOnly: true, -// maxAge: THOUSAND_DAYS_MS, -// }), -// ); -// }); - -// it('should respond with a error message if no refresh token returned', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-nonce': 'NONCE' }, -// query: { -// state: 'NONCE', -// }, -// } as unknown) as express.Request; - -// const spyPassport = jest -// .spyOn(passport, 'authenticate') -// .mockImplementation((_x, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(null, {}); -// return jest.fn(); -// }); - -// const spyPostMessage = jest -// .spyOn(utils, 'postMessageResponse') -// .mockImplementation(() => jest.fn()); - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(spyPassport).toBeCalledTimes(1); -// expect(spyPostMessage).toBeCalledTimes(1); -// expect(spyPostMessage).toBeCalledWith(mockResponse, { -// type: 'auth-result', -// error: new Error('Missing refresh token'), -// }); -// }); - -// it('should respond with a error message if auth failed', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-nonce': 'NONCE' }, -// query: { -// state: 'NONCE', -// }, -// } as unknown) as express.Request; - -// const spyPassport = jest -// .spyOn(passport, 'authenticate') -// .mockImplementation((_x, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(new Error('TokenError'), null); -// return jest.fn(); -// }); - -// const spyPostMessage = jest -// .spyOn(utils, 'postMessageResponse') -// .mockImplementation(() => jest.fn()); - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(spyPassport).toBeCalledTimes(1); -// expect(spyPostMessage).toBeCalledTimes(1); -// expect(spyPostMessage).toBeCalledWith(mockResponse, { -// type: 'auth-result', -// error: new Error('Google auth failed, Error: TokenError'), -// }); -// }); - -// it('should respond with a error message if cookie nonce is missing', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: {}, -// query: { state: 'NONCE' }, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith('Missing nonce'); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// }); - -// it('should respond with a error message if state nonce is missing', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-nonce': 'NONCE' }, -// query: {}, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith('Missing nonce'); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// }); - -// it('should respond with a error message if nonce mismatch', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-nonce': 'NONCA' }, -// query: { state: 'NONCEB' }, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith('Invalid nonce'); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// }); -// }); - -// describe('strategy handler', () => { -// it('should return a valid passport strategy', () => { -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// expect(googleAuthProvider.strategy()).toBeInstanceOf(passport.Strategy); -// }); - -// it('should throw an error for invalid options', () => { -// expect(() => { -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfigInvalidOptions, -// ); -// googleAuthProvider.strategy(); -// }).toThrow(); -// }); -// }); - -// describe('refresh token handler', () => { -// const mockResponse = ({ -// status: jest.fn().mockReturnThis(), -// send: jest.fn().mockReturnThis(), -// } as unknown) as express.Response; - -// describe('no refresh token cookie', () => { -// it('should respond with a 401', () => { -// const mockRequest = ({ -// cookies: jest.fn(), -// header: () => 'XMLHttpRequest', -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.refresh(mockRequest, mockResponse); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith('Missing session cookie'); - -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// }); -// }); - -// describe('refresh token cookie, no scope', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, -// query: {}, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// it('should request for a new access token and fail if no access token returned', () => { -// const spyRefresh = jest -// .spyOn(refresh, 'requestNewAccessToken') -// .mockImplementation((_x, _y, _z, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(undefined, undefined, undefined, {}); -// }); - -// googleAuthProvider.refresh(mockRequest, mockResponse); -// expect(spyRefresh).toBeCalledTimes(1); -// expect(spyRefresh).toBeCalledWith( -// 'google', -// 'REFRESH_TOKEN', -// {}, -// expect.any(Function), -// ); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith( -// 'Failed to refresh access token', -// ); -// }); - -// it('should request for a new access token and return 401 if any error', () => { -// const spyRefresh = jest -// .spyOn(refresh, 'requestNewAccessToken') -// .mockImplementation((_x, _y, _z, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb({ error: 'ERROR' }, undefined, undefined, {}); -// }); - -// googleAuthProvider.refresh(mockRequest, mockResponse); -// expect(spyRefresh).toBeCalledTimes(1); -// expect(spyRefresh).toBeCalledWith( -// 'google', -// 'REFRESH_TOKEN', -// {}, -// expect.any(Function), -// ); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith( -// 'Failed to refresh access token', -// ); -// }); - -// it('should fetch and return a new access token', () => { -// const spyRefresh = jest -// .spyOn(refresh, 'requestNewAccessToken') -// .mockImplementation((_x, _y, _z, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(undefined, 'ACCESS_TOKEN', undefined, { -// expires_in: 'EXPIRES_IN', -// id_token: 'ID_TOKEN', -// }); -// }); - -// googleAuthProvider.refresh(mockRequest, mockResponse); -// expect(spyRefresh).toBeCalledTimes(1); -// expect(spyRefresh).toBeCalledWith( -// 'google', -// 'REFRESH_TOKEN', -// {}, -// expect.any(Function), -// ); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith({ -// accessToken: 'ACCESS_TOKEN', -// idToken: 'ID_TOKEN', -// expiresInSeconds: 'EXPIRES_IN', -// scope: undefined, -// }); -// }); -// }); - -// describe('refresh token cookie and scope', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, -// query: { -// scope: 'a,b', -// }, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// it('should fetch and return a new access token with scopes', () => { -// const spyRefresh = jest -// .spyOn(refresh, 'requestNewAccessToken') -// .mockImplementation((_x, _y, _z, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(undefined, 'ACCESS_TOKEN', undefined, { -// expires_in: 'EXPIRES_IN', -// id_token: 'ID_TOKEN', -// scope: 'a,b', -// }); -// }); - -// googleAuthProvider.refresh(mockRequest, mockResponse); -// expect(spyRefresh).toBeCalledTimes(1); -// expect(spyRefresh).toBeCalledWith( -// 'google', -// 'REFRESH_TOKEN', -// { scope: 'a,b' }, -// expect.any(Function), -// ); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith({ -// accessToken: 'ACCESS_TOKEN', -// idToken: 'ID_TOKEN', -// expiresInSeconds: 'EXPIRES_IN', -// scope: 'a,b', -// }); -// }); - -// it('ensures x-requested-with header', () => { -// const mockHeaderRequest = ({ -// header: () => 'TEST', -// } as unknown) as express.Request; - -// googleAuthProvider.refresh(mockHeaderRequest, mockResponse); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith( -// 'Invalid X-Requested-With header', -// ); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// }); -// }); -// }); -// }); diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index a090f7f950..bfa6ac7a81 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -1,151 +1,68 @@ -// /* -// * 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 express from 'express'; +import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, +} from '../PassportStrategyHelper'; +import { + OAuthProviderHandlers, + AuthInfoBase, + AuthInfoPrivate, + RedirectInfo, + AuthProviderConfig, +} from '../types'; -// import passport from 'passport'; -// import express from 'express'; -// import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -// import { -// AuthProvider, -// AuthProviderRouteHandlers, -// AuthProviderConfig, -// } from './../types'; -// import { postMessageResponse, ensuresXRequestedWith } from './../utils'; -// import { InputError } from '@backstage/backend-common'; +export class GoogleAuthProvider implements OAuthProviderHandlers { + private readonly provider: string; + private readonly providerConfig: AuthProviderConfig; + private readonly _strategy: GoogleStrategy; -// export class GoogleAuthProvider -// implements AuthProvider, AuthProviderRouteHandlers { -// private readonly provider: string; -// private readonly providerConfig: AuthProviderConfig; -// private readonly _strategy: GoogleStrategy; + constructor(providerConfig: AuthProviderConfig) { + this.provider = providerConfig.provider; + this.providerConfig = providerConfig; + // TODO: throw error if env variables not set? + this._strategy = new GoogleStrategy( + { ...this.providerConfig.options }, + ( + accessToken: any, + refreshToken: any, + params: any, + profile: any, + done: any, + ) => { + done( + undefined, + { + profile, + idToken: params.id_token, + accessToken, + scope: params.scope, + expiresInSeconds: 10, + }, + { + refreshToken, + }, + ); + }, + ); + } -// constructor(handler: OAuthProviderHandlers) { -// this.provider = providerConfig.provider; -// this.providerConfig = providerConfig; -// // TODO: throw error if env variables not set? -// this._strategy = new GoogleStrategy( -// { ...this.providerConfig.options }, -// ( -// accessToken: any, -// refreshToken: any, -// params: any, -// profile: any, -// done: any, -// ) => { -// done( -// undefined, -// { -// profile, -// idToken: params.id_token, -// accessToken, -// scope: params.scope, -// expiresInSeconds: params.expires_in, -// }, -// { -// refreshToken, -// }, -// ); -// }, -// ); -// } + async start(req: express.Request, options: any): Promise { + return await executeRedirectStrategy(req, this._strategy, options); + } -// async start(req: express.Request, res: express.Response) { -// const scope = req.query.scope?.toString() ?? ''; + async handler( + req: express.Request, + ): Promise<{ user: AuthInfoBase; info: AuthInfoPrivate }> { + return await executeFrameHandlerStrategy(req, this._strategy); + } -// if (!scope) { -// throw new InputError('missing scope parameter'); -// } - -// // router -> [AuthProviderRouteHandlers] -> OAuthProvider -> [OAuthProviderHandler] -> GoogleAuthProvider -// // router -> [AuthProviderRouteHandlers] -> GoogleAuthProvider - -// // class GoogleAuthProvider2 implements OAuthProviderHandler { -// // async start(req: express.Request): Promise { - -// // } -// // async handler(req: express.Request): Promise { -// // const { user, info } = await executeFrameHandlerStrategy( -// // req, -// // this.provider, -// // this._strategy, -// // ); -// // return { user, info } -// // } -// // } - -// executeRedirectStrategy(req, res, this.provider, this._strategy, { -// scope, -// accessType: 'offline', -// prompt: 'consent', -// }); -// } - -// async frameHandler(req: express.Request, res: express.Response) { -// try { -// // const { user, info } = await this.handler.handler(req); -// const { user, info } = await executeFrameHandlerStrategy(req); - -// const { refreshToken } = info; -// if (!refreshToken) { -// throw new Error('Missing refresh token'); -// } - -// setRefreshTokenCookie(res, this.provider, refreshToken); - -// return postMessageResponse(res, { -// type: 'auth-result', -// payload: user, -// }); -// } catch (error) { -// return postMessageResponse(res, { -// type: 'auth-result', -// error: { -// name: error.name, -// message: error.message, -// }, -// }); -// } -// } - -// async logout(req: express.Request, res: express.Response) { -// if (!ensuresXRequestedWith(req)) { -// return res.status(401).send('Invalid X-Requested-With header'); -// } - -// removeRefreshTokenCookie(res, this.provider); -// return res.send('logout!'); -// } - -// async refresh(req: express.Request, res: express.Response) { -// if (!ensuresXRequestedWith(req)) { -// return res.status(401).send('Invalid X-Requested-With header'); -// } - -// try { -// const refreshInfo = await executeRefreshTokenStrategy( -// req, -// this.provider, -// this._strategy, -// ); -// res.send(refreshInfo); -// } catch (error) { -// res.status(401).send(`${error.message}`); -// } -// } - -// strategy(): passport.Strategy { -// return this._strategy; -// } -// } + async refresh(refreshToken: string, scope: string): Promise { + return await executeRefreshTokenStrategy( + this._strategy, + refreshToken, + scope, + ); + } +} diff --git a/plugins/auth-backend/src/providers/index.test.ts b/plugins/auth-backend/src/providers/index.test.ts deleted file mode 100644 index 74f7657c44..0000000000 --- a/plugins/auth-backend/src/providers/index.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -// /* -// * 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 passport from 'passport'; -// import express from 'express'; -// import { makeProvider, defaultRouter } from '.'; -// import { AuthProviderRouteHandlers, AuthProviderConfig } from './types'; -// import * as passportGoogleOAuth20 from 'passport-google-oauth20'; -// import { ProviderFactories } from './factories'; - -// class MyOAuthProvider implements AuthProviderHandlers {} - -// class MyAuthProvider implements AuthProviderRouteHandlers { -// // private readonly providerConfig: AuthProviderConfig; -// constructor(providerConfig: AuthProviderConfig) { -// this.providerConfig = providerConfig; -// } - -// strategy(): passport.Strategy { -// return new passportGoogleOAuth20.Strategy( -// this.providerConfig.options, -// () => {}, -// ); -// } -// async start(_: express.Request, res: express.Response): Promise { -// res.send('start'); -// } -// async frameHandler(_: express.Request, res: express.Response): Promise { -// res.send('frameHandler'); -// } -// async logout(_: express.Request, res: express.Response): Promise { -// res.send('logout'); -// } -// } - -// class MyAuthProviderWithRefresh extends MyAuthProvider { -// async refresh(_: express.Request, res: express.Response): Promise { -// res.send('logout'); -// } -// } - -// const providerConfig = { -// provider: 'a', -// options: { -// clientID: 'somevalue', -// }, -// }; - -// const providerConfigInvalid = { -// provider: 'b', -// options: { -// clientID: 'somevalue', -// }, -// }; - -// describe('makeProvider', () => { -// it('makes a provider for Myauthprovider', () => { -// jest -// .spyOn(ProviderFactories, 'getProviderFactory') -// .mockReturnValueOnce(MyAuthProvider); -// const provider = makeProvider(providerConfig); -// expect(provider.providerId).toEqual('a'); -// expect(provider.providerRouter).toBeDefined(); -// }); - -// it('throws an error when provider implementation does not exist', () => { -// expect(() => { -// makeProvider(providerConfigInvalid); -// }).toThrow('Provider Implementation missing for : b auth provider'); -// }); -// }); - -// describe('defaultRouter', () => { -// it('make router for auth provider without refresh', () => { -// expect( -// defaultRouter(new MyAuthProvider({ provider: 'a', options: {} })), -// ).toBeDefined(); -// }); - -// it('make router for auth provider with refresh', () => { -// expect( -// defaultRouter( -// new MyAuthProviderWithRefresh({ provider: 'b', options: {} }), -// ), -// ).toBeDefined(); -// }); -// }); diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index a9c43716e2..6c4bf82481 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -26,34 +26,14 @@ export interface OAuthProviderHandlers { start(req: express.Request, options: any): Promise; handler(req: express.Request): Promise; refresh(refreshToken: string, scope: string): Promise; - logout( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; + logout?(): Promise; } export interface AuthProviderRouteHandlers { - start( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; - frameHandler( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; - refresh?( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; - logout( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; + start(req: express.Request, res: express.Response): Promise; + frameHandler(req: express.Request, res: express.Response): Promise; + refresh?(req: express.Request, res: express.Response): Promise; + logout(req: express.Request, res: express.Response): Promise; } export type AuthProviderFactories = {