break out the providers router into a separate file

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-02-16 14:33:10 +01:00
parent f5e04e39d2
commit fa7ea3f2f0
9 changed files with 208 additions and 124 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Internal refactor to break out how the router is constructed
+1 -1
View File
@@ -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';
+9 -8
View File
@@ -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;
}
+1 -1
View File
@@ -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';
@@ -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';
@@ -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));
};
}
+17
View File
@@ -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';
+19 -114
View File
@@ -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));
};
}