From 53a947bd5a7fcb6c91da5c15cec51efc8f312b93 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 2 Sep 2020 17:24:41 +0200 Subject: [PATCH 1/8] auth-backend: split OAuthProvider into lib/oauth and lib/flow --- .../src/lib/flow/authFlowHelpers.test.ts | 103 ++++++++++++ .../src/lib/flow/authFlowHelpers.ts | 56 +++++++ plugins/auth-backend/src/lib/flow/index.ts | 17 ++ .../src/lib/{ => oauth}/OAuthProvider.test.ts | 148 +----------------- .../src/lib/{ => oauth}/OAuthProvider.ts | 100 +----------- .../src/lib/oauth/helpers.test.ts | 77 +++++++++ plugins/auth-backend/src/lib/oauth/helpers.ts | 60 +++++++ plugins/auth-backend/src/lib/oauth/index.ts | 17 ++ .../src/providers/auth0/provider.ts | 2 +- .../src/providers/github/provider.ts | 2 +- .../src/providers/gitlab/provider.ts | 2 +- .../src/providers/google/provider.ts | 2 +- .../src/providers/microsoft/provider.ts | 2 +- .../src/providers/oauth2/provider.ts | 2 +- .../src/providers/okta/provider.ts | 2 +- .../src/providers/saml/provider.ts | 2 +- 16 files changed, 344 insertions(+), 250 deletions(-) create mode 100644 plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts create mode 100644 plugins/auth-backend/src/lib/flow/authFlowHelpers.ts create mode 100644 plugins/auth-backend/src/lib/flow/index.ts rename plugins/auth-backend/src/lib/{ => oauth}/OAuthProvider.test.ts (60%) rename plugins/auth-backend/src/lib/{ => oauth}/OAuthProvider.ts (75%) create mode 100644 plugins/auth-backend/src/lib/oauth/helpers.test.ts create mode 100644 plugins/auth-backend/src/lib/oauth/helpers.ts create mode 100644 plugins/auth-backend/src/lib/oauth/index.ts diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts new file mode 100644 index 0000000000..10e99c0fe1 --- /dev/null +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts @@ -0,0 +1,103 @@ +/* + * 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 { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers'; +import { WebMessageResponse } from '../../providers/types'; + +describe('OAuthProvider Utils', () => { + 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: { + id: 'a', + idToken: 'a.b.c', + }, + }, + }; + const jsonData = JSON.stringify(data); + const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); + + postMessageResponse(mockResponse, appOrigin, data); + expect(mockResponse.setHeader).toBeCalledTimes(3); + expect(mockResponse.end).toBeCalledTimes(1); + expect(mockResponse.end).toBeCalledWith( + expect.stringContaining(base64Data), + ); + }); + + 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 occured'), + }; + const jsonData = JSON.stringify(data); + const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); + + postMessageResponse(mockResponse, appOrigin, data); + expect(mockResponse.setHeader).toBeCalledTimes(3); + expect(mockResponse.end).toBeCalledTimes(1); + expect(mockResponse.end).toBeCalledWith( + expect.stringContaining(base64Data), + ); + }); + }); + + 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 new file mode 100644 index 0000000000..500a6cec8e --- /dev/null +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts @@ -0,0 +1,56 @@ +/* + * 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 crypto from 'crypto'; +import { WebMessageResponse } from '../../providers/types'; + +export const postMessageResponse = ( + res: express.Response, + appOrigin: string, + response: WebMessageResponse, +) => { + const jsonData = JSON.stringify(response); + const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); + + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-Frame-Options', 'sameorigin'); + + // TODO: Make target app origin configurable globally + const script = ` + (window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), '${appOrigin}') + window.close() + `; + const hash = crypto.createHash('sha256').update(script).digest('base64'); + res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`); + + res.end(` + + + + + + `); +}; + +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 new file mode 100644 index 0000000000..a5f2f7a3ac --- /dev/null +++ b/plugins/auth-backend/src/lib/flow/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers'; diff --git a/plugins/auth-backend/src/lib/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts similarity index 60% rename from plugins/auth-backend/src/lib/OAuthProvider.test.ts rename to plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts index 06bbd15c51..124ace1013 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts @@ -16,15 +16,12 @@ import express from 'express'; import { - ensuresXRequestedWith, - postMessageResponse, THOUSAND_DAYS_MS, TEN_MINUTES_MS, - verifyNonce, - encodeState, OAuthProvider, } from './OAuthProvider'; -import { WebMessageResponse, OAuthProviderHandlers } from '../providers/types'; +import { OAuthProviderHandlers } from '../../providers/types'; +import { encodeState } from './helpers'; const mockResponseData = { providerInfo: { @@ -41,147 +38,6 @@ const mockResponseData = { }, }; -describe('OAuthProvider Utils', () => { - describe('verifyNonce', () => { - it('should throw error if cookie nonce missing', () => { - const state = { nonce: 'NONCE', env: 'development' }; - const mockRequest = ({ - cookies: {}, - query: { - state: encodeState(state), - }, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).toThrowError('Auth response is missing cookie nonce'); - }); - - it('should throw error if state nonce missing', () => { - const mockRequest = ({ - cookies: { - 'providera-nonce': 'NONCE', - }, - query: {}, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).toThrowError('Invalid state passed via request'); - }); - - it('should throw error if nonce mismatch', () => { - const state = { nonce: 'NONCEB', env: 'development' }; - const mockRequest = ({ - cookies: { - 'providera-nonce': 'NONCEA', - }, - query: { - state: encodeState(state), - }, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).toThrowError('Invalid nonce'); - }); - - it('should not throw any error if nonce matches', () => { - const state = { nonce: 'NONCE', env: 'development' }; - const mockRequest = ({ - cookies: { - 'providera-nonce': 'NONCE', - }, - query: { - state: encodeState(state), - }, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).not.toThrow(); - }); - }); - - 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: { - id: 'a', - idToken: 'a.b.c', - }, - }, - }; - const jsonData = JSON.stringify(data); - const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); - - postMessageResponse(mockResponse, appOrigin, data); - expect(mockResponse.setHeader).toBeCalledTimes(3); - expect(mockResponse.end).toBeCalledTimes(1); - expect(mockResponse.end).toBeCalledWith( - expect.stringContaining(base64Data), - ); - }); - - 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 occured'), - }; - const jsonData = JSON.stringify(data); - const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); - - postMessageResponse(mockResponse, appOrigin, data); - expect(mockResponse.setHeader).toBeCalledTimes(3); - expect(mockResponse.end).toBeCalledTimes(1); - expect(mockResponse.end).toBeCalledWith( - expect.stringContaining(base64Data), - ); - }); - }); - - 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); - }); - }); -}); - describe('OAuthProvider', () => { class MyAuthProvider implements OAuthProviderHandlers { async start() { diff --git a/plugins/auth-backend/src/lib/OAuthProvider.ts b/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts similarity index 75% rename from plugins/auth-backend/src/lib/OAuthProvider.ts rename to plugins/auth-backend/src/lib/oauth/OAuthProvider.ts index dac31b442d..95948ff7cc 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts @@ -20,13 +20,13 @@ import { URL } from 'url'; import { AuthProviderRouteHandlers, OAuthProviderHandlers, - WebMessageResponse, BackstageIdentity, - OAuthState, AuthProviderConfig, -} from '../providers/types'; +} from '../../providers/types'; import { InputError } from '@backstage/backend-common'; -import { TokenIssuer } from '../identity'; +import { TokenIssuer } from '../../identity'; +import { verifyNonce, encodeState } from './helpers'; +import { postMessageResponse, ensuresXRequestedWith } from '../flow'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -42,85 +42,6 @@ export type Options = { tokenIssuer: TokenIssuer; }; -const readState = (stateString: string): OAuthState => { - const state = Object.fromEntries( - new URLSearchParams(decodeURIComponent(stateString)), - ); - if ( - !state.nonce || - !state.env || - state.nonce?.length === 0 || - state.env?.length === 0 - ) { - throw Error(`Invalid state passed via request`); - } - return { - nonce: state.nonce, - env: state.env, - }; -}; - -export const encodeState = (state: OAuthState): string => { - const searchParams = new URLSearchParams(); - searchParams.append('nonce', state.nonce); - searchParams.append('env', state.env); - - return encodeURIComponent(searchParams.toString()); -}; - -export const verifyNonce = (req: express.Request, providerId: string) => { - const cookieNonce = req.cookies[`${providerId}-nonce`]; - const state: OAuthState = readState(req.query.state?.toString() ?? ''); - const stateNonce = state.nonce; - - if (!cookieNonce) { - throw new Error('Auth response is missing cookie nonce'); - } - if (stateNonce.length === 0) { - throw new Error('Auth response is missing state nonce'); - } - if (cookieNonce !== stateNonce) { - throw new Error('Invalid nonce'); - } -}; - -export const postMessageResponse = ( - res: express.Response, - appOrigin: string, - response: WebMessageResponse, -) => { - const jsonData = JSON.stringify(response); - const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); - - res.setHeader('Content-Type', 'text/html'); - res.setHeader('X-Frame-Options', 'sameorigin'); - - // TODO: Make target app origin configurable globally - const script = ` - (window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), '${appOrigin}') - window.close() - `; - const hash = crypto.createHash('sha256').update(script).digest('base64'); - res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`); - - res.end(` - - - - - - `); -}; - -export const ensuresXRequestedWith = (req: express.Request) => { - const requiredHeader = req.header('X-Requested-With'); - - if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') { - return false; - } - return true; -}; - export class OAuthProvider implements AuthProviderRouteHandlers { static fromConfig( config: AuthProviderConfig, @@ -287,19 +208,6 @@ export class OAuthProvider implements AuthProviderRouteHandlers { } } - identifyEnv(req: express.Request): string | undefined { - const reqEnv = req.query.env?.toString(); - if (reqEnv) { - return reqEnv; - } - const stateParams = req.query.state?.toString(); - if (!stateParams) { - return undefined; - } - const env = readState(stateParams).env; - return env; - } - /** * If the response from the OAuth provider includes a Backstage identity, we * make sure it's populated with all the information we can derive from the user ID. diff --git a/plugins/auth-backend/src/lib/oauth/helpers.test.ts b/plugins/auth-backend/src/lib/oauth/helpers.test.ts new file mode 100644 index 0000000000..fcf56705d2 --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/helpers.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { verifyNonce, encodeState } from './helpers'; + +describe('OAuthProvider Utils', () => { + describe('verifyNonce', () => { + it('should throw error if cookie nonce missing', () => { + const state = { nonce: 'NONCE', env: 'development' }; + const mockRequest = ({ + cookies: {}, + query: { + state: encodeState(state), + }, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).toThrowError('Auth response is missing cookie nonce'); + }); + + it('should throw error if state nonce missing', () => { + const mockRequest = ({ + cookies: { + 'providera-nonce': 'NONCE', + }, + query: {}, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).toThrowError('Invalid state passed via request'); + }); + + it('should throw error if nonce mismatch', () => { + const state = { nonce: 'NONCEB', env: 'development' }; + const mockRequest = ({ + cookies: { + 'providera-nonce': 'NONCEA', + }, + query: { + state: encodeState(state), + }, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).toThrowError('Invalid nonce'); + }); + + it('should not throw any error if nonce matches', () => { + const state = { nonce: 'NONCE', env: 'development' }; + const mockRequest = ({ + cookies: { + 'providera-nonce': 'NONCE', + }, + query: { + state: encodeState(state), + }, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).not.toThrow(); + }); + }); +}); diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts new file mode 100644 index 0000000000..34f12b8d22 --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -0,0 +1,60 @@ +/* + * 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 { OAuthState } from '../../providers/types'; + +const readState = (stateString: string): OAuthState => { + const state = Object.fromEntries( + new URLSearchParams(decodeURIComponent(stateString)), + ); + if ( + !state.nonce || + !state.env || + state.nonce?.length === 0 || + state.env?.length === 0 + ) { + throw Error(`Invalid state passed via request`); + } + return { + nonce: state.nonce, + env: state.env, + }; +}; + +export const encodeState = (state: OAuthState): string => { + const searchParams = new URLSearchParams(); + searchParams.append('nonce', state.nonce); + searchParams.append('env', state.env); + + return encodeURIComponent(searchParams.toString()); +}; + +export const verifyNonce = (req: express.Request, providerId: string) => { + const cookieNonce = req.cookies[`${providerId}-nonce`]; + const state: OAuthState = readState(req.query.state?.toString() ?? ''); + const stateNonce = state.nonce; + + if (!cookieNonce) { + throw new Error('Auth response is missing cookie nonce'); + } + if (stateNonce.length === 0) { + throw new Error('Auth response is missing state nonce'); + } + if (cookieNonce !== stateNonce) { + throw new Error('Invalid nonce'); + } +}; diff --git a/plugins/auth-backend/src/lib/oauth/index.ts b/plugins/auth-backend/src/lib/oauth/index.ts new file mode 100644 index 0000000000..4c120799e7 --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { OAuthProvider } from './OAuthProvider'; diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 7eeca59028..f785d1e147 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -19,7 +19,7 @@ import passport from 'passport'; import Auth0Strategy from './strategy'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, executeFrameHandlerStrategy, diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index b63acdfb20..8e9bf1aa4a 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -29,7 +29,7 @@ import { OAuthResponse, PassportDoneCallback, } from '../types'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import passport from 'passport'; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 1c2a822d82..2100bc753e 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -29,7 +29,7 @@ import { OAuthResponse, PassportDoneCallback, } from '../types'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import passport from 'passport'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index d4455ac4d3..366f9cd96c 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -31,7 +31,7 @@ import { OAuthResponse, PassportDoneCallback, } from '../types'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import passport from 'passport'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 5997d8e1a8..fb96d2236f 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -35,7 +35,7 @@ import { PassportDoneCallback, } from '../types'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import { Config } from '@backstage/config'; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 4b8a826d81..c08e18960c 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -19,7 +19,7 @@ import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, executeFrameHandlerStrategy, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index db8e0cf313..a6ccc25d21 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import express from 'express'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { OAuthProvider } from '../../lib/oauth'; import { Strategy as OktaStrategy } from 'passport-okta-oauth'; import passport from 'passport'; import { diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 56793d85f4..d98bcc9579 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -30,7 +30,7 @@ import { PassportDoneCallback, ProfileInfo, } from '../types'; -import { postMessageResponse } from '../../lib/OAuthProvider'; +import { postMessageResponse } from '../../lib/flow'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import { Config } from '@backstage/config'; From b8a3f851cdb111e291611eb1c8f95abcd325fd8b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 09:25:44 +0200 Subject: [PATCH 2/8] auth-backend: cleanup types and move them closer to home --- .../src/lib/EnvironmentHandler.ts | 9 +- .../src/lib/PassportStrategyHelper.ts | 40 ++++- .../src/lib/flow/authFlowHelpers.test.ts | 2 +- .../src/lib/flow/authFlowHelpers.ts | 2 +- plugins/auth-backend/src/lib/flow/types.ts | 31 ++++ .../src/lib/oauth/OAuthProvider.test.ts | 2 +- .../src/lib/oauth/OAuthProvider.ts | 2 +- plugins/auth-backend/src/lib/oauth/helpers.ts | 4 +- plugins/auth-backend/src/lib/oauth/index.ts | 8 + plugins/auth-backend/src/lib/oauth/types.ts | 112 +++++++++++++ .../src/providers/auth0/provider.ts | 16 +- .../auth-backend/src/providers/factories.ts | 7 +- .../src/providers/github/provider.ts | 18 +- .../src/providers/gitlab/provider.ts | 18 +- .../src/providers/google/provider.ts | 14 +- .../src/providers/microsoft/provider.ts | 18 +- .../src/providers/oauth2/provider.ts | 16 +- .../src/providers/okta/provider.ts | 18 +- .../src/providers/saml/provider.ts | 2 +- plugins/auth-backend/src/providers/types.ts | 157 ------------------ 20 files changed, 258 insertions(+), 238 deletions(-) create mode 100644 plugins/auth-backend/src/lib/flow/types.ts create mode 100644 plugins/auth-backend/src/lib/oauth/types.ts diff --git a/plugins/auth-backend/src/lib/EnvironmentHandler.ts b/plugins/auth-backend/src/lib/EnvironmentHandler.ts index 53e56a7420..dd9c972cb2 100644 --- a/plugins/auth-backend/src/lib/EnvironmentHandler.ts +++ b/plugins/auth-backend/src/lib/EnvironmentHandler.ts @@ -15,16 +15,17 @@ */ import express from 'express'; -import { - AuthProviderRouteHandlers, - EnvironmentIdentifierFn, -} from '../providers/types'; +import { AuthProviderRouteHandlers } from '../providers/types'; import { InputError } from '@backstage/backend-common'; export type EnvironmentHandlers = { [key: string]: AuthProviderRouteHandlers; }; +export type EnvironmentIdentifierFn = ( + req: express.Request, +) => string | undefined; + export class EnvironmentHandler implements AuthProviderRouteHandlers { constructor( private readonly providerId: string, diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 9a169f9696..3b8d0801e9 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -17,12 +17,13 @@ import express from 'express'; import passport from 'passport'; import jwtDecoder from 'jwt-decode'; -import { - RedirectInfo, - RefreshTokenResponse, - ProfileInfo, - ProviderStrategy, -} from '../providers/types'; +import { ProfileInfo } from '../providers/types'; + +export type PassportDoneCallback = ( + err?: Error, + response?: Res, + privateInfo?: Private, +) => void; export const makeProfileInfo = ( profile: passport.Profile, @@ -63,6 +64,17 @@ export const makeProfileInfo = ( }; }; +export type RedirectInfo = { + /** + * URL to redirect to + */ + url: string; + /** + * Status code to use for the redirect + */ + status?: number; +}; + export const executeRedirectStrategy = async ( req: express.Request, providerStrategy: passport.Strategy, @@ -106,6 +118,18 @@ export const executeFrameHandlerStrategy = async ( ); }; +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, @@ -156,6 +180,10 @@ export const executeRefreshTokenStrategy = async ( }); }; +type ProviderStrategy = { + userProfile(accessToken: string, callback: Function): void; +}; + export const executeFetchUserProfileStrategy = async ( providerStrategy: passport.Strategy, accessToken: string, diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts index 10e99c0fe1..b525e98e12 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts @@ -16,7 +16,7 @@ import express from 'express'; import { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers'; -import { WebMessageResponse } from '../../providers/types'; +import { WebMessageResponse } from './types'; describe('OAuthProvider Utils', () => { describe('postMessageResponse', () => { diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts index 500a6cec8e..63d7c28b50 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts @@ -16,7 +16,7 @@ import express from 'express'; import crypto from 'crypto'; -import { WebMessageResponse } from '../../providers/types'; +import { WebMessageResponse } from './types'; export const postMessageResponse = ( res: express.Response, diff --git a/plugins/auth-backend/src/lib/flow/types.ts b/plugins/auth-backend/src/lib/flow/types.ts new file mode 100644 index 0000000000..98bb551c2c --- /dev/null +++ b/plugins/auth-backend/src/lib/flow/types.ts @@ -0,0 +1,31 @@ +/* + * 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 { AuthResponse } from '../../providers/types'; + +/** + * 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 WebMessageResponse = + | { + type: 'authorization_response'; + response: AuthResponse; + } + | { + type: 'authorization_response'; + error: Error; + }; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts index 124ace1013..ce78733211 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts @@ -20,8 +20,8 @@ import { TEN_MINUTES_MS, OAuthProvider, } from './OAuthProvider'; -import { OAuthProviderHandlers } from '../../providers/types'; import { encodeState } from './helpers'; +import { OAuthProviderHandlers } from './types'; const mockResponseData = { providerInfo: { diff --git a/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts b/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts index 95948ff7cc..1fe62845a0 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts @@ -19,7 +19,6 @@ import crypto from 'crypto'; import { URL } from 'url'; import { AuthProviderRouteHandlers, - OAuthProviderHandlers, BackstageIdentity, AuthProviderConfig, } from '../../providers/types'; @@ -27,6 +26,7 @@ import { InputError } from '@backstage/backend-common'; import { TokenIssuer } from '../../identity'; import { verifyNonce, encodeState } from './helpers'; import { postMessageResponse, ensuresXRequestedWith } from '../flow'; +import { OAuthProviderHandlers } from './types'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts index 34f12b8d22..9f250a0285 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -15,9 +15,9 @@ */ import express from 'express'; -import { OAuthState } from '../../providers/types'; +import { OAuthState } from './types'; -const readState = (stateString: string): OAuthState => { +export const readState = (stateString: string): OAuthState => { const state = Object.fromEntries( new URLSearchParams(decodeURIComponent(stateString)), ); diff --git a/plugins/auth-backend/src/lib/oauth/index.ts b/plugins/auth-backend/src/lib/oauth/index.ts index 4c120799e7..eac43141a4 100644 --- a/plugins/auth-backend/src/lib/oauth/index.ts +++ b/plugins/auth-backend/src/lib/oauth/index.ts @@ -14,4 +14,12 @@ * limitations under the License. */ +export { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler'; export { OAuthProvider } from './OAuthProvider'; +export type { + OAuthProviderHandlers, + OAuthProviderInfo, + OAuthProviderOptions, + OAuthResponse, + OAuthState, +} from './types'; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts new file mode 100644 index 0000000000..ecc665831f --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -0,0 +1,112 @@ +/* + * 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 { AuthResponse } from '../../providers/types'; +import { RedirectInfo } from '../PassportStrategyHelper'; + +/** + * Common options for passport.js-based OAuth providers + */ +export type OAuthProviderOptions = { + /** + * Client ID of the auth provider. + */ + clientId: string; + /** + * Client Secret of the auth provider. + */ + clientSecret: string; + /** + * Callback URL to be passed to the auth provider to redirect to after the user signs in. + */ + callbackUrl: string; +}; + +export type OAuthResponse = AuthResponse; + +export type OAuthProviderInfo = { + /** + * An access token issued for the signed in user. + */ + accessToken: string; + /** + * (Optional) Id token issued for the signed in user. + */ + idToken?: string; + /** + * Expiry of the access token in seconds. + */ + expiresInSeconds?: number; + /** + * Scopes granted for the access token. + */ + scope: string; + /** + * A refresh token issued for the signed in user + */ + refreshToken?: string; +}; + +export type OAuthState = { + /* A type for the serialized value in the `state` parameter of the OAuth authorization flow + */ + nonce: string; + env: string; +}; + +/** + * Any OAuth provider needs to implement this interface which has provider specific + * handlers for different methods to perform authentication, get access tokens, + * refresh tokens and perform sign out. + */ +export interface OAuthProviderHandlers { + /** + * This method initiates a sign in request with an auth provider. + * @param {express.Request} req + * @param options + */ + 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<{ + response: AuthResponse; + 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>; + + /** + * (Optional) Sign out of the auth provider. + */ + logout?(): Promise; +} diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index f785d1e147..ff55846302 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -19,22 +19,22 @@ import passport from 'passport'; import Auth0Strategy from './strategy'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; -import { OAuthProvider } from '../../lib/oauth'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { executeFetchUserProfileStrategy, executeFrameHandlerStrategy, executeRedirectStrategy, executeRefreshTokenStrategy, makeProfileInfo, -} from '../../lib/PassportStrategyHelper'; -import { - AuthProviderConfig, - OAuthProviderHandlers, - OAuthResponse, PassportDoneCallback, RedirectInfo, - OAuthProviderOptions, -} from '../types'; +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; import { Config } from '@backstage/config'; type PrivateInfo = { diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 26989d8759..2b67510991 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -25,15 +25,12 @@ import { createOktaProvider } from './okta'; import { createSamlProvider } from './saml'; import { createAuth0Provider } from './auth0'; import { createMicrosoftProvider } from './microsoft'; -import { - AuthProviderConfig, - AuthProviderFactory, - EnvironmentIdentifierFn, -} from './types'; +import { AuthProviderConfig, AuthProviderFactory } from './types'; import { Config } from '@backstage/config'; import { EnvironmentHandlers, EnvironmentHandler, + EnvironmentIdentifierFn, } from '../lib/EnvironmentHandler'; const factories: { [providerId: string]: AuthProviderFactory } = { diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 8e9bf1aa4a..22fb908ef9 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -20,16 +20,16 @@ import { executeFrameHandlerStrategy, executeRedirectStrategy, makeProfileInfo, -} from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, - AuthProviderConfig, - RedirectInfo, - OAuthProviderOptions, - OAuthResponse, PassportDoneCallback, -} from '../types'; -import { OAuthProvider } from '../../lib/oauth'; + RedirectInfo, +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import passport from 'passport'; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 2100bc753e..a3bd6deef3 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -20,16 +20,16 @@ import { executeFrameHandlerStrategy, executeRedirectStrategy, makeProfileInfo, -} from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, - AuthProviderConfig, - RedirectInfo, - OAuthProviderOptions, - OAuthResponse, PassportDoneCallback, -} from '../types'; -import { OAuthProvider } from '../../lib/oauth'; + RedirectInfo, +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import passport from 'passport'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 366f9cd96c..0301062474 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -22,16 +22,16 @@ import { executeRefreshTokenStrategy, makeProfileInfo, executeFetchUserProfileStrategy, -} from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, + PassportDoneCallback, RedirectInfo, - AuthProviderConfig, +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; +import { + OAuthProvider, + OAuthProviderHandlers, OAuthProviderOptions, OAuthResponse, - PassportDoneCallback, -} from '../types'; -import { OAuthProvider } from '../../lib/oauth'; +} from '../../lib/oauth'; import passport from 'passport'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index fb96d2236f..5eec047b69 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -24,18 +24,18 @@ import { executeRefreshTokenStrategy, makeProfileInfo, executeFetchUserProfileStrategy, + PassportDoneCallback, + RedirectInfo, } from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, - RedirectInfo, - AuthProviderConfig, - OAuthProviderOptions, - OAuthResponse, - PassportDoneCallback, -} from '../types'; +import { AuthProviderConfig } from '../types'; -import { OAuthProvider } from '../../lib/oauth'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import { Config } from '@backstage/config'; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index c08e18960c..a1a3cc7d97 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -19,22 +19,22 @@ import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; -import { OAuthProvider } from '../../lib/oauth'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { executeFetchUserProfileStrategy, executeFrameHandlerStrategy, executeRedirectStrategy, executeRefreshTokenStrategy, makeProfileInfo, -} from '../../lib/PassportStrategyHelper'; -import { - AuthProviderConfig, - OAuthProviderOptions, - OAuthProviderHandlers, - OAuthResponse, PassportDoneCallback, RedirectInfo, -} from '../types'; +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; import { Config } from '@backstage/config'; type PrivateInfo = { diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index a6ccc25d21..e46e396aa5 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -14,7 +14,12 @@ * limitations under the License. */ import express from 'express'; -import { OAuthProvider } from '../../lib/oauth'; +import { + OAuthProvider, + OAuthProviderOptions, + OAuthProviderHandlers, + OAuthResponse, +} from '../../lib/oauth'; import { Strategy as OktaStrategy } from 'passport-okta-oauth'; import passport from 'passport'; import { @@ -23,15 +28,10 @@ import { executeRefreshTokenStrategy, makeProfileInfo, executeFetchUserProfileStrategy, -} from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, - RedirectInfo, - AuthProviderConfig, - OAuthProviderOptions, - OAuthResponse, PassportDoneCallback, -} from '../types'; + RedirectInfo, +} from '../../lib/PassportStrategyHelper'; +import { AuthProviderConfig } from '../types'; import { Logger } from 'winston'; import { StateStore } from 'passport-oauth2'; import { TokenIssuer } from '../../identity'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index d98bcc9579..d6ea0520fc 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -23,11 +23,11 @@ import { import { executeFrameHandlerStrategy, executeRedirectStrategy, + PassportDoneCallback, } from '../../lib/PassportStrategyHelper'; import { AuthProviderConfig, AuthProviderRouteHandlers, - PassportDoneCallback, ProfileInfo, } from '../types'; import { postMessageResponse } from '../../lib/flow'; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index b3b7e518cc..c7b70adbdc 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -19,21 +19,6 @@ import { Logger } from 'winston'; import { TokenIssuer } from '../identity'; import { Config } from '@backstage/config'; -export type OAuthProviderOptions = { - /** - * Client ID of the auth provider. - */ - clientId: string; - /** - * Client Secret of the auth provider. - */ - clientSecret: string; - /** - * Callback URL to be passed to the auth provider to redirect to after the user signs in. - */ - callbackUrl: string; -}; - export type AuthProviderConfig = { /** * The protocol://domain[:port] where the app is hosted. This is used to construct the @@ -47,49 +32,6 @@ export type AuthProviderConfig = { appUrl: string; }; -/** - * Any OAuth provider needs to implement this interface which has provider specific - * handlers for different methods to perform authentication, get access tokens, - * refresh tokens and perform sign out. - */ -export interface OAuthProviderHandlers { - /** - * This method initiates a sign in request with an auth provider. - * @param {express.Request} req - * @param options - */ - 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<{ - response: AuthResponse; - 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>; - - /** - * (Optional) Sign out of the auth provider. - */ - logout?(): Promise; -} - /** * Any Auth provider needs to implement this interface which handles the routes in the * auth backend. Any auth API requests from the frontend reaches these methods. @@ -180,8 +122,6 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentity; }; -export type OAuthResponse = AuthResponse; - export type BackstageIdentity = { /** * The backstage user ID. @@ -194,67 +134,6 @@ export type BackstageIdentity = { idToken?: string; }; -export type OAuthProviderInfo = { - /** - * An access token issued for the signed in user. - */ - accessToken: string; - /** - * (Optional) Id token issued for the signed in user. - */ - idToken?: string; - /** - * Expiry of the access token in seconds. - */ - expiresInSeconds?: number; - /** - * Scopes granted for the access token. - */ - scope: string; - /** - * A refresh token issued for the signed in user - */ - refreshToken?: string; -}; - -export type OAuthPrivateInfo = { - /** - * A refresh token issued for the signed in user. - */ - refreshToken: string; -}; - -/** - * 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 WebMessageResponse = - | { - type: 'authorization_response'; - response: AuthResponse; - } - | { - type: 'authorization_response'; - error: Error; - }; - -export type PassportDoneCallback = ( - err?: Error, - response?: Res, - privateInfo?: Private, -) => void; - -export type RedirectInfo = { - /** - * URL to redirect to - */ - url: string; - /** - * Status code to use for the redirect - */ - status?: number; -}; - /** * Used to display login information to user, i.e. sidebar popup. * @@ -276,39 +155,3 @@ export type ProfileInfo = { */ picture?: string; }; - -export 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 type ProviderStrategy = { - userProfile(accessToken: string, callback: Function): void; -}; - -export type SAMLProviderConfig = { - entryPoint: string; - issuer: string; -}; - -export type SAMLEnvironmentProviderConfig = { - [key: string]: SAMLProviderConfig; -}; - -export type OAuthState = { - /* A type for the serialized value in the `state` parameter of the OAuth authorization flow - */ - nonce: string; - env: string; -}; - -export type EnvironmentIdentifierFn = ( - req: express.Request, -) => string | undefined; From 88cf6da18d2f793d891c78df3ae5809548cf8536 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 12:02:58 +0200 Subject: [PATCH 3/8] auth-backend: move passport helpers into separate lib --- plugins/auth-backend/src/lib/oauth/types.ts | 3 +-- .../PassportStrategyHelper.test.ts | 0 .../{ => passport}/PassportStrategyHelper.ts | 13 +--------- .../auth-backend/src/lib/passport/index.ts | 24 +++++++++++++++++++ .../src/providers/auth0/provider.ts | 5 ++-- .../src/providers/github/provider.ts | 5 ++-- .../src/providers/gitlab/provider.ts | 5 ++-- .../src/providers/google/provider.ts | 5 ++-- .../src/providers/microsoft/provider.ts | 5 ++-- .../src/providers/oauth2/provider.ts | 5 ++-- .../src/providers/okta/provider.ts | 5 ++-- .../src/providers/saml/provider.ts | 2 +- plugins/auth-backend/src/providers/types.ts | 11 +++++++++ 13 files changed, 52 insertions(+), 36 deletions(-) rename plugins/auth-backend/src/lib/{ => passport}/PassportStrategyHelper.test.ts (100%) rename plugins/auth-backend/src/lib/{ => passport}/PassportStrategyHelper.ts (96%) create mode 100644 plugins/auth-backend/src/lib/passport/index.ts diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index ecc665831f..440b89ed6f 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -15,8 +15,7 @@ */ import express from 'express'; -import { AuthResponse } from '../../providers/types'; -import { RedirectInfo } from '../PassportStrategyHelper'; +import { AuthResponse, RedirectInfo } from '../../providers/types'; /** * Common options for passport.js-based OAuth providers diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts similarity index 100% rename from plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts rename to plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts similarity index 96% rename from plugins/auth-backend/src/lib/PassportStrategyHelper.ts rename to plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts index 3b8d0801e9..7bc34186f4 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts @@ -17,7 +17,7 @@ import express from 'express'; import passport from 'passport'; import jwtDecoder from 'jwt-decode'; -import { ProfileInfo } from '../providers/types'; +import { ProfileInfo, RedirectInfo } from '../../providers/types'; export type PassportDoneCallback = ( err?: Error, @@ -64,17 +64,6 @@ export const makeProfileInfo = ( }; }; -export type RedirectInfo = { - /** - * URL to redirect to - */ - url: string; - /** - * Status code to use for the redirect - */ - status?: number; -}; - export const executeRedirectStrategy = async ( req: express.Request, providerStrategy: passport.Strategy, diff --git a/plugins/auth-backend/src/lib/passport/index.ts b/plugins/auth-backend/src/lib/passport/index.ts new file mode 100644 index 0000000000..c307e212fa --- /dev/null +++ b/plugins/auth-backend/src/lib/passport/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +export { + executeFetchUserProfileStrategy, + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, + makeProfileInfo, +} from './PassportStrategyHelper'; +export type { PassportDoneCallback } from './PassportStrategyHelper'; diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index ff55846302..19f8d5d803 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -32,9 +32,8 @@ import { executeRefreshTokenStrategy, makeProfileInfo, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { Config } from '@backstage/config'; type PrivateInfo = { diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 22fb908ef9..68810a5b1b 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -21,9 +21,8 @@ import { executeRedirectStrategy, makeProfileInfo, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { OAuthProvider, OAuthProviderOptions, diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index a3bd6deef3..d076644e46 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -21,9 +21,8 @@ import { executeRedirectStrategy, makeProfileInfo, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { OAuthProvider, OAuthProviderOptions, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 0301062474..21cd76b26f 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -23,9 +23,8 @@ import { makeProfileInfo, executeFetchUserProfileStrategy, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { OAuthProvider, OAuthProviderHandlers, diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 5eec047b69..a7edbb134e 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -25,10 +25,9 @@ import { makeProfileInfo, executeFetchUserProfileStrategy, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; +} from '../../lib/passport'; -import { AuthProviderConfig } from '../types'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { OAuthProvider, diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index a1a3cc7d97..bc50408948 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -32,9 +32,8 @@ import { executeRefreshTokenStrategy, makeProfileInfo, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { Config } from '@backstage/config'; type PrivateInfo = { diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index e46e396aa5..05a692ba40 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -29,9 +29,8 @@ import { makeProfileInfo, executeFetchUserProfileStrategy, PassportDoneCallback, - RedirectInfo, -} from '../../lib/PassportStrategyHelper'; -import { AuthProviderConfig } from '../types'; +} from '../../lib/passport'; +import { AuthProviderConfig, RedirectInfo } from '../types'; import { Logger } from 'winston'; import { StateStore } from 'passport-oauth2'; import { TokenIssuer } from '../../identity'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index d6ea0520fc..82a1cfa1b7 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -24,7 +24,7 @@ import { executeFrameHandlerStrategy, executeRedirectStrategy, PassportDoneCallback, -} from '../../lib/PassportStrategyHelper'; +} from '../../lib/passport'; import { AuthProviderConfig, AuthProviderRouteHandlers, diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index c7b70adbdc..bc80a3d9b5 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -32,6 +32,17 @@ export type AuthProviderConfig = { appUrl: string; }; +export type RedirectInfo = { + /** + * URL to redirect to + */ + url: string; + /** + * Status code to use for the redirect + */ + status?: number; +}; + /** * Any Auth provider needs to implement this interface which handles the routes in the * auth backend. Any auth API requests from the frontend reaches these methods. From fb2d4cf24131911f93066b448529500106b28b8b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 12:39:01 +0200 Subject: [PATCH 4/8] auth-backend: refactor EnvironmentHandler to be OAuth-specific and move env config to be a provider concern --- app-config.yaml | 7 +- .../OAuthEnvironmentHandler.ts} | 89 ++++++++++++------- .../src/providers/auth0/provider.ts | 51 +++++------ .../auth-backend/src/providers/factories.ts | 31 +------ .../src/providers/github/provider.ts | 79 ++++++++-------- .../src/providers/gitlab/provider.ts | 53 ++++++----- .../src/providers/google/provider.ts | 47 +++++----- .../src/providers/microsoft/provider.ts | 59 ++++++------ .../src/providers/oauth2/provider.ts | 55 ++++++------ .../src/providers/okta/provider.ts | 51 +++++------ .../src/providers/saml/provider.ts | 21 ++--- plugins/auth-backend/src/providers/types.ts | 24 ++--- 12 files changed, 265 insertions(+), 302 deletions(-) rename plugins/auth-backend/src/lib/{EnvironmentHandler.ts => oauth/OAuthEnvironmentHandler.ts} (54%) diff --git a/app-config.yaml b/app-config.yaml index 7a8863495e..beb610a926 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -118,10 +118,9 @@ auth: audience: $secret: env: GITLAB_BASE_URL - # saml: - # development: - # entryPoint: "http://localhost:7001/" - # issuer: "passport-saml" + saml: + entryPoint: "http://localhost:7001/" + issuer: "passport-saml" okta: development: clientId: diff --git a/plugins/auth-backend/src/lib/EnvironmentHandler.ts b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts similarity index 54% rename from plugins/auth-backend/src/lib/EnvironmentHandler.ts rename to plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts index dd9c972cb2..d22fc52499 100644 --- a/plugins/auth-backend/src/lib/EnvironmentHandler.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts @@ -15,47 +15,32 @@ */ import express from 'express'; -import { AuthProviderRouteHandlers } from '../providers/types'; +import { Config } from '@backstage/config'; import { InputError } from '@backstage/backend-common'; +import { readState } from './helpers'; +import { AuthProviderRouteHandlers } from '../../providers/types'; -export type EnvironmentHandlers = { - [key: string]: AuthProviderRouteHandlers; -}; +export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { + static mapConfig( + config: Config, + factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers, + ) { + const envs = config.keys(); + const handlers = new Map(); -export type EnvironmentIdentifierFn = ( - req: express.Request, -) => string | undefined; - -export class EnvironmentHandler implements AuthProviderRouteHandlers { - constructor( - private readonly providerId: string, - private readonly providers: EnvironmentHandlers, - private readonly envIdentifier: EnvironmentIdentifierFn, - ) {} - - private getProviderForEnv( - req: express.Request, - res: express.Response, - ): AuthProviderRouteHandlers | undefined { - const env: string | undefined = this.envIdentifier(req); - - if (!env) { - throw new InputError(`Must specify 'env' query to select environment`); + for (const env of envs) { + const envConfig = config.getConfig(env); + const handler = factoryFunc(envConfig); + handlers.set(env, handler); } - if (this.providers.hasOwnProperty(env)) { - return this.providers[env]; - } - - res.status(404).send( - `Missing configuration. -
-
-For this flow to work you need to supply a valid configuration for the "${env}" environment of the "${this.providerId}" provider.`, - ); - return undefined; + return new OAuthEnvironmentHandler(handlers); } + constructor( + private readonly handlers: Map, + ) {} + async start(req: express.Request, res: express.Response): Promise { const provider = this.getProviderForEnv(req, res); await provider?.start(req, res); @@ -78,4 +63,40 @@ For this flow to work you need to supply a valid configuration for the "${env}" const provider = this.getProviderForEnv(req, res); await provider?.logout?.(req, res); } + + private getRequestFromEnv(req: express.Request): string | undefined { + const reqEnv = req.query.env?.toString(); + if (reqEnv) { + return reqEnv; + } + const stateParams = req.query.state?.toString(); + if (!stateParams) { + return undefined; + } + const env = readState(stateParams).env; + return env; + } + + private getProviderForEnv( + req: express.Request, + res: express.Response, + ): AuthProviderRouteHandlers | undefined { + const env: string | undefined = this.getRequestFromEnv(req); + + if (!env) { + throw new InputError(`Must specify 'env' query to select environment`); + } + + if (!this.handlers.has(env)) { + res.status(404).send( + `Missing configuration. +
+
+ For this flow to work you need to supply a valid configuration for the "${env}" environment of provider.`, + ); + return undefined; + } + + return this.handlers.get(env); + } } diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 19f8d5d803..a091a37e90 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -17,13 +17,12 @@ import express from 'express'; import passport from 'passport'; import Auth0Strategy from './strategy'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; import { OAuthProvider, OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -33,8 +32,7 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; -import { Config } from '@backstage/config'; +import { RedirectInfo, AuthProviderFactory } from '../types'; type PrivateInfo = { refreshToken: string; @@ -150,29 +148,28 @@ export class Auth0AuthProvider implements OAuthProviderHandlers { } } -export function createAuth0Provider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'auth0'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const domain = envConfig.getString('domain'); - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; +export const createAuth0Provider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'auth0'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const domain = envConfig.getString('domain'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new Auth0AuthProvider({ - clientId, - clientSecret, - callbackUrl, - domain, - }); + const provider = new Auth0AuthProvider({ + clientId, + clientSecret, + callbackUrl, + domain, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: true, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 2b67510991..6c83dffda3 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -27,11 +27,6 @@ import { createAuth0Provider } from './auth0'; import { createMicrosoftProvider } from './microsoft'; import { AuthProviderConfig, AuthProviderFactory } from './types'; import { Config } from '@backstage/config'; -import { - EnvironmentHandlers, - EnvironmentHandler, - EnvironmentIdentifierFn, -} from '../lib/EnvironmentHandler'; const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider, @@ -47,9 +42,9 @@ const factories: { [providerId: string]: AuthProviderFactory } = { export const createAuthProviderRouter = ( providerId: string, globalConfig: AuthProviderConfig, - providerConfig: Config, + config: Config, logger: Logger, - issuer: TokenIssuer, + tokenIssuer: TokenIssuer, ) => { const factory = factories[providerId]; if (!factory) { @@ -57,28 +52,8 @@ export const createAuthProviderRouter = ( } const router = Router(); - const envs = providerConfig.keys(); - const envProviders: EnvironmentHandlers = {}; - let envIdentifier: EnvironmentIdentifierFn | undefined; - for (const env of envs) { - const envConfig = providerConfig.getConfig(env); - const provider = factory(globalConfig, env, envConfig, logger, issuer); - if (provider) { - envProviders[env] = provider; - envIdentifier = provider.identifyEnv; - } - } - - if (typeof envIdentifier === 'undefined') { - throw Error(`No envIdentifier provided for '${providerId}'`); - } - - const handler = new EnvironmentHandler( - providerId, - envProviders, - envIdentifier, - ); + const handler = factory({ globalConfig, config, logger, tokenIssuer }); router.get('/start', handler.start.bind(handler)); router.get('/handler/frame', handler.frameHandler.bind(handler)); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 68810a5b1b..ab52b628e7 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -22,17 +22,15 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { OAuthProvider, OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; import passport from 'passport'; -import { Config } from '@backstage/config'; export type GithubAuthProviderOptions = OAuthProviderOptions & { tokenUrl?: string; @@ -136,43 +134,42 @@ export class GithubAuthProvider implements OAuthProviderHandlers { } } -export function createGithubProvider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'github'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const enterpriseInstanceUrl = envConfig.getOptionalString( - 'enterpriseInstanceUrl', - ); - const authorizationUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/authorize` - : undefined; - const tokenUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/access_token` - : undefined; - const userProfileUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/api/v3/user` - : undefined; - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; +export const createGithubProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'github'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const enterpriseInstanceUrl = envConfig.getOptionalString( + 'enterpriseInstanceUrl', + ); + const authorizationUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/authorize` + : undefined; + const tokenUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/access_token` + : undefined; + const userProfileUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/api/v3/user` + : undefined; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new GithubAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenUrl, - userProfileUrl, - authorizationUrl, - }); + const provider = new GithubAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenUrl, + userProfileUrl, + authorizationUrl, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: true, - persistScopes: true, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: true, + persistScopes: true, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index d076644e46..26e849aa5c 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -22,17 +22,15 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { OAuthProvider, OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; import passport from 'passport'; -import { Config } from '@backstage/config'; export type GitlabAuthProviderOptions = OAuthProviderOptions & { baseUrl: string; @@ -139,30 +137,29 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { } } -export function createGitlabProvider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'gitlab'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const baseUrl = audience || 'https://gitlab.com'; - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; +export const createGitlabProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'gitlab'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const baseUrl = audience || 'https://gitlab.com'; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new GitlabAuthProvider({ - clientId, - clientSecret, - callbackUrl, - baseUrl, - }); + const provider = new GitlabAuthProvider({ + clientId, + clientSecret, + callbackUrl, + baseUrl, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: true, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 21cd76b26f..62e1d90577 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -24,17 +24,15 @@ import { executeFetchUserProfileStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { OAuthProvider, OAuthProviderHandlers, OAuthProviderOptions, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; import passport from 'passport'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; -import { Config } from '@backstage/config'; type PrivateInfo = { refreshToken: string; @@ -147,27 +145,26 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { } } -export function createGoogleProvider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'google'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; +export const createGoogleProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'google'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new GoogleAuthProvider({ - clientId, - clientSecret, - callbackUrl, - }); + const provider = new GoogleAuthProvider({ + clientId, + clientSecret, + callbackUrl, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index a7edbb134e..97e7dc3781 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -27,17 +27,15 @@ import { PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { OAuthProvider, OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; -import { Config } from '@backstage/config'; import got from 'got'; @@ -204,34 +202,33 @@ export class MicrosoftAuthProvider implements OAuthProviderHandlers { } } -export function createMicrosoftProvider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'microsoft'; +export const createMicrosoftProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'microsoft'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const tenantID = envConfig.getString('tenantId'); + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const tenantID = envConfig.getString('tenantId'); - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`; - const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`; + const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`; - const provider = new MicrosoftAuthProvider({ - clientId, - clientSecret, - callbackUrl, - authorizationUrl, - tokenUrl, + const provider = new MicrosoftAuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + }); + + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - }); -} diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index bc50408948..fea7d58e5c 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -17,13 +17,12 @@ import express from 'express'; import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; import { OAuthProvider, OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -33,8 +32,7 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; -import { Config } from '@backstage/config'; +import { RedirectInfo, AuthProviderFactory } from '../types'; type PrivateInfo = { refreshToken: string; @@ -158,31 +156,30 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers { } } -export function createOAuth2Provider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'oauth2'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = envConfig.getString('authorizationUrl'); - const tokenUrl = envConfig.getString('tokenUrl'); +export const createOAuth2Provider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'oauth2'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = envConfig.getString('authorizationUrl'); + const tokenUrl = envConfig.getString('tokenUrl'); - const provider = new OAuth2AuthProvider({ - clientId, - clientSecret, - callbackUrl, - authorizationUrl, - tokenUrl, - }); + const provider = new OAuth2AuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 05a692ba40..3d33868e84 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -19,6 +19,7 @@ import { OAuthProviderOptions, OAuthProviderHandlers, OAuthResponse, + OAuthEnvironmentHandler, } from '../../lib/oauth'; import { Strategy as OktaStrategy } from 'passport-okta-oauth'; import passport from 'passport'; @@ -30,11 +31,8 @@ import { executeFetchUserProfileStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderConfig, RedirectInfo } from '../types'; -import { Logger } from 'winston'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { StateStore } from 'passport-oauth2'; -import { TokenIssuer } from '../../identity'; -import { Config } from '@backstage/config'; type PrivateInfo = { refreshToken: string; @@ -169,29 +167,28 @@ export class OktaAuthProvider implements OAuthProviderHandlers { } } -export function createOktaProvider( - config: AuthProviderConfig, - _: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'okta'; - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`; +export const createOktaProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'okta'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new OktaAuthProvider({ - audience, - clientId, - clientSecret, - callbackUrl, - }); + const provider = new OktaAuthProvider({ + audience, + clientId, + clientSecret, + callbackUrl, + }); - return OAuthProvider.fromConfig(config, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 82a1cfa1b7..2bd8ed0bf3 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -26,14 +26,12 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { - AuthProviderConfig, AuthProviderRouteHandlers, ProfileInfo, + AuthProviderFactory, } from '../types'; import { postMessageResponse } from '../../lib/flow'; -import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; -import { Config } from '@backstage/config'; type SamlInfo = { userId: string; @@ -119,15 +117,12 @@ type SAMLProviderOptions = { tokenIssuer: TokenIssuer; }; -export function createSamlProvider( - _authProviderConfig: AuthProviderConfig, - _env: string, - envConfig: Config, - _logger: Logger, - tokenIssuer: TokenIssuer, -) { - const entryPoint = envConfig.getString('entryPoint'); - const issuer = envConfig.getString('issuer'); +export const createSamlProvider: AuthProviderFactory = ({ + config, + tokenIssuer, +}) => { + const entryPoint = config.getString('entryPoint'); + const issuer = config.getString('issuer'); const opts = { entryPoint, issuer, @@ -136,4 +131,4 @@ export function createSamlProvider( }; return new SamlAuthProvider(opts); -} +}; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index bc80a3d9b5..5c05b02739 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -108,24 +108,18 @@ export interface AuthProviderRouteHandlers { * @param {express.Response} res */ logout?(req: express.Request, res: express.Response): Promise; - - /** - *(Optional) A method to identify the environment Context of the Request - * - *Request - *- contains the environment context information encoded in the request - * @param {express.Request} req - */ - identifyEnv?(req: express.Request): string | undefined; } +export type AuthProviderFactoryOptions = { + globalConfig: AuthProviderConfig; + config: Config; + logger: Logger; + tokenIssuer: TokenIssuer; +}; + export type AuthProviderFactory = ( - globalConfig: AuthProviderConfig, - env: string, - envConfig: Config, - logger: Logger, - issuer: TokenIssuer, -) => AuthProviderRouteHandlers | undefined; + options: AuthProviderFactoryOptions, +) => AuthProviderRouteHandlers; export type AuthResponse = { providerInfo: ProviderInfo; From fa32cee7adb2758f5c5dcec0059c74f069f21371 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 14:51:22 +0200 Subject: [PATCH 5/8] docs/auth: update to reflect backend changes --- docs/auth/add-auth-provider.md | 151 ++++++++++++++++++++---------- docs/auth/auth-backend-classes.md | 47 +++++----- 2 files changed, 127 insertions(+), 71 deletions(-) diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index a2830dcf4f..55ef554c05 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -48,22 +48,35 @@ provider class which implements a handler for the chosen framework. #### Adding an OAuth based provider If we're adding an `OAuth` based provider we would implement the -[OAuthProviderHandlers](#OAuthProviderHandlers) interface. +[OAuthProviderHandlers](#OAuthProviderHandlers) interface. By implementing this +interface we can use the `OAuthProvider` class provided by `lib/oauth`, meaning +we don't need to implement the full +[AuthProviderRouteHandlers](#AuthProviderRouteHandlers) interface that providers +otherwise need to implement. -The provider class takes the provider's configuration as a class parameter. It -also imports the `Strategy` from the passport package. +The provider class takes the provider's options as a class parameter. It also +imports the `Strategy` from the passport package. ```ts import { Strategy as ProviderAStrategy } from 'passport-provider-a'; +export type ProviderAProviderOptions = OAuthProviderOptions & { + // extra options here +} + export class ProviderAAuthProvider implements OAuthProviderHandlers { - private readonly providerConfig: AuthProviderConfig; private readonly _strategy: ProviderAStrategy; - constructor(providerConfig: AuthProviderConfig) { - this.providerConfig = providerConfig; + constructor(options: ProviderAProviderOptions) { this._strategy = new ProviderAStrategy( - { ...providerConfig.options }, + { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + passReqToCallback: false as true, + response_type: 'code', + /// ... etc + } verifyFunction, // See the "Verify Callback" section ); } @@ -82,14 +95,18 @@ An non-`OAuth` based provider could implement [AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead. ```ts +type ProviderAOptions = { + // ... +}; + export class ProviderAAuthProvider implements AuthProviderRouteHandlers { - private readonly providerConfig: AuthProviderConfig; private readonly _strategy: ProviderAStrategy; - constructor(providerConfig: AuthProviderConfig) { - this.providerConfig = providerConfig; + constructor(options: ProviderAOptions) { this._strategy = new ProviderAStrategy( - { ...providerConfig.options }, + { + // ... + }, verifyFunction, // See the "Verify Callback" section ); } @@ -101,31 +118,61 @@ export class ProviderAAuthProvider implements AuthProviderRouteHandlers { } ``` -#### Create method +#### Factory method -Each provider exports a create method that creates the provider instance, -optionally extending a supported authorization framework. This method exists to -allow for flexibility if additional frameworks are supported in the future. +Each provider exports a factory function that instantiates the provider. The +factory should implement [AuthProviderFactory](#AuthProviderFactory), which +passes in a object with utilities for configuration, logging, token issuing, +etc. The factory should return an implementation of +[AuthProviderRouteHandlers](#AuthProviderRouteHandlers). -Implementing OAuth by returning an instance of `OAuthProvider` based of the -provider's class: +The factory is what decides the mapping from +[static configuration](../conf/index.md) to the creation of auth providers. For +example, OAuth providers use `OAuthEnvironmentHandler` to allow for multiple +different configurations, one for each environment, which looks like this; ```ts -export function createProviderAProvider(config: AuthProviderConfig) { - const provider = new ProviderAAuthProvider(config); - const oauthProvider = new OAuthProvider(provider, config.provider, true); - return oauthProvider; -} +export const createOktaProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + // read options from config + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + + // instantiate our OAuthProviderHandlers implementation + const provider = new OktaAuthProvider({ + audience, + clientId, + clientSecret, + callbackUrl, + }); + + // Wrap the OAuthProviderHandlers with OAuthProvider, which implements AuthProviderRouteHandlers + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); + }); ``` -Not extending with OAuth, the main difference here is that the create method is -returning a instance of the class without adding the OAuth authorization -framework to it. +The purpose of the different environment is to allow for a single auth-backend +to service as the authentication service for multiple different frontend +environments, such as local development, staging, and production. + +The factory function for other providers can be a lot simpler, as they might not +have configuration for each environment. Looking something like this: ```ts -export function createProviderAProvider(config: AuthProviderConfig) { - return new ProviderAAuthProvider(config); -} +export const createProviderAProvider: AuthProviderFactory = ({ config }) => { + const a = config.getString('a'); + const b = config.getString('b'); + + return new ProviderAAuthProvider({ a, b }); +}; ``` #### Verify Callback @@ -153,19 +200,6 @@ export { createProviderAProvider } from './provider'; ### Hook it up to the backend -**`plugins/auth-backend/src/providers/config.ts`** The provider needs to be -configured properly so you need to add it to the list of configured providers, -all of which implement [AuthProviderConfig](#AuthProviderConfig): - -```ts -export const providers = [ - { - provider: 'providerA', # used as an identifier - options: { ... }, # consult the provider documentation for which options you should provide - disableRefresh: true # if the provider lacks refresh tokens - }, -``` - **`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend` starts it sets up routing for all the available providers by calling `createAuthProviderRouter` on each provider. You need to import the create @@ -173,6 +207,7 @@ method from the provider and add it to the factory: ```ts import { createProviderAProvider } from './providerA'; + const factories: { [providerId: string]: AuthProviderFactory } = { providerA: createProviderAProvider, }; @@ -203,10 +238,21 @@ web browser and you should be able to trigger the authorization flow. ```ts export interface OAuthProviderHandlers { - start(req: express.Request, options: any): Promise; - handler(req: express.Request): Promise; - refresh?(refreshToken: string, scope: string): Promise; - logout?(): Promise; + start( + req: express.Request, + options: Record, + ): Promise; + handler( + req: express.Request, + ): Promise<{ + response: AuthResponse; + refreshToken?: string; + }>; + refresh?( + refreshToken: string, + scope: string, + ): Promise>; + logout?(): Promise; } ``` @@ -221,12 +267,17 @@ export interface AuthProviderRouteHandlers { } ``` -##### AuthProviderConfig +##### AuthProviderFactory ```ts -export type AuthProviderConfig = { - provider: string; - options: any; - disableRefresh?: boolean; +export type AuthProviderFactoryOptions = { + globalConfig: AuthProviderConfig; + config: Config; + logger: Logger; + tokenIssuer: TokenIssuer; }; + +export type AuthProviderFactory = ( + options: AuthProviderFactoryOptions, +) => AuthProviderRouteHandlers; ``` diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md index 9034a3d054..258a732651 100644 --- a/docs/auth/auth-backend-classes.md +++ b/docs/auth/auth-backend-classes.md @@ -50,11 +50,12 @@ OAuth2) providers, you can configure them by setting the right variables in Each authentication provider (except SAML) needs five parameters: an OAuth client ID, a client secret, an authorization endpoint and a token endpoint, and an app origin. The app origin is the URL at which the frontend of the -application is hosted. This is required because the application opens a popup -window to perform the authentication, and once the flow is completed, the popup -window sends a `postMessage` to the frontend application to indicate the result -of the operation. Also this URL is used to verify that authentication requests -are coming from only this endpoint. +application is hosted, and it is read from the `app.baseUrl` config. This is +required because the application opens a popup window to perform the +authentication, and once the flow is completed, the popup window sends a +`postMessage` to the frontend application to indicate the result of the +operation. Also this URL is used to verify that authentication requests are +coming from only this endpoint. These values are configured via the `app-config.yaml` present in the root of your app folder. @@ -64,8 +65,6 @@ auth: providers: google: development: - appOrigin: "http://localhost:3000/" - secure: false clientId: $secret: env: AUTH_GOOGLE_CLIENT_ID @@ -74,8 +73,6 @@ auth: env: AUTH_GOOGLE_CLIENT_SECRET github: development: - appOrigin: "http://localhost:3000/" - secure: false clientId: $secret: env: AUTH_GITHUB_CLIENT_ID @@ -87,8 +84,6 @@ auth: env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL gitlab: development: - appOrigin: "http://localhost:3000/" - secure: false clientId: $secret: ... @@ -96,24 +91,34 @@ auth: ## Technical Notes -### EnvironmentHandler +### OAuthEnvironmentHandler The concept of an "env" is core to the way the auth backend works. It uses an `env` query parameter to identify the environment in which the application is running (`development`, `staging`, `production`, etc). Each runtime can support multiple environments at the same time and the right handler for each request is identified and dispatched to based on the `env` parameter. All -`AuthProviderRouteHandlers` are wrapped within an `EnvironmentHandler`. +`AuthProviderRouteHandlers` are wrapped within an `OAuthEnvironmentHandler`. -An `EnvironmentHandler` takes an ID for each provider that it wraps, the -handlers for each env the provider is supported in, and a function that, given a -`Request` as argument, can extract the information about the env under which it -should be processed. +To instantiate multiple OAuth providers for different environments, use +`OAuthEnvironmentHandler.mapConfig`. It's a helper to iterate over a +configuration object that is a map of environment to configurations. See one of +the existing OAuth providers for an example of how it is used. -Each provider exposes a factory function `createXProvider` (where X is the name -of the provider) that takes the global config, env and other parameters and -returns an `AuthProviderRouteHandlers` for each env, and an `envIdentifier` -function to identify the env of a request. +Given the following configuration: + +```yaml +development: + clientId: abc + clientSecret: secret +production: + clientId: xyz + clientSecret: supersecret +``` + +The `OAuthEnvironmentHandler.mapConfig(config, envConfig => ...)` call will +split the `config` by the top level `development` and `production` keys, and +pass on each block as `envConfig`. For a list of currently available providers, look in the `factories` module located in `plugins/auth-backend/src/providers/factories.ts` From c8785b9ee063fd3c76c36bdcc17e37d218c6d7db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 15:08:38 +0200 Subject: [PATCH 6/8] auth-backend: rename OAuthProvider to OAuthAdapter --- docs/auth/auth-backend-classes.md | 2 +- .../src/lib/flow/authFlowHelpers.test.ts | 2 +- ...hProvider.test.ts => OAuthAdapter.test.ts} | 24 ++++++++---------- .../{OAuthProvider.ts => OAuthAdapter.ts} | 25 ++++++++----------- plugins/auth-backend/src/lib/oauth/index.ts | 4 +-- plugins/auth-backend/src/lib/oauth/types.ts | 2 +- .../src/providers/auth0/provider.ts | 8 +++--- .../src/providers/github/provider.ts | 8 +++--- .../src/providers/gitlab/provider.ts | 8 +++--- .../src/providers/google/provider.ts | 8 +++--- .../src/providers/microsoft/provider.ts | 8 +++--- .../src/providers/oauth2/provider.ts | 8 +++--- .../src/providers/okta/provider.ts | 8 +++--- 13 files changed, 53 insertions(+), 62 deletions(-) rename plugins/auth-backend/src/lib/oauth/{OAuthProvider.test.ts => OAuthAdapter.test.ts} (91%) rename plugins/auth-backend/src/lib/oauth/{OAuthProvider.ts => OAuthAdapter.ts} (91%) diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md index 258a732651..0147c66dd9 100644 --- a/docs/auth/auth-backend-classes.md +++ b/docs/auth/auth-backend-classes.md @@ -26,7 +26,7 @@ refer to the type documentation under `plugins/auth-backend/src/providers/types.ts`. There are currently two different classes for two authentication mechanisms that -implement this interface: an `OAuthProvider` for [OAuth](https://oauth.net/2/) +implement this interface: an `OAuthAdapter` for [OAuth](https://oauth.net/2/) based mechanisms and a `SAMLAuthProvider` for [SAML](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) based mechanisms. diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts index b525e98e12..f42e4c4270 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts @@ -18,7 +18,7 @@ import express from 'express'; import { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers'; import { WebMessageResponse } from './types'; -describe('OAuthProvider Utils', () => { +describe('oauth helpers', () => { describe('postMessageResponse', () => { const appOrigin = 'http://localhost:3000'; it('should post a message back with payload success', () => { diff --git a/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts similarity index 91% rename from plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts rename to plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index ce78733211..d2b31213f2 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -15,13 +15,9 @@ */ import express from 'express'; -import { - THOUSAND_DAYS_MS, - TEN_MINUTES_MS, - OAuthProvider, -} from './OAuthProvider'; +import { THOUSAND_DAYS_MS, TEN_MINUTES_MS, OAuthAdapter } from './OAuthAdapter'; import { encodeState } from './helpers'; -import { OAuthProviderHandlers } from './types'; +import { OAuthHandlers } from './types'; const mockResponseData = { providerInfo: { @@ -38,8 +34,8 @@ const mockResponseData = { }, }; -describe('OAuthProvider', () => { - class MyAuthProvider implements OAuthProviderHandlers { +describe('OAuthAdapter', () => { + class MyAuthProvider implements OAuthHandlers { async start() { return { url: '/url', @@ -71,7 +67,7 @@ describe('OAuthProvider', () => { }; it('sets the correct headers in start', async () => { - const oauthProvider = new OAuthProvider( + const oauthProvider = new OAuthAdapter( providerInstance, oAuthProviderOptions, ); @@ -106,7 +102,7 @@ describe('OAuthProvider', () => { }); it('sets the refresh cookie if refresh is enabled', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, }); @@ -140,7 +136,7 @@ describe('OAuthProvider', () => { }); it('does not set the refresh cookie if refresh is disabled', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: true, }); @@ -165,7 +161,7 @@ describe('OAuthProvider', () => { }); it('removes refresh cookie when logging out', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, }); @@ -190,7 +186,7 @@ describe('OAuthProvider', () => { it('gets new access-token when refreshing', async () => { oAuthProviderOptions.disableRefresh = false; - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, }); @@ -219,7 +215,7 @@ describe('OAuthProvider', () => { }); it('handles refresh without capabilities', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: true, }); diff --git a/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts similarity index 91% rename from plugins/auth-backend/src/lib/oauth/OAuthProvider.ts rename to plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 1fe62845a0..0f092ae780 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -26,7 +26,7 @@ import { InputError } from '@backstage/backend-common'; import { TokenIssuer } from '../../identity'; import { verifyNonce, encodeState } from './helpers'; import { postMessageResponse, ensuresXRequestedWith } from '../flow'; -import { OAuthProviderHandlers } from './types'; +import { OAuthHandlers } from './types'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -42,20 +42,20 @@ export type Options = { tokenIssuer: TokenIssuer; }; -export class OAuthProvider implements AuthProviderRouteHandlers { +export class OAuthAdapter implements AuthProviderRouteHandlers { static fromConfig( config: AuthProviderConfig, - providerHandlers: OAuthProviderHandlers, + handlers: OAuthHandlers, options: Pick< Options, 'providerId' | 'persistScopes' | 'disableRefresh' | 'tokenIssuer' >, - ): OAuthProvider { + ): OAuthAdapter { const { origin: appOrigin } = new URL(config.appUrl); const secure = config.baseUrl.startsWith('https://'); const url = new URL(config.baseUrl); const cookiePath = `${url.pathname}/${options.providerId}`; - return new OAuthProvider(providerHandlers, { + return new OAuthAdapter(handlers, { ...options, appOrigin, cookieDomain: url.hostname, @@ -65,7 +65,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { } constructor( - private readonly providerHandlers: OAuthProviderHandlers, + private readonly handlers: OAuthHandlers, private readonly options: Options, ) {} @@ -94,10 +94,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { state: stateParameter, }; - const { url, status } = await this.providerHandlers.start( - req, - queryParameters, - ); + const { url, status } = await this.handlers.start(req, queryParameters); res.statusCode = status || 302; res.setHeader('Location', url); @@ -113,9 +110,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { // verify nonce cookie and state cookie on callback verifyNonce(req, this.options.providerId); - const { response, refreshToken } = await this.providerHandlers.handler( - req, - ); + const { response, refreshToken } = await this.handlers.handler(req); if (this.options.persistScopes) { const grantedScopes = this.getScopesFromCookie( @@ -172,7 +167,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { return; } - if (!this.providerHandlers.refresh || this.options.disableRefresh) { + if (!this.handlers.refresh || this.options.disableRefresh) { res.send( `Refresh token not supported for provider: ${this.options.providerId}`, ); @@ -191,7 +186,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { const scope = req.query.scope?.toString() ?? ''; // get new access_token - const response = await this.providerHandlers.refresh(refreshToken, scope); + const response = await this.handlers.refresh(refreshToken, scope); await this.populateIdentity(response.backstageIdentity); diff --git a/plugins/auth-backend/src/lib/oauth/index.ts b/plugins/auth-backend/src/lib/oauth/index.ts index eac43141a4..05c8bd9d3d 100644 --- a/plugins/auth-backend/src/lib/oauth/index.ts +++ b/plugins/auth-backend/src/lib/oauth/index.ts @@ -15,9 +15,9 @@ */ export { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler'; -export { OAuthProvider } from './OAuthProvider'; +export { OAuthAdapter } from './OAuthAdapter'; export type { - OAuthProviderHandlers, + OAuthHandlers, OAuthProviderInfo, OAuthProviderOptions, OAuthResponse, diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index 440b89ed6f..a854326bed 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -72,7 +72,7 @@ export type OAuthState = { * handlers for different methods to perform authentication, get access tokens, * refresh tokens and perform sign out. */ -export interface OAuthProviderHandlers { +export interface OAuthHandlers { /** * This method initiates a sign in request with an auth provider. * @param {express.Request} req diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index a091a37e90..3b7817ffe9 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -18,9 +18,9 @@ import express from 'express'; import passport from 'passport'; import Auth0Strategy from './strategy'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -42,7 +42,7 @@ export type Auth0AuthProviderOptions = OAuthProviderOptions & { domain: string; }; -export class Auth0AuthProvider implements OAuthProviderHandlers { +export class Auth0AuthProvider implements OAuthHandlers { private readonly _strategy: Auth0Strategy; constructor(options: Auth0AuthProviderOptions) { @@ -167,7 +167,7 @@ export const createAuth0Provider: AuthProviderFactory = ({ domain, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: true, providerId, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index ab52b628e7..2205fb6794 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -24,9 +24,9 @@ import { } from '../../lib/passport'; import { RedirectInfo, AuthProviderFactory } from '../types'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -38,7 +38,7 @@ export type GithubAuthProviderOptions = OAuthProviderOptions & { authorizationUrl?: string; }; -export class GithubAuthProvider implements OAuthProviderHandlers { +export class GithubAuthProvider implements OAuthHandlers { private readonly _strategy: GithubStrategy; static transformPassportProfile(rawProfile: any): passport.Profile { @@ -166,7 +166,7 @@ export const createGithubProvider: AuthProviderFactory = ({ authorizationUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: true, persistScopes: true, providerId, diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 26e849aa5c..97f0b2bd22 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -24,9 +24,9 @@ import { } from '../../lib/passport'; import { RedirectInfo, AuthProviderFactory } from '../types'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -36,7 +36,7 @@ export type GitlabAuthProviderOptions = OAuthProviderOptions & { baseUrl: string; }; -export class GitlabAuthProvider implements OAuthProviderHandlers { +export class GitlabAuthProvider implements OAuthHandlers { private readonly _strategy: GitlabStrategy; static transformPassportProfile(rawProfile: any): passport.Profile { @@ -157,7 +157,7 @@ export const createGitlabProvider: AuthProviderFactory = ({ baseUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: true, providerId, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 62e1d90577..9ee2e1d680 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -26,8 +26,8 @@ import { } from '../../lib/passport'; import { RedirectInfo, AuthProviderFactory } from '../types'; import { - OAuthProvider, - OAuthProviderHandlers, + OAuthAdapter, + OAuthHandlers, OAuthProviderOptions, OAuthResponse, OAuthEnvironmentHandler, @@ -38,7 +38,7 @@ type PrivateInfo = { refreshToken: string; }; -export class GoogleAuthProvider implements OAuthProviderHandlers { +export class GoogleAuthProvider implements OAuthHandlers { private readonly _strategy: GoogleStrategy; constructor(options: OAuthProviderOptions) { @@ -162,7 +162,7 @@ export const createGoogleProvider: AuthProviderFactory = ({ callbackUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 97e7dc3781..edc5509d84 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -30,9 +30,9 @@ import { import { RedirectInfo, AuthProviderFactory } from '../types'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -48,7 +48,7 @@ export type MicrosoftAuthProviderOptions = OAuthProviderOptions & { tokenUrl?: string; }; -export class MicrosoftAuthProvider implements OAuthProviderHandlers { +export class MicrosoftAuthProvider implements OAuthHandlers { private readonly _strategy: MicrosoftStrategy; static transformAuthResponse( @@ -226,7 +226,7 @@ export const createMicrosoftProvider: AuthProviderFactory = ({ tokenUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index fea7d58e5c..5a4882fa6f 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -18,9 +18,9 @@ import express from 'express'; import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -43,7 +43,7 @@ export type OAuth2AuthProviderOptions = OAuthProviderOptions & { tokenUrl: string; }; -export class OAuth2AuthProvider implements OAuthProviderHandlers { +export class OAuth2AuthProvider implements OAuthHandlers { private readonly _strategy: OAuth2Strategy; constructor(options: OAuth2AuthProviderOptions) { @@ -177,7 +177,7 @@ export const createOAuth2Provider: AuthProviderFactory = ({ tokenUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 3d33868e84..0368bd8415 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -15,9 +15,9 @@ */ import express from 'express'; import { - OAuthProvider, + OAuthAdapter, OAuthProviderOptions, - OAuthProviderHandlers, + OAuthHandlers, OAuthResponse, OAuthEnvironmentHandler, } from '../../lib/oauth'; @@ -42,7 +42,7 @@ export type OktaAuthProviderOptions = OAuthProviderOptions & { audience: string; }; -export class OktaAuthProvider implements OAuthProviderHandlers { +export class OktaAuthProvider implements OAuthHandlers { private readonly _strategy: any; /** @@ -186,7 +186,7 @@ export const createOktaProvider: AuthProviderFactory = ({ callbackUrl, }); - return OAuthProvider.fromConfig(globalConfig, provider, { + return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, tokenIssuer, From 39934f43b68bd6da3144d7b85a6636758ce2845a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 15:11:17 +0200 Subject: [PATCH 7/8] auth-backend: add not found error if provider is not found --- plugins/auth-backend/src/service/router.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index b5451cad6c..19a74d47c5 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -22,6 +22,7 @@ import { Logger } from 'winston'; import { createAuthProviderRouter } from '../providers'; import { Config } from '@backstage/config'; import { DatabaseKeyStore, TokenFactory, createOidcRouter } from '../identity'; +import { NotFoundError } from '@backstage/backend-common'; export interface RouterOptions { logger: Logger; @@ -88,5 +89,10 @@ export async function createRouter( }), ); + router.use('/:provider/', req => { + const { provider } = req.params; + throw new NotFoundError(`No auth provider registered for '${provider}'`); + }); + return router; } From 5a772948298dc3858b4964bc688487697e219a90 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Sep 2020 17:48:10 +0200 Subject: [PATCH 8/8] docs/auth: minor text fixes --- docs/auth/add-auth-provider.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index 55ef554c05..7e4f5c8e2f 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -118,7 +118,7 @@ export class ProviderAAuthProvider implements AuthProviderRouteHandlers { } ``` -#### Factory method +#### Factory function Each provider exports a factory function that instantiates the provider. The factory should implement [AuthProviderFactory](#AuthProviderFactory), which @@ -159,8 +159,8 @@ export const createOktaProvider: AuthProviderFactory = ({ }); ``` -The purpose of the different environment is to allow for a single auth-backend -to service as the authentication service for multiple different frontend +The purpose of the different environments is to allow for a single auth-backend +to serve as the authentication service for multiple different frontend environments, such as local development, staging, and production. The factory function for other providers can be a lot simpler, as they might not @@ -191,7 +191,7 @@ export const createProviderAProvider: AuthProviderFactory = ({ config }) => { > http://www.passportjs.org/docs/configure/ **`plugins/auth-backend/src/providers/providerA/index.ts`** is simply -re-exporting the create method to be used for hooking the provider up to the +re-exporting the factory function to be used for hooking the provider up to the backend. ```ts @@ -202,8 +202,8 @@ export { createProviderAProvider } from './provider'; **`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend` starts it sets up routing for all the available providers by calling -`createAuthProviderRouter` on each provider. You need to import the create -method from the provider and add it to the factory: +`createAuthProviderRouter` on each provider. You need to import the factory +function from the provider and add it to the factory: ```ts import { createProviderAProvider } from './providerA';