Merge pull request #27741 from backstage/jhaals/httpauth
backend-defaults: export DefaultHttpAuthService
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Export `DefaultHttpAuthService` to allow for custom token extraction logic.
|
||||
@@ -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 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';
|
||||
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.
|
||||
|
||||
@@ -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,51 @@
|
||||
> 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;
|
||||
getTokenFromRequest?: (req: Request_2) => {
|
||||
token?: string;
|
||||
};
|
||||
// (undocumented)
|
||||
pluginId: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const httpAuthServiceFactory: ServiceFactory<
|
||||
HttpAuthService,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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',
|
||||
getTokenFromRequest: req => {
|
||||
let token: string | undefined;
|
||||
const header = req.headers.test;
|
||||
if (typeof header === 'string') {
|
||||
token = header;
|
||||
}
|
||||
return { token };
|
||||
},
|
||||
});
|
||||
await httpAuthService.credentials(
|
||||
createRequest({ headers: { test: 'mock-user-token' } }),
|
||||
);
|
||||
expect(auth.authenticate).toHaveBeenCalledWith('mock-user-token');
|
||||
});
|
||||
});
|
||||
@@ -33,17 +33,14 @@ const FIVE_MINUTES_MS = 5 * 60 * 1000;
|
||||
const BACKSTAGE_AUTH_COOKIE = 'backstage-auth';
|
||||
|
||||
function getTokenFromRequest(req: Request) {
|
||||
// TODO: support multiple auth headers (iterate rawHeaders)
|
||||
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) {
|
||||
@@ -71,23 +68,55 @@ 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 logic for extracting the token from the request.
|
||||
*/
|
||||
getTokenFromRequest?: (req: Request) => { token?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* DefaultHttpAuthService is the default implementation of the HttpAuthService
|
||||
*/
|
||||
export class DefaultHttpAuthService implements HttpAuthService {
|
||||
readonly #auth: AuthService;
|
||||
readonly #discovery: DiscoveryService;
|
||||
readonly #pluginId: string;
|
||||
readonly #getToken: (req: Request) => { token?: string };
|
||||
|
||||
constructor(
|
||||
private constructor(
|
||||
auth: AuthService,
|
||||
discovery: DiscoveryService,
|
||||
pluginId: string,
|
||||
getToken?: (req: Request) => { token?: string },
|
||||
) {
|
||||
this.#auth = auth;
|
||||
this.#discovery = discovery;
|
||||
this.#pluginId = pluginId;
|
||||
this.#getToken = getToken ?? getTokenFromRequest;
|
||||
}
|
||||
|
||||
static create(
|
||||
options: DefaultHttpAuthServiceOptions,
|
||||
): DefaultHttpAuthService {
|
||||
return new DefaultHttpAuthService(
|
||||
options.auth,
|
||||
options.discovery,
|
||||
options.pluginId,
|
||||
options.getTokenFromRequest,
|
||||
);
|
||||
}
|
||||
|
||||
async #extractCredentialsFromRequest(req: Request) {
|
||||
const token = getTokenFromRequest(req);
|
||||
const { token } = this.#getToken(req);
|
||||
if (!token) {
|
||||
return await this.#auth.getNoneCredentials();
|
||||
}
|
||||
@@ -96,7 +125,7 @@ class DefaultHttpAuthService implements HttpAuthService {
|
||||
}
|
||||
|
||||
async #extractLimitedCredentialsFromRequest(req: Request) {
|
||||
const token = getTokenFromRequest(req);
|
||||
const { token } = this.#getToken(req);
|
||||
if (token) {
|
||||
return await this.#auth.authenticate(token, {
|
||||
allowLimitedAccess: true,
|
||||
@@ -289,6 +318,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