diff --git a/.changeset/dirty-carrots-notice.md b/.changeset/dirty-carrots-notice.md new file mode 100644 index 0000000000..ccc31c25ca --- /dev/null +++ b/.changeset/dirty-carrots-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added `authPlugin` export for the new backend system. The plugin does not include any built-in auth providers, they must instead be added by installing additional modules, for example `authModuleGoogleProvider` from `@backstage/plugin-auth-backend-module-google-provider`. diff --git a/.changeset/mighty-crabs-cheat.md b/.changeset/mighty-crabs-cheat.md new file mode 100644 index 0000000000..ef2abbada4 --- /dev/null +++ b/.changeset/mighty-crabs-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added the ability to disable the built-in auth providers by passing `disableDefaultProviderFactories` to `createRouter`. diff --git a/.changeset/ninety-news-visit.md b/.changeset/ninety-news-visit.md new file mode 100644 index 0000000000..1b990f9ac7 --- /dev/null +++ b/.changeset/ninety-news-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +The algorithm used when generating Backstage tokens can be configured via `auth.identityTokenAlgorithm`. diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 5eb038c436..9c5249f733 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -10,6 +10,7 @@ import { AuthProviderFactory as AuthProviderFactory_2 } from '@backstage/plugin- import { AuthProviderRouteHandlers as AuthProviderRouteHandlers_2 } from '@backstage/plugin-auth-node'; import { AuthResolverCatalogUserQuery as AuthResolverCatalogUserQuery_2 } from '@backstage/plugin-auth-node'; import { AuthResolverContext as AuthResolverContext_2 } from '@backstage/plugin-auth-node'; +import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageSignInResult } from '@backstage/plugin-auth-node'; import { CacheService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; @@ -51,6 +52,9 @@ export type AuthHandlerResult = { profile: ProfileInfo; }; +// @public +export const authPlugin: () => BackendFeature; + // @public @deprecated (undocumented) export type AuthProviderConfig = AuthProviderConfig_2; @@ -658,6 +662,8 @@ export interface RouterOptions { // (undocumented) database: PluginDatabaseManager; // (undocumented) + disableDefaultProviderFactories?: boolean; + // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) logger: LoggerService; diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 4ebf7c9de1..ce7ebc5204 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -31,6 +31,16 @@ export interface Config { secret?: string; }; + /** + * JWS "alg" (Algorithm) Header Parameter value. Defaults to ES256. + * Must match one of the algorithms defined for IdentityClient. + * When setting a different algorithm, check if the `key` field + * of the `signing_keys` table can fit the length of the generated keys. + * If not, add a knex migration file in the migrations folder. + * More info on supported algorithms: https://github.com/panva/jose + */ + identityTokenAlgorithm?: string; + /** To control how to store JWK data in auth-backend */ keyStore?: { provider?: 'database' | 'memory' | 'firestore'; diff --git a/plugins/auth-backend/dev/index.ts b/plugins/auth-backend/dev/index.ts new file mode 100644 index 0000000000..4d295bdc77 --- /dev/null +++ b/plugins/auth-backend/dev/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2023 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 { createBackend } from '@backstage/backend-defaults'; +import { authPlugin } from '../src'; +import { authModuleGoogleProvider } from '@backstage/plugin-auth-backend-module-google-provider'; + +const backend = createBackend(); + +backend.add(authPlugin); +backend.add(authModuleGoogleProvider); + +backend.start(); diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index c8d1633a47..e1500d6516 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -41,6 +41,7 @@ "@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^", "@backstage/plugin-auth-backend-module-google-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", "@davidzemon/passport-okta-oauth": "^0.0.5", "@google-cloud/firestore": "^6.0.0", @@ -80,6 +81,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/body-parser": "^1.19.0", diff --git a/plugins/auth-backend/src/authPlugin.test.ts b/plugins/auth-backend/src/authPlugin.test.ts new file mode 100644 index 0000000000..d8a15eba3e --- /dev/null +++ b/plugins/auth-backend/src/authPlugin.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2023 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import request from 'supertest'; +import { authPlugin } from './authPlugin'; + +describe('authPlugin', () => { + it('should provide an OpenID configuration', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + }, + }), + ], + }); + + const res = await request(server).get( + '/api/auth/.well-known/openid-configuration', + ); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ + claims_supported: ['sub'], + issuer: `http://localhost:${server.port()}/api/auth`, + }); + }); +}); diff --git a/plugins/auth-backend/src/authPlugin.ts b/plugins/auth-backend/src/authPlugin.ts new file mode 100644 index 0000000000..0522424a99 --- /dev/null +++ b/plugins/auth-backend/src/authPlugin.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2023 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 { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { + AuthProviderFactory, + authProvidersExtensionPoint, +} from '@backstage/plugin-auth-node'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { createRouter } from './service/router'; + +/** + * Auth plugin + * + * @public + */ +export const authPlugin = createBackendPlugin({ + pluginId: 'auth', + register(reg) { + const providers = new Map(); + + reg.registerExtensionPoint(authProvidersExtensionPoint, { + registerProvider({ providerId, factory }) { + if (providers.has(providerId)) { + throw new Error( + `Auth provider '${providerId}' was already registered`, + ); + } + providers.set(providerId, factory); + }, + }); + + reg.registerInit({ + deps: { + httpRouter: coreServices.httpRouter, + logger: coreServices.logger, + config: coreServices.rootConfig, + database: coreServices.database, + discovery: coreServices.discovery, + tokenManager: coreServices.tokenManager, + catalogApi: catalogServiceRef, + }, + async init({ + httpRouter, + logger, + config, + database, + discovery, + tokenManager, + catalogApi, + }) { + const router = await createRouter({ + logger, + config, + database, + discovery, + tokenManager, + catalogApi, + providerFactories: Object.fromEntries(providers), + disableDefaultProviderFactories: true, + }); + httpRouter.use(router); + }, + }); + }, +}); diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index f8a8580359..d8323e02ee 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export { authPlugin } from './authPlugin'; export * from './service/router'; export type { TokenParams } from './identity'; export * from './providers'; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 026a6263b7..e3bfc1c3f2 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -51,6 +51,7 @@ export interface RouterOptions { tokenManager: TokenManager; tokenFactoryAlgorithm?: string; providerFactories?: ProviderFactories; + disableDefaultProviderFactories?: boolean; catalogApi?: CatalogApi; } @@ -65,7 +66,7 @@ export async function createRouter( database, tokenManager, tokenFactoryAlgorithm, - providerFactories, + providerFactories = {}, catalogApi, } = options; const router = Router(); @@ -85,7 +86,9 @@ export async function createRouter( keyStore, keyDurationSeconds, logger: logger.child({ component: 'token-factory' }), - algorithm: tokenFactoryAlgorithm, + algorithm: + tokenFactoryAlgorithm ?? + config.getOptionalString('auth.identityTokenAlgorithm'), }); const secret = config.getOptionalString('auth.session.secret'); @@ -113,19 +116,21 @@ export async function createRouter( router.use(express.urlencoded({ extended: false })); router.use(express.json()); - const allProviderFactories = { - ...defaultAuthProviderFactories, - ...providerFactories, - }; - const providersConfig = config.getConfig('auth.providers'); - const configuredProviders = providersConfig.keys(); + const allProviderFactories = options.disableDefaultProviderFactories + ? providerFactories + : { + ...defaultAuthProviderFactories, + ...providerFactories, + }; + + const providersConfig = config.getOptionalConfig('auth.providers'); const isOriginAllowed = createOriginFilter(config); for (const [providerId, providerFactory] of Object.entries( allProviderFactories, )) { - if (configuredProviders.includes(providerId)) { + if (providersConfig?.has(providerId)) { logger.info(`Configuring auth provider: ${providerId}`); try { const provider = providerFactory({ diff --git a/yarn.lock b/yarn.lock index ec0379375a..1255a13dae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4570,6 +4570,7 @@ __metadata: resolution: "@backstage/plugin-auth-backend@workspace:plugins/auth-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" @@ -4580,6 +4581,7 @@ __metadata: "@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^" "@backstage/plugin-auth-backend-module-google-provider": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" "@backstage/types": "workspace:^" "@davidzemon/passport-okta-oauth": ^0.0.5 "@google-cloud/firestore": ^6.0.0