diff --git a/.changeset/bright-buttons-rescue.md b/.changeset/bright-buttons-rescue.md new file mode 100644 index 0000000000..71a6b76fa2 --- /dev/null +++ b/.changeset/bright-buttons-rescue.md @@ -0,0 +1,27 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +**BREAKING** Added `tokenManager` as a required property for the auth-backend `createRouter` function. This dependency is used to issue server tokens that are used by the `CatalogIdentityClient` when looking up users and their group membership during authentication. + +These changes are **required** to `packages/backend/src/plugins/auth.ts`: + +```diff +export default async function createPlugin({ + logger, + database, + config, + discovery, ++ tokenManager, +}: PluginEnvironment): Promise { + return await createRouter({ + logger, + config, + database, + discovery, ++ tokenManager, + }); +} +``` + +**BREAKING** The `CatalogIdentityClient` constructor now expects a `TokenManager` instead of a `TokenIssuer`. The `TokenManager` interface is used to generate a server token when [resolving a user's identity and membership through the catalog](https://backstage.io/docs/auth/identity-resolver). Using server tokens for these requests allows the auth-backend to bypass authorization checks when permissions are enabled for Backstage. This change will break apps that rely on the user tokens that were previously used by the client. Refer to the ["Backend-to-backend Authentication" tutorial](https://backstage.io/docs/tutorials/backend-to-backend-auth) for more information on server token usage. diff --git a/.changeset/sour-chairs-double.md b/.changeset/sour-chairs-double.md new file mode 100644 index 0000000000..4f481e20e7 --- /dev/null +++ b/.changeset/sour-chairs-double.md @@ -0,0 +1,25 @@ +--- +'@backstage/create-app': patch +--- + +Added `tokenManager` as a required property for the auth-backend `createRouter` function. This dependency is used to issue server tokens that are used by the `CatalogIdentityClient` when looking up users and their group membership during authentication. + +These changes are **required** to `packages/backend/src/plugins/auth.ts`: + +```diff +export default async function createPlugin({ + logger, + database, + config, + discovery, ++ tokenManager, +}: PluginEnvironment): Promise { + return await createRouter({ + logger, + config, + database, + discovery, ++ tokenManager, + }); +} +``` diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 4e51518bc1..7f2b950c3c 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -23,6 +23,13 @@ export default async function createPlugin({ database, config, discovery, + tokenManager, }: PluginEnvironment): Promise { - return await createRouter({ logger, config, database, discovery }); + return await createRouter({ + logger, + config, + database, + discovery, + tokenManager, + }); } diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/auth.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/auth.ts index 52165104a2..015c86466f 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/auth.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/auth.ts @@ -7,6 +7,13 @@ export default async function createPlugin({ database, config, discovery, + tokenManager, }: PluginEnvironment): Promise { - return await createRouter({ logger, config, database, discovery }); + return await createRouter({ + logger, + config, + database, + discovery, + tokenManager, + }); } diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 3490ea317b..7cdc33f0e2 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -15,6 +15,7 @@ import { Logger as Logger_2 } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Profile } from 'passport'; +import { TokenManager } from '@backstage/backend-common'; import { TokenSet } from 'openid-client'; import { UserEntity } from '@backstage/catalog-model'; import { UserinfoResponse } from 'openid-client'; @@ -85,6 +86,7 @@ export type AuthProviderFactoryOptions = { globalConfig: AuthProviderConfig; config: Config; logger: Logger_2; + tokenManager: TokenManager; tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; catalogApi: CatalogApi; @@ -205,7 +207,7 @@ export const bitbucketUsernameSignInResolver: SignInResolver; // Warning: (ae-forgotten-export) The symbol "MemberClaimQuery" needs to be exported by the entry point index.d.ts @@ -671,6 +673,8 @@ export interface RouterOptions { // // (undocumented) providerFactories?: ProviderFactories; + // (undocumented) + tokenManager: TokenManager; } // @public (undocumented) diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 0ddcaa6d2c..897233294f 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ +import { TokenManager } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { RELATION_MEMBER_OF, UserEntity, UserEntityV1alpha1, } from '@backstage/catalog-model'; -import { TokenIssuer } from '../../identity'; import { CatalogIdentityClient } from './CatalogIdentityClient'; describe('CatalogIdentityClient', () => { @@ -36,19 +36,19 @@ describe('CatalogIdentityClient', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), }; - const tokenIssuer: jest.Mocked = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), + const tokenManager: jest.Mocked = { + getToken: jest.fn(), + authenticate: jest.fn(), }; afterEach(() => jest.resetAllMocks()); it('findUser passes through the correct search params', async () => { catalogApi.getEntities.mockResolvedValueOnce({ items: [{} as UserEntity] }); - tokenIssuer.issueToken.mockResolvedValue('my-token'); + tokenManager.getToken.mockResolvedValue({ token: 'my-token' }); const client = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); await client.findUser({ annotations: { key: 'value' } }); @@ -62,11 +62,7 @@ describe('CatalogIdentityClient', () => { }, { token: 'my-token' }, ); - expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ - claims: { - sub: 'backstage.io/auth-backend', - }, - }); + expect(tokenManager.getToken).toHaveBeenCalledWith(); }); it('resolveCatalogMembership resolves membership', async () => { @@ -114,35 +110,39 @@ describe('CatalogIdentityClient', () => { }, ]; catalogApi.getEntities.mockResolvedValueOnce({ items: mockUsers }); + tokenManager.getToken.mockResolvedValue({ token: 'my-token' }); const client = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const claims = await client.resolveCatalogMembership({ entityRefs: ['inigom', 'User:default/imontoya', 'User:reality/mpatinkin'], }); - expect(catalogApi.getEntities).toHaveBeenCalledWith({ - filter: [ - { - kind: 'user', - 'metadata.namespace': 'default', - 'metadata.name': 'inigom', - }, - { - kind: 'user', - 'metadata.namespace': 'default', - 'metadata.name': 'imontoya', - }, - { - kind: 'user', - 'metadata.namespace': 'reality', - 'metadata.name': 'mpatinkin', - }, - ], - }); + expect(catalogApi.getEntities).toHaveBeenCalledWith( + { + filter: [ + { + kind: 'user', + 'metadata.namespace': 'default', + 'metadata.name': 'inigom', + }, + { + kind: 'user', + 'metadata.namespace': 'default', + 'metadata.name': 'imontoya', + }, + { + kind: 'user', + 'metadata.namespace': 'reality', + 'metadata.name': 'mpatinkin', + }, + ], + }, + { token: 'my-token' }, + ); expect(claims).toMatchObject([ 'user:default/inigom', diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index a1f915d152..aecf018975 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -24,7 +24,7 @@ import { stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; -import { TokenIssuer } from '../../identity'; +import { TokenManager } from '@backstage/backend-common'; type UserQuery = { annotations: Record; @@ -40,11 +40,11 @@ type MemberClaimQuery = { */ export class CatalogIdentityClient { private readonly catalogApi: CatalogApi; - private readonly tokenIssuer: TokenIssuer; + private readonly tokenManager: TokenManager; - constructor(options: { catalogApi: CatalogApi; tokenIssuer: TokenIssuer }) { + constructor(options: { catalogApi: CatalogApi; tokenManager: TokenManager }) { this.catalogApi = options.catalogApi; - this.tokenIssuer = options.tokenIssuer; + this.tokenManager = options.tokenManager; } /** @@ -60,10 +60,7 @@ export class CatalogIdentityClient { filter[`metadata.annotations.${key}`] = value; } - // TODO(Rugvip): cache the token - const token = await this.tokenIssuer.issueToken({ - claims: { sub: 'backstage.io/auth-backend' }, - }); + const { token } = await this.tokenManager.getToken(); const { items } = await this.catalogApi.getEntities({ filter }, { token }); if (items.length !== 1) { @@ -106,8 +103,9 @@ export class CatalogIdentityClient { 'metadata.namespace': ref.namespace, 'metadata.name': ref.name, })); + const { token } = await this.tokenManager.getToken(); const entities = await this.catalogApi - .getEntities({ filter }) + .getEntities({ filter }, { token }) .then(r => r.items); if (entityRefs.length !== entities.length) { diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index 9987a88407..2696fd233f 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -197,6 +197,7 @@ export const createAtlassianProvider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => @@ -208,7 +209,7 @@ export const createAtlassianProvider = ( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index dbd6924804..15578117ea 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -220,6 +220,7 @@ export const createAuth0Provider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => @@ -231,7 +232,7 @@ export const createAuth0Provider = ( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = options?.authHandler diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index cc111b4aaf..12f7c7f4b4 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -241,7 +241,7 @@ export type AwsAlbProviderOptions = { export const createAwsAlbProvider = ( options?: AwsAlbProviderOptions, ): AuthProviderFactory => { - return ({ config, tokenIssuer, catalogApi, logger }) => { + return ({ config, tokenIssuer, catalogApi, logger, tokenManager }) => { const region = config.getString('region'); const issuer = config.getOptionalString('iss'); @@ -253,7 +253,7 @@ export const createAwsAlbProvider = ( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = options?.authHandler diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index ff203e0a9d..4d5bdcf6f6 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -273,6 +273,7 @@ export const createBitbucketProvider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => @@ -283,7 +284,7 @@ export const createBitbucketProvider = ( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index a54c7e9fb2..7816f68b76 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -102,7 +102,7 @@ export class GcpIapProvider implements AuthProviderRouteHandlers { export function createGcpIapProvider( options: GcpIapProviderOptions, ): AuthProviderFactory { - return ({ config, tokenIssuer, catalogApi, logger }) => { + return ({ config, tokenIssuer, catalogApi, logger, tokenManager }) => { const audience = config.getString('audience'); const authHandler = options.authHandler ?? defaultAuthHandler; @@ -111,7 +111,7 @@ export function createGcpIapProvider( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); return new GcpIapProvider({ diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index bef1a87b0a..9dc70cf60e 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -245,6 +245,7 @@ export const createGithubProvider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => @@ -270,7 +271,7 @@ export const createGithubProvider = ( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = options?.authHandler diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 5454065847..da816f2fd2 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -227,6 +227,7 @@ export const createGitlabProvider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => @@ -239,7 +240,7 @@ export const createGitlabProvider = ( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 44c22f757e..13c5093aa5 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -259,6 +259,7 @@ export const createGoogleProvider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => @@ -269,7 +270,7 @@ export const createGoogleProvider = ( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = options?.authHandler diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index b4ef30f491..9249f643dc 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -267,6 +267,7 @@ export const createMicrosoftProvider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => @@ -281,7 +282,7 @@ export const createMicrosoftProvider = ( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = options?.authHandler diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts index eceebc5924..24bb4d0362 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts @@ -182,12 +182,12 @@ export const createOauth2ProxyProvider = ( options: Oauth2ProxyProviderOptions, ): AuthProviderFactory => - ({ catalogApi, logger, tokenIssuer }) => { + ({ catalogApi, logger, tokenIssuer, tokenManager }) => { const signInResolver = options.signIn.resolver; const authHandler = options.authHandler; const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); return new Oauth2ProxyAuthProvider({ logger, diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index ec777c231b..8e69a3ee1d 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -233,6 +233,7 @@ export const createOAuth2Provider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => @@ -249,7 +250,7 @@ export const createOAuth2Provider = ( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = options?.authHandler diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 0c820648e4..fa46294784 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -257,6 +257,7 @@ export const createOidcProvider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => @@ -272,7 +273,7 @@ export const createOidcProvider = ( const prompt = envConfig.getOptionalString('prompt'); const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = options?.authHandler diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 30dd295d12..def0da694e 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -268,6 +268,7 @@ export const createOktaProvider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => @@ -286,7 +287,7 @@ export const createOktaProvider = ( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = _options?.authHandler diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index ea84b402d5..a5ab4f658b 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -219,6 +219,7 @@ export const createOneLoginProvider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => @@ -230,7 +231,7 @@ export const createOneLoginProvider = ( const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = options?.authHandler diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index b9c8d89f5b..1ca8cf0d5d 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -187,12 +187,13 @@ export const createSamlProvider = ( globalConfig, config, tokenIssuer, + tokenManager, catalogApi, logger, }) => { const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, - tokenIssuer, + tokenManager, }); const authHandler: AuthHandler = options?.authHandler diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 179d536f3c..b02e8d74b8 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; @@ -124,6 +127,7 @@ export type AuthProviderFactoryOptions = { globalConfig: AuthProviderConfig; config: Config; logger: Logger; + tokenManager: TokenManager; tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; catalogApi: CatalogApi; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 6fdd768498..bdb68929b1 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -25,6 +25,7 @@ import { import { PluginDatabaseManager, PluginEndpointDiscovery, + TokenManager, } from '@backstage/backend-common'; import { assertError, NotFoundError } from '@backstage/errors'; import { CatalogClient } from '@backstage/catalog-client'; @@ -41,13 +42,21 @@ export interface RouterOptions { database: PluginDatabaseManager; config: Config; discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; providerFactories?: ProviderFactories; } export async function createRouter( options: RouterOptions, ): Promise { - const { logger, config, discovery, database, providerFactories } = options; + const { + logger, + config, + discovery, + database, + tokenManager, + providerFactories, + } = options; const router = Router(); const appUrl = config.getString('app.baseUrl'); @@ -105,6 +114,7 @@ export async function createRouter( globalConfig: { baseUrl: authUrl, appUrl, isOriginAllowed }, config: providersConfig.getConfig(providerId), logger, + tokenManager, tokenIssuer, discovery, catalogApi, diff --git a/plugins/auth-backend/src/service/standaloneServer.ts b/plugins/auth-backend/src/service/standaloneServer.ts index 15ffe1d053..4e416e8dc7 100644 --- a/plugins/auth-backend/src/service/standaloneServer.ts +++ b/plugins/auth-backend/src/service/standaloneServer.ts @@ -17,6 +17,7 @@ import { createServiceBuilder, loadBackendConfig, + ServerTokenManager, SingleHostDiscovery, useHotMemoize, } from '@backstage/backend-common'; @@ -58,6 +59,7 @@ export async function startStandaloneServer( }, }, discovery, + tokenManager: ServerTokenManager.noop(), }); const service = createServiceBuilder(module)