break out the providers router into a separate file
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Internal refactor to break out how the router is constructed
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user