From 97c6837ef11210a9bf6c86720311b54e76f1bb40 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 20 Nov 2024 13:17:44 +0100 Subject: [PATCH 1/5] backend-defaults: export DefaultHttpAuthService Signed-off-by: Johan Haals --- .changeset/empty-bugs-deliver.md | 5 ++ packages/backend-defaults/package.json | 1 + .../backend-defaults/report-httpAuth.api.md | 41 +++++++++++++++++ .../httpAuth/DefaultHttpAuthService.test.ts | 46 +++++++++++++++++++ .../httpAuth/httpAuthServiceFactory.ts | 46 ++++++++++++++++--- .../src/entrypoints/httpAuth/index.ts | 6 ++- yarn.lock | 1 + 7 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 .changeset/empty-bugs-deliver.md create mode 100644 packages/backend-defaults/src/entrypoints/httpAuth/DefaultHttpAuthService.test.ts diff --git a/.changeset/empty-bugs-deliver.md b/.changeset/empty-bugs-deliver.md new file mode 100644 index 0000000000..5b26f4eaca --- /dev/null +++ b/.changeset/empty-bugs-deliver.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Export `DefaultHttpAuthService` to allow for custom token extraction logic. diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 73f90fd153..d57692de90 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -201,6 +201,7 @@ "aws-sdk-client-mock": "^4.0.0", "http-errors": "^2.0.0", "msw": "^1.0.0", + "node-mocks-http": "^1.0.0", "supertest": "^7.0.0", "wait-for-expect": "^3.0.2" }, diff --git a/packages/backend-defaults/report-httpAuth.api.md b/packages/backend-defaults/report-httpAuth.api.md index 6c08a6f4d0..4aa492bac3 100644 --- a/packages/backend-defaults/report-httpAuth.api.md +++ b/packages/backend-defaults/report-httpAuth.api.md @@ -3,9 +3,50 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { BackstagePrincipalTypes } from '@backstage/backend-plugin-api'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import { HttpAuthService } from '@backstage/backend-plugin-api'; +import { Request as Request_2 } from 'express'; +import { Response as Response_2 } from 'express'; import { ServiceFactory } from '@backstage/backend-plugin-api'; +// @public +export class DefaultHttpAuthService implements HttpAuthService { + // (undocumented) + static create(options: DefaultHttpAuthServiceOptions): DefaultHttpAuthService; + // (undocumented) + credentials( + req: Request_2, + options?: { + allow?: Array; + allowLimitedAccess?: boolean; + }, + ): Promise>; + // (undocumented) + issueUserCookie( + res: Response_2, + options?: { + credentials?: BackstageCredentials; + }, + ): Promise<{ + expiresAt: Date; + }>; +} + +// @public +export interface DefaultHttpAuthServiceOptions { + // (undocumented) + auth: AuthService; + // (undocumented) + discovery: DiscoveryService; + // (undocumented) + extractTokenFromRequest?: (req: Request_2) => string | undefined; + // (undocumented) + pluginId: string; +} + // @public export const httpAuthServiceFactory: ServiceFactory< HttpAuthService, diff --git a/packages/backend-defaults/src/entrypoints/httpAuth/DefaultHttpAuthService.test.ts b/packages/backend-defaults/src/entrypoints/httpAuth/DefaultHttpAuthService.test.ts new file mode 100644 index 0000000000..bbcd77795e --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/httpAuth/DefaultHttpAuthService.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { DefaultHttpAuthService } from './httpAuthServiceFactory'; +import { mockServices } from '@backstage/backend-test-utils'; +import { createRequest } from 'node-mocks-http'; + +describe('DefaultHttpAuthService', () => { + it('should extract token from custom header', async () => { + const auth = mockServices.auth.mock(); + const httpAuthService = DefaultHttpAuthService.create({ + discovery: mockServices.discovery(), + auth, + pluginId: 'test', + extractTokenFromRequest: req => { + const authHeader = req.headers.test; + if (typeof authHeader === 'string') { + const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i); + const token = matches?.[1]; + if (token) { + return token; + } + } + + return undefined; + }, + }); + await httpAuthService.credentials( + createRequest({ headers: { test: 'Bearer mock-user-token' } }), + ); + expect(auth.authenticate).toHaveBeenCalledWith('mock-user-token'); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts b/packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts index 76550873f6..4259a604c4 100644 --- a/packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts @@ -33,7 +33,6 @@ const FIVE_MINUTES_MS = 5 * 60 * 1000; const BACKSTAGE_AUTH_COOKIE = 'backstage-auth'; function getTokenFromRequest(req: Request) { - // TODO: support multiple auth headers (iterate rawHeaders) const authHeader = req.headers.authorization; if (typeof authHeader === 'string') { const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i); @@ -71,23 +70,54 @@ type RequestWithCredentials = Request & { [limitedCredentialsSymbol]?: Promise; }; -class DefaultHttpAuthService implements HttpAuthService { +/** + * @public + * Options for creating a DefaultHttpAuthService. + */ +export interface DefaultHttpAuthServiceOptions { + auth: AuthService; + discovery: DiscoveryService; + pluginId: string; + // Optionally override the default token extraction logic + extractTokenFromRequest?: (req: Request) => string | undefined; +} + +/** + * @public + * DefaultHttpAuthService is the default implementation of the HttpAuthService + */ +export class DefaultHttpAuthService implements HttpAuthService { readonly #auth: AuthService; readonly #discovery: DiscoveryService; readonly #pluginId: string; + readonly #extractTokenFromRequest: (req: Request) => string | undefined; - constructor( + private constructor( auth: AuthService, discovery: DiscoveryService, pluginId: string, + extractTokenFromRequest?: (req: Request) => string | undefined, ) { this.#auth = auth; this.#discovery = discovery; this.#pluginId = pluginId; + this.#extractTokenFromRequest = + extractTokenFromRequest ?? getTokenFromRequest; + } + + static create( + options: DefaultHttpAuthServiceOptions, + ): DefaultHttpAuthService { + return new DefaultHttpAuthService( + options.auth, + options.discovery, + options.pluginId, + options.extractTokenFromRequest, + ); } async #extractCredentialsFromRequest(req: Request) { - const token = getTokenFromRequest(req); + const token = this.#extractTokenFromRequest(req); if (!token) { return await this.#auth.getNoneCredentials(); } @@ -96,7 +126,7 @@ class DefaultHttpAuthService implements HttpAuthService { } async #extractLimitedCredentialsFromRequest(req: Request) { - const token = getTokenFromRequest(req); + const token = this.#extractTokenFromRequest(req); if (token) { return await this.#auth.authenticate(token, { allowLimitedAccess: true, @@ -289,6 +319,10 @@ export const httpAuthServiceFactory = createServiceFactory({ plugin: coreServices.pluginMetadata, }, async factory({ auth, discovery, plugin }) { - return new DefaultHttpAuthService(auth, discovery, plugin.getId()); + return DefaultHttpAuthService.create({ + auth, + discovery, + pluginId: plugin.getId(), + }); }, }); diff --git a/packages/backend-defaults/src/entrypoints/httpAuth/index.ts b/packages/backend-defaults/src/entrypoints/httpAuth/index.ts index edd7e53026..9afe06ae0e 100644 --- a/packages/backend-defaults/src/entrypoints/httpAuth/index.ts +++ b/packages/backend-defaults/src/entrypoints/httpAuth/index.ts @@ -14,4 +14,8 @@ * limitations under the License. */ -export { httpAuthServiceFactory } from './httpAuthServiceFactory'; +export { + httpAuthServiceFactory, + DefaultHttpAuthService, +} from './httpAuthServiceFactory'; +export type { DefaultHttpAuthServiceOptions } from './httpAuthServiceFactory'; diff --git a/yarn.lock b/yarn.lock index 738da2d156..18ad296c19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3645,6 +3645,7 @@ __metadata: mysql2: ^3.0.0 node-fetch: ^2.7.0 node-forge: ^1.3.1 + node-mocks-http: ^1.0.0 p-limit: ^3.1.0 path-to-regexp: ^8.0.0 pg: ^8.11.3 From 0f848209959cd981c0e98c434d5cf3bc139d056f Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 20 Nov 2024 14:06:18 +0100 Subject: [PATCH 2/5] Make return type an object. Improve naming Signed-off-by: Johan Haals --- .../backend-defaults/report-httpAuth.api.md | 5 ++-- .../httpAuth/DefaultHttpAuthService.test.ts | 18 +++++-------- .../httpAuth/httpAuthServiceFactory.ts | 27 +++++++++---------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/backend-defaults/report-httpAuth.api.md b/packages/backend-defaults/report-httpAuth.api.md index 4aa492bac3..b3843beb3d 100644 --- a/packages/backend-defaults/report-httpAuth.api.md +++ b/packages/backend-defaults/report-httpAuth.api.md @@ -41,8 +41,9 @@ export interface DefaultHttpAuthServiceOptions { auth: AuthService; // (undocumented) discovery: DiscoveryService; - // (undocumented) - extractTokenFromRequest?: (req: Request_2) => string | undefined; + getTokenFromRequest?: (req: Request_2) => { + token?: string; + }; // (undocumented) pluginId: string; } diff --git a/packages/backend-defaults/src/entrypoints/httpAuth/DefaultHttpAuthService.test.ts b/packages/backend-defaults/src/entrypoints/httpAuth/DefaultHttpAuthService.test.ts index bbcd77795e..e314564b59 100644 --- a/packages/backend-defaults/src/entrypoints/httpAuth/DefaultHttpAuthService.test.ts +++ b/packages/backend-defaults/src/entrypoints/httpAuth/DefaultHttpAuthService.test.ts @@ -25,21 +25,17 @@ describe('DefaultHttpAuthService', () => { discovery: mockServices.discovery(), auth, pluginId: 'test', - extractTokenFromRequest: req => { - const authHeader = req.headers.test; - if (typeof authHeader === 'string') { - const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i); - const token = matches?.[1]; - if (token) { - return token; - } + getTokenFromRequest: req => { + let token: string | undefined; + const header = req.headers.test; + if (typeof header === 'string') { + token = header; } - - return undefined; + return { token }; }, }); await httpAuthService.credentials( - createRequest({ headers: { test: 'Bearer mock-user-token' } }), + createRequest({ headers: { test: 'mock-user-token' } }), ); expect(auth.authenticate).toHaveBeenCalledWith('mock-user-token'); }); diff --git a/packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts b/packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts index 4259a604c4..4bc6b3b1fc 100644 --- a/packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts @@ -33,16 +33,14 @@ const FIVE_MINUTES_MS = 5 * 60 * 1000; const BACKSTAGE_AUTH_COOKIE = 'backstage-auth'; function getTokenFromRequest(req: Request) { + let token: string | undefined; const authHeader = req.headers.authorization; if (typeof authHeader === 'string') { const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i); - const token = matches?.[1]; - if (token) { - return token; - } + token = matches?.[1]; } - return undefined; + return { token }; } function getCookieFromRequest(req: Request) { @@ -78,8 +76,10 @@ export interface DefaultHttpAuthServiceOptions { auth: AuthService; discovery: DiscoveryService; pluginId: string; - // Optionally override the default token extraction logic - extractTokenFromRequest?: (req: Request) => string | undefined; + /** + * Optionally override logic for extracting the token from the request. + */ + getTokenFromRequest?: (req: Request) => { token?: string }; } /** @@ -90,19 +90,18 @@ export class DefaultHttpAuthService implements HttpAuthService { readonly #auth: AuthService; readonly #discovery: DiscoveryService; readonly #pluginId: string; - readonly #extractTokenFromRequest: (req: Request) => string | undefined; + readonly #getToken: (req: Request) => { token?: string }; private constructor( auth: AuthService, discovery: DiscoveryService, pluginId: string, - extractTokenFromRequest?: (req: Request) => string | undefined, + getToken?: (req: Request) => { token?: string }, ) { this.#auth = auth; this.#discovery = discovery; this.#pluginId = pluginId; - this.#extractTokenFromRequest = - extractTokenFromRequest ?? getTokenFromRequest; + this.#getToken = getToken ?? getTokenFromRequest; } static create( @@ -112,12 +111,12 @@ export class DefaultHttpAuthService implements HttpAuthService { options.auth, options.discovery, options.pluginId, - options.extractTokenFromRequest, + options.getTokenFromRequest, ); } async #extractCredentialsFromRequest(req: Request) { - const token = this.#extractTokenFromRequest(req); + const { token } = this.#getToken(req); if (!token) { return await this.#auth.getNoneCredentials(); } @@ -126,7 +125,7 @@ export class DefaultHttpAuthService implements HttpAuthService { } async #extractLimitedCredentialsFromRequest(req: Request) { - const token = this.#extractTokenFromRequest(req); + const { token } = this.#getToken(req); if (token) { return await this.#auth.authenticate(token, { allowLimitedAccess: true, From 866ce05cc75e05eac210117215ee6a9bfc067115 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 22 Nov 2024 11:09:16 +0100 Subject: [PATCH 3/5] add docs Signed-off-by: Johan Haals --- .../backend-system/core-services/http-auth.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/backend-system/core-services/http-auth.md b/docs/backend-system/core-services/http-auth.md index 13938c921d..3936670b77 100644 --- a/docs/backend-system/core-services/http-auth.md +++ b/docs/backend-system/core-services/http-auth.md @@ -102,6 +102,45 @@ or [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING. ::: +### Custom token extraction logic + +In some cases, you might want to customize how tokens are extracted from incoming requests. It might be that you want to extract tokens from a different location in the request. To support this you supply your own slightly modified httpAuth service. The `DefaultHttpAuthService` class is exported from the `@backstage/backend-defaults` package and it's `create` method can be used to pass in a custom `getTokenFromRequest` extraction function. + +```ts +import { DefaultHttpAuthService } from '@backstage/backend-defaults/httpAuth'; +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; + +export const customizedAuthServiceFactory = createServiceFactory({ + service: coreServices.httpAuth, + deps: { + auth: coreServices.auth, + discovery: coreServices.discovery, + plugin: coreServices.pluginMetadata, + }, + async factory({ auth, discovery, plugin }) { + return DefaultHttpAuthService.create({ + auth, + discovery, + pluginId: plugin.getId(), + getTokenFromRequest: req => { + let token: string | undefined; + const header = req.headers.some_random_header; + if (typeof header === 'string') { + const parts = header.split(' '); + if (parts.length === 2 && parts[0] === 'Bearer') { + token = parts[1]; + } + } + return { token }; + }, + }); + }, +}); +``` + This service has no configuration options, but it abides by the policies you have set up using [the `httpRouter` service](./http-router.md) for your routes, if any. From 5aad192efa1d553052f9279869485b6820cd1473 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 22 Nov 2024 13:52:19 +0100 Subject: [PATCH 4/5] Update docs/backend-system/core-services/http-auth.md Signed-off-by: Johan Haals Co-authored-by: Peter Macdonald Signed-off-by: Johan Haals --- docs/backend-system/core-services/http-auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backend-system/core-services/http-auth.md b/docs/backend-system/core-services/http-auth.md index 3936670b77..7154bac325 100644 --- a/docs/backend-system/core-services/http-auth.md +++ b/docs/backend-system/core-services/http-auth.md @@ -104,7 +104,7 @@ or [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING. ### Custom token extraction logic -In some cases, you might want to customize how tokens are extracted from incoming requests. It might be that you want to extract tokens from a different location in the request. To support this you supply your own slightly modified httpAuth service. The `DefaultHttpAuthService` class is exported from the `@backstage/backend-defaults` package and it's `create` method can be used to pass in a custom `getTokenFromRequest` extraction function. +In some cases, you might want to customize how tokens are extracted from incoming requests. It might be that you want to extract tokens from a different location in the request. To support this you can supply your own slightly modified httpAuth service. The `DefaultHttpAuthService` class is exported from the `@backstage/backend-defaults` package and it's `create` method can be used to pass in a custom `getTokenFromRequest` extraction function. ```ts import { DefaultHttpAuthService } from '@backstage/backend-defaults/httpAuth'; From 2c122e4280bab6a2293bbb3349a9923e1be1c6e5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 22 Nov 2024 14:03:27 +0100 Subject: [PATCH 5/5] Update docs/backend-system/core-services/http-auth.md Signed-off-by: Patrik Oldsberg --- docs/backend-system/core-services/http-auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backend-system/core-services/http-auth.md b/docs/backend-system/core-services/http-auth.md index 7154bac325..f3ce96fb25 100644 --- a/docs/backend-system/core-services/http-auth.md +++ b/docs/backend-system/core-services/http-auth.md @@ -104,7 +104,7 @@ or [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING. ### Custom token extraction logic -In some cases, you might want to customize how tokens are extracted from incoming requests. It might be that you want to extract tokens from a different location in the request. To support this you can supply your own slightly modified httpAuth service. The `DefaultHttpAuthService` class is exported from the `@backstage/backend-defaults` package and it's `create` method can be used to pass in a custom `getTokenFromRequest` extraction function. +In some cases, you might want to customize how tokens are extracted from incoming requests. It might be that you want to extract tokens from a different location in the request. To support this you can supply your own slightly modified httpAuth service. The `DefaultHttpAuthService` class is exported from `@backstage/backend-defaults/httpAuth` and it's static `create` method can be used to pass in a custom `getTokenFromRequest` extraction function. ```ts import { DefaultHttpAuthService } from '@backstage/backend-defaults/httpAuth';