backend-defaults: export DefaultHttpAuthService

Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
Johan Haals
2024-11-20 13:17:44 +01:00
parent d589a90b10
commit 97c6837ef1
7 changed files with 139 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
Export `DefaultHttpAuthService` to allow for custom token extraction logic.
+1
View File
@@ -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"
},
@@ -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<TAllowed extends keyof BackstagePrincipalTypes = 'unknown'>(
req: Request_2,
options?: {
allow?: Array<TAllowed>;
allowLimitedAccess?: boolean;
},
): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>>;
// (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,
@@ -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');
});
});
@@ -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<BackstageCredentials>;
};
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(),
});
},
});
@@ -14,4 +14,8 @@
* limitations under the License.
*/
export { httpAuthServiceFactory } from './httpAuthServiceFactory';
export {
httpAuthServiceFactory,
DefaultHttpAuthService,
} from './httpAuthServiceFactory';
export type { DefaultHttpAuthServiceOptions } from './httpAuthServiceFactory';
+1
View File
@@ -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