From fa7ea3f2f0810ae5b82b6829801208d2599f1f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 16 Feb 2024 14:33:10 +0100 Subject: [PATCH] break out the providers router into a separate file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/six-sloths-listen.md | 5 + plugins/auth-backend/src/identity/index.ts | 2 +- plugins/auth-backend/src/identity/router.ts | 17 +- plugins/auth-backend/src/index.ts | 2 +- plugins/auth-backend/src/providers/index.ts | 1 + .../src/{service => providers}/router.test.ts | 0 plugins/auth-backend/src/providers/router.ts | 155 ++++++++++++++++++ plugins/auth-backend/src/service/index.ts | 17 ++ plugins/auth-backend/src/service/router.ts | 133 +++------------ 9 files changed, 208 insertions(+), 124 deletions(-) create mode 100644 .changeset/six-sloths-listen.md rename plugins/auth-backend/src/{service => providers}/router.test.ts (100%) create mode 100644 plugins/auth-backend/src/providers/router.ts create mode 100644 plugins/auth-backend/src/service/index.ts diff --git a/.changeset/six-sloths-listen.md b/.changeset/six-sloths-listen.md new file mode 100644 index 0000000000..47886e3f77 --- /dev/null +++ b/.changeset/six-sloths-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Internal refactor to break out how the router is constructed diff --git a/plugins/auth-backend/src/identity/index.ts b/plugins/auth-backend/src/identity/index.ts index a5e0dd4b80..0492836723 100644 --- a/plugins/auth-backend/src/identity/index.ts +++ b/plugins/auth-backend/src/identity/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { createOidcRouter } from './router'; +export { bindOidcRouter } from './router'; export { TokenFactory } from './TokenFactory'; export { DatabaseKeyStore } from './DatabaseKeyStore'; export { MemoryKeyStore } from './MemoryKeyStore'; diff --git a/plugins/auth-backend/src/identity/router.ts b/plugins/auth-backend/src/identity/router.ts index c9ae9e685a..c3c419e9ea 100644 --- a/plugins/auth-backend/src/identity/router.ts +++ b/plugins/auth-backend/src/identity/router.ts @@ -14,18 +14,21 @@ * limitations under the License. */ +import express from 'express'; import Router from 'express-promise-router'; import { TokenIssuer } from './types'; -export type Options = { - baseUrl: string; - tokenIssuer: TokenIssuer; -}; - -export function createOidcRouter(options: Options) { +export function bindOidcRouter( + targetRouter: express.Router, + options: { + baseUrl: string; + tokenIssuer: TokenIssuer; + }, +) { const { baseUrl, tokenIssuer } = options; const router = Router(); + targetRouter.use(router); const config = { issuer: baseUrl, @@ -68,6 +71,4 @@ export function createOidcRouter(options: Options) { router.get('/v1/userinfo', (_req, res) => { res.status(501).send('Not Implemented'); }); - - return router; } diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 3943387e15..6c3f866031 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -21,7 +21,7 @@ */ export { authPlugin as default } from './authPlugin'; -export * from './service/router'; +export * from './service'; export type { TokenParams } from './identity'; export * from './providers'; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 8173a7fbc3..b9543c275c 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -34,6 +34,7 @@ export type { SamlAuthResult } from './saml'; export type { GcpIapResult, GcpIapTokenInfo } from './gcp-iap'; export { providers, defaultAuthProviderFactories } from './providers'; +export { createOriginFilter, type ProviderFactories } from './router'; export { createAuthProviderIntegration } from './createAuthProviderIntegration'; diff --git a/plugins/auth-backend/src/service/router.test.ts b/plugins/auth-backend/src/providers/router.test.ts similarity index 100% rename from plugins/auth-backend/src/service/router.test.ts rename to plugins/auth-backend/src/providers/router.test.ts diff --git a/plugins/auth-backend/src/providers/router.ts b/plugins/auth-backend/src/providers/router.ts new file mode 100644 index 0000000000..9971466ad4 --- /dev/null +++ b/plugins/auth-backend/src/providers/router.ts @@ -0,0 +1,155 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { NotFoundError, assertError } from '@backstage/errors'; +import { AuthProviderFactory } from '@backstage/plugin-auth-node'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Minimatch } from 'minimatch'; +import { CatalogAuthResolverContext } from '../lib/resolvers/CatalogAuthResolverContext'; +import { TokenIssuer } from '../identity/types'; + +/** @public */ +export type ProviderFactories = { [s: string]: AuthProviderFactory }; + +export function bindProviderRouters( + targetRouter: express.Router, + options: { + providers: ProviderFactories; + appUrl: string; + baseUrl: string; + config: Config; + logger: LoggerService; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + tokenIssuer: TokenIssuer; + catalogApi?: CatalogApi; + }, +) { + const { + providers, + appUrl, + baseUrl, + config, + logger, + discovery, + tokenManager, + tokenIssuer, + catalogApi, + } = options; + + const providersConfig = config.getOptionalConfig('auth.providers'); + + const isOriginAllowed = createOriginFilter(config); + + for (const [providerId, providerFactory] of Object.entries(providers)) { + if (providersConfig?.has(providerId)) { + logger.info(`Configuring auth provider: ${providerId}`); + try { + const provider = providerFactory({ + providerId, + appUrl, + baseUrl: baseUrl, + isOriginAllowed, + globalConfig: { + baseUrl: baseUrl, + appUrl, + isOriginAllowed, + }, + config: providersConfig.getConfig(providerId), + logger, + resolverContext: CatalogAuthResolverContext.create({ + logger, + catalogApi: + catalogApi ?? new CatalogClient({ discoveryApi: discovery }), + tokenIssuer, + tokenManager, + }), + }); + + const r = Router(); + + r.get('/start', provider.start.bind(provider)); + r.get('/handler/frame', provider.frameHandler.bind(provider)); + r.post('/handler/frame', provider.frameHandler.bind(provider)); + if (provider.logout) { + r.post('/logout', provider.logout.bind(provider)); + } + if (provider.refresh) { + r.get('/refresh', provider.refresh.bind(provider)); + r.post('/refresh', provider.refresh.bind(provider)); + } + + targetRouter.use(`/${providerId}`, r); + } catch (e) { + assertError(e); + if (process.env.NODE_ENV !== 'development') { + throw new Error( + `Failed to initialize ${providerId} auth provider, ${e.message}`, + ); + } + + logger.warn(`Skipping ${providerId} auth provider, ${e.message}`); + + targetRouter.use(`/${providerId}`, () => { + // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found. + throw new NotFoundError( + `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` + + `auth.providers.${providerId} are missing or the environment variables used are not defined. ` + + `Check the auth backend plugin logs when the backend starts to see more details.`, + ); + }); + } + } else { + targetRouter.use(`/${providerId}`, () => { + throw new NotFoundError( + `No auth provider registered for '${providerId}'`, + ); + }); + } + } +} + +/** @public */ +export function createOriginFilter( + config: Config, +): (origin: string) => boolean { + const appUrl = config.getString('app.baseUrl'); + const { origin: appOrigin } = new URL(appUrl); + + const allowedOrigins = config.getOptionalStringArray( + 'auth.experimentalExtraAllowedOrigins', + ); + + const allowedOriginPatterns = + allowedOrigins?.map( + pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), + ) ?? []; + + return origin => { + if (origin === appOrigin) { + return true; + } + return allowedOriginPatterns.some(pattern => pattern.match(origin)); + }; +} diff --git a/plugins/auth-backend/src/service/index.ts b/plugins/auth-backend/src/service/index.ts new file mode 100644 index 0000000000..d26055aa59 --- /dev/null +++ b/plugins/auth-backend/src/service/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createRouter, type RouterOptions } from './router'; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index a8876a7559..459c347835 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -24,24 +24,19 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; -import { assertError, NotFoundError } from '@backstage/errors'; -import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; -import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; +import { NotFoundError } from '@backstage/errors'; +import { CatalogApi } from '@backstage/catalog-client'; +import { bindOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; import connectSessionKnex from 'connect-session-knex'; import passport from 'passport'; -import { Minimatch } from 'minimatch'; -import { CatalogAuthResolverContext } from '../lib/resolvers'; import { AuthDatabase } from '../database/AuthDatabase'; import { readBackstageTokenExpiration } from './readBackstageTokenExpiration'; import { TokenIssuer } from '../identity/types'; import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; import { Config } from '@backstage/config'; -import { AuthProviderFactory } from '@backstage/plugin-auth-node'; - -/** @public */ -export type ProviderFactories = { [s: string]: AuthProviderFactory }; +import { ProviderFactories, bindProviderRouters } from '../providers/router'; /** @public */ export interface RouterOptions { @@ -65,10 +60,8 @@ export async function createRouter( config, discovery, database, - tokenManager, tokenFactoryAlgorithm, providerFactories = {}, - catalogApi, } = options; const router = Router(); @@ -103,6 +96,7 @@ export async function createRouter( config.getOptionalString('auth.identityTokenAlgorithm'), }); } + const secret = config.getOptionalString('auth.session.secret'); if (secret) { router.use(cookieParser(secret)); @@ -125,96 +119,31 @@ export async function createRouter( } else { router.use(cookieParser()); } + router.use(express.urlencoded({ extended: false })); router.use(express.json()); - const allProviderFactories = options.disableDefaultProviderFactories + const providers = options.disableDefaultProviderFactories ? providerFactories : { ...defaultAuthProviderFactories, ...providerFactories, }; - const providersConfig = config.getOptionalConfig('auth.providers'); + bindProviderRouters(router, { + providers, + appUrl, + baseUrl: authUrl, + tokenIssuer, + ...options, + }); - const isOriginAllowed = createOriginFilter(config); - - for (const [providerId, providerFactory] of Object.entries( - allProviderFactories, - )) { - if (providersConfig?.has(providerId)) { - logger.info(`Configuring auth provider: ${providerId}`); - try { - const provider = providerFactory({ - providerId, - appUrl, - baseUrl: authUrl, - isOriginAllowed, - globalConfig: { - baseUrl: authUrl, - appUrl, - isOriginAllowed, - }, - config: providersConfig.getConfig(providerId), - logger, - resolverContext: CatalogAuthResolverContext.create({ - logger, - catalogApi: - catalogApi ?? new CatalogClient({ discoveryApi: discovery }), - tokenIssuer, - tokenManager, - }), - }); - - const r = Router(); - - r.get('/start', provider.start.bind(provider)); - r.get('/handler/frame', provider.frameHandler.bind(provider)); - r.post('/handler/frame', provider.frameHandler.bind(provider)); - if (provider.logout) { - r.post('/logout', provider.logout.bind(provider)); - } - if (provider.refresh) { - r.get('/refresh', provider.refresh.bind(provider)); - r.post('/refresh', provider.refresh.bind(provider)); - } - - router.use(`/${providerId}`, r); - } catch (e) { - assertError(e); - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerId} auth provider, ${e.message}`, - ); - } - - logger.warn(`Skipping ${providerId} auth provider, ${e.message}`); - - router.use(`/${providerId}`, () => { - // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found. - throw new NotFoundError( - `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` + - `auth.providers.${providerId} are missing or the environment variables used are not defined. ` + - `Check the auth backend plugin logs when the backend starts to see more details.`, - ); - }); - } - } else { - router.use(`/${providerId}`, () => { - throw new NotFoundError( - `No auth provider registered for '${providerId}'`, - ); - }); - } - } - - router.use( - createOidcRouter({ - tokenIssuer, - baseUrl: authUrl, - }), - ); + bindOidcRouter(router, { + tokenIssuer, + baseUrl: authUrl, + }); + // Gives a more helpful error message than a plain 404 router.use('/:provider/', req => { const { provider } = req.params; throw new NotFoundError(`Unknown auth provider '${provider}'`); @@ -222,27 +151,3 @@ export async function createRouter( return router; } - -/** @public */ -export function createOriginFilter( - config: Config, -): (origin: string) => boolean { - const appUrl = config.getString('app.baseUrl'); - const { origin: appOrigin } = new URL(appUrl); - - const allowedOrigins = config.getOptionalStringArray( - 'auth.experimentalExtraAllowedOrigins', - ); - - const allowedOriginPatterns = - allowedOrigins?.map( - pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), - ) ?? []; - - return origin => { - if (origin === appOrigin) { - return true; - } - return allowedOriginPatterns.some(pattern => pattern.match(origin)); - }; -}