diff --git a/.changeset/giant-nails-grow.md b/.changeset/giant-nails-grow.md index c715f78d5c..bed8c422aa 100644 --- a/.changeset/giant-nails-grow.md +++ b/.changeset/giant-nails-grow.md @@ -2,4 +2,10 @@ '@backstage/plugin-auth-backend': minor --- -Made `IdentityClient.listPublicKeys` private. It was only used in tests, and should not be part of the API surface of that class. The interface is marked as experimental, and therefore this is a breaking change without a deprecation period. +- Made `IdentityClient.listPublicKeys` private. It was only used in tests, and + should not be part of the API surface of that class. +- Removed the static `IdentityClient.getBearerToken`. It is now replaced by + `getBearerTokenFromAuthorizationHeader` from `@backstage/plugin-auth-node`. + +Since the `IdentityClient` interface is marked as experimental, this is a +breaking change without a deprecation period. diff --git a/.changeset/little-onions-fly.md b/.changeset/little-onions-fly.md new file mode 100644 index 0000000000..8bff96d866 --- /dev/null +++ b/.changeset/little-onions-fly.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-search-backend': patch +--- + +Use `getBearerTokenFromAuthorizationHeader` from `@backstage/plugin-auth-node` instead of the deprecated `IdentityClient` method. diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 09f1c5185e..6a77f1a7cf 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -428,9 +428,6 @@ export type GoogleProviderOptions = { export class IdentityClient { constructor(options: { discovery: PluginEndpointDiscovery; issuer: string }); authenticate(token: string | undefined): Promise; - static getBearerToken( - authorizationHeader: string | undefined, - ): string | undefined; } // Warning: (ae-missing-release-tag) "microsoftEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 3dab33d8da..4f2ef4e76c 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -30,6 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/plugin-auth-node": "^0.0.0", "@backstage/backend-common": "^0.10.7-next.0", "@backstage/catalog-client": "^0.5.5", "@backstage/catalog-model": "^0.9.10", diff --git a/plugins/auth-backend/src/identity/IdentityClient.test.ts b/plugins/auth-backend/src/identity/IdentityClient.test.ts index da7a8e7dce..cf60fd0852 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.test.ts +++ b/plugins/auth-backend/src/identity/IdentityClient.test.ts @@ -199,38 +199,6 @@ describe('IdentityClient', () => { }); }); - describe('getBearerToken', () => { - it('should return undefined on undefined input', async () => { - const token = IdentityClient.getBearerToken(undefined); - expect(token).toBeUndefined(); - }); - - it('should return undefined on malformed input', async () => { - const token = IdentityClient.getBearerToken('malformed'); - expect(token).toBeUndefined(); - }); - - it('should return undefined on unexpected scheme', async () => { - const token = IdentityClient.getBearerToken('Basic token'); - expect(token).toBeUndefined(); - }); - - it('should return Bearer token', async () => { - const token = IdentityClient.getBearerToken('Bearer token'); - expect(token).toEqual('token'); - }); - - it('should return Bearer token despite extra space', async () => { - const token = IdentityClient.getBearerToken('Bearer \n token '); - expect(token).toEqual('token'); - }); - - it('should return Bearer token despite unconventionial case', async () => { - const token = IdentityClient.getBearerToken('bEARER token'); - expect(token).toEqual('token'); - }); - }); - describe('listPublicKeys', () => { const defaultServiceResponse: { keys: JSONWebKey[]; diff --git a/plugins/auth-backend/src/identity/IdentityClient.ts b/plugins/auth-backend/src/identity/IdentityClient.ts index 810ae56722..5fbd126cdf 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.ts +++ b/plugins/auth-backend/src/identity/IdentityClient.ts @@ -84,20 +84,6 @@ export class IdentityClient { return user; } - /** - * Parses the given authorization header and returns - * the bearer token, or null if no bearer token is given - */ - static getBearerToken( - authorizationHeader: string | undefined, - ): string | undefined { - if (typeof authorizationHeader !== 'string') { - return undefined; - } - const matches = authorizationHeader.match(/Bearer\s+(\S+)/i); - return matches?.[1]; - } - /** * Returns the public signing key matching the given jwt token, * or null if no matching key was found diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts index 24bb4d0362..8c4dcc3249 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts @@ -17,6 +17,7 @@ import express from 'express'; import { Logger } from 'winston'; import { AuthenticationError } from '@backstage/errors'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { AuthHandler, SignInResolver, @@ -26,7 +27,6 @@ import { } from '../types'; import { CatalogIdentityClient } from '../../lib/catalog'; import { JWT } from 'jose'; -import { IdentityClient } from '../../identity'; import { TokenIssuer } from '../../identity/types'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; @@ -156,7 +156,7 @@ export class Oauth2ProxyAuthProvider private getResult(req: express.Request): OAuth2ProxyResult { const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER); - const jwt = IdentityClient.getBearerToken(authHeader); + const jwt = getBearerTokenFromAuthorizationHeader(authHeader); if (!jwt) { throw new AuthenticationError( diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md index bbbf455b18..e32a35880b 100644 --- a/plugins/auth-node/api-report.md +++ b/plugins/auth-node/api-report.md @@ -4,5 +4,7 @@ ```ts // @public -export const COMMON_CONSTANT = 1; +export function getBearerTokenFromAuthorizationHeader( + authorizationHeader: unknown, +): string | undefined; ``` diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 3231c6993e..7e4b4bfe77 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -19,12 +19,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.6", + "@backstage/backend-common": "^0.10.7-next.0", "@backstage/config": "^0.1.13", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.1" + "@backstage/cli": "^0.13.2-next.0" }, "files": [ "dist" diff --git a/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts new file mode 100644 index 0000000000..0bd7b5ea93 --- /dev/null +++ b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2022 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 { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader'; + +describe('getBearerToken', () => { + it('should return undefined on bad input', async () => { + expect(getBearerTokenFromAuthorizationHeader(undefined)).toBeUndefined(); + expect(getBearerTokenFromAuthorizationHeader(7)).toBeUndefined(); + expect( + getBearerTokenFromAuthorizationHeader('Bearer \n token'), + ).toBeUndefined(); + expect( + getBearerTokenFromAuthorizationHeader('Bearer token '), + ).toBeUndefined(); + }); + + it('should return undefined on malformed input', async () => { + const token = getBearerTokenFromAuthorizationHeader('malformed'); + expect(token).toBeUndefined(); + }); + + it('should return undefined on unexpected scheme', async () => { + const token = getBearerTokenFromAuthorizationHeader('Basic token'); + expect(token).toBeUndefined(); + }); + + it('should return Bearer token', async () => { + const token = getBearerTokenFromAuthorizationHeader('Bearer token'); + expect(token).toEqual('token'); + }); + + it('should return Bearer token despite unconventional case', async () => { + const token = getBearerTokenFromAuthorizationHeader('bEARER token'); + expect(token).toEqual('token'); + }); +}); diff --git a/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts new file mode 100644 index 0000000000..8451cd6ab1 --- /dev/null +++ b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2022 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. + */ + +/** + * Parses the given authorization header and returns the bearer token, or + * undefined if no bearer token is given. + * + * @remarks + * + * This function is explicitly built to tolerate bad inputs safely, so you may + * call it directly with e.g. the output of `req.header('authorization')` + * without first checking that it exists. + * + * @public + */ +export function getBearerTokenFromAuthorizationHeader( + authorizationHeader: unknown, +): string | undefined { + if (typeof authorizationHeader !== 'string') { + return undefined; + } + const matches = authorizationHeader.match(/^Bearer[ ]+(\S+)$/i); + return matches?.[1]; +} diff --git a/plugins/auth-node/src/index.ts b/plugins/auth-node/src/index.ts index 04d073a748..83037f75be 100644 --- a/plugins/auth-node/src/index.ts +++ b/plugins/auth-node/src/index.ts @@ -20,9 +20,4 @@ * @packageDocumentation */ -/** - * Dummy. - * - * @public - */ -export const COMMON_CONSTANT = 1; +export { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader'; diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index bc6faae17b..9ee959243d 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -23,6 +23,7 @@ "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", "@backstage/plugin-auth-backend": "^0.10.0-next.0", + "@backstage/plugin-auth-node": "^0.0.0", "@backstage/plugin-permission-common": "^0.4.0", "@backstage/plugin-permission-node": "^0.4.3-next.0", "@types/express": "*", diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index e22aec8129..10a70b01db 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -27,6 +27,7 @@ import { BackstageIdentityResponse, IdentityClient, } from '@backstage/plugin-auth-backend'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { AuthorizeResult, AuthorizeDecision, @@ -157,7 +158,9 @@ export async function createRouter( req: Request, res: Response, ) => { - const token = IdentityClient.getBearerToken(req.header('authorization')); + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); const user = token ? await identity.authenticate(token) : undefined; const parseResult = requestSchema.safeParse(req.body); diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index ee064851fa..3770dbd875 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -24,7 +24,7 @@ "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", "@backstage/search-common": "^0.2.2", - "@backstage/plugin-auth-backend": "^0.10.0-next.0", + "@backstage/plugin-auth-node": "^0.0.0", "@backstage/plugin-permission-common": "^0.4.0-next.0", "@backstage/plugin-permission-node": "^0.4.3-next.0", "@backstage/plugin-search-backend-node": "^0.4.5", diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 28a3247919..3f25c4f24a 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -22,7 +22,7 @@ import { errorHandler } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; import { JsonObject, JsonValue } from '@backstage/types'; -import { IdentityClient } from '@backstage/plugin-auth-backend'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { DocumentTypeInfo, SearchResultSet } from '@backstage/search-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; @@ -106,7 +106,9 @@ export async function createRouter( }, pageCursor=${query.pageCursor ?? ''}`, ); - const token = IdentityClient.getBearerToken(req.header('authorization')); + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); try { const resultSet = await engine?.query(query, { token });