backend-defaults: export DefaultHttpAuthService
Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Export `DefaultHttpAuthService` to allow for custom token extraction logic.
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user