1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

chore: bearer token middleware (#6624)

Adds a bearer token middleware that adds support for tokens prefixed
with "Bearer" scheme. Prefixing with "Bearer" is optional and the old
way of authenticating still works, so we now support both ways.

Also, added as part of our OpenAPI spec which now displays authorization
as follows:

![image](https://github.com/Unleash/unleash/assets/455064/77b17342-2315-4c08-bf34-4655e12a1cc3)

Related to #4630. Doesn't fully close the issue as we're still using
some invalid characters for the RFC, in particular `*` and `[]`

For safety reasons this is behind a feature flag

---------

Co-authored-by: Gastón Fournier <gaston@getunleash.io>
This commit is contained in:
Nuno Góis 2024-04-02 10:21:38 +01:00 committed by GitHub
parent 1c55d6b1f9
commit a30ddd81c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 113 additions and 2 deletions

View File

@ -76,6 +76,7 @@ exports[`should create default config 1`] = `
"adminTokenKillSwitch": false,
"anonymiseEventLog": false,
"automatedActions": false,
"bearerTokenMiddleware": false,
"caseInsensitiveInOperators": false,
"celebrateUnleash": false,
"collectTrafficDataUsage": false,

View File

@ -28,6 +28,7 @@ import maintenanceMiddleware from './features/maintenance/maintenance-middleware
import { unless } from './middleware/unless-middleware';
import { catchAllErrorHandler } from './middleware/catch-all-error-handler';
import NotFoundError from './error/notfound-error';
import { bearerTokenMiddleware } from './middleware/bearer-token-middleware';
export default async function getApp(
config: IUnleashConfig,
@ -59,6 +60,8 @@ export default async function getApp(
app.use(requestLogger(config));
app.use(bearerTokenMiddleware(config));
if (typeof config.preHook === 'function') {
config.preHook(app, config, services, db);
}

View File

@ -0,0 +1,66 @@
import { bearerTokenMiddleware } from './bearer-token-middleware';
import type { IUnleashConfig } from '../types';
import { createTestConfig } from '../../test/config/test-config';
import getLogger from '../../test/fixtures/no-logger';
import type { Request, Response } from 'express';
const exampleSignalToken = 'signal_tokensecret';
describe('bearerTokenMiddleware', () => {
const req = { headers: {}, path: '' } as Request;
const res = {} as Response;
const next = jest.fn();
let config: IUnleashConfig;
beforeEach(() => {
config = createTestConfig({
getLogger,
experimental: {
flags: {
bearerTokenMiddleware: true,
},
},
});
});
it('should call next', () => {
const middleware = bearerTokenMiddleware(config);
middleware(req, res, next);
expect(next).toHaveBeenCalled();
});
it('should leave Unleash tokens intact', () => {
const middleware = bearerTokenMiddleware(config);
req.headers = { authorization: exampleSignalToken };
middleware(req, res, next);
expect(req.headers.authorization).toBe(exampleSignalToken);
});
it('should convert Bearer token to Unleash token', () => {
const middleware = bearerTokenMiddleware(config);
const bearerToken = `Bearer ${exampleSignalToken}`;
req.headers = { authorization: bearerToken };
middleware(req, res, next);
expect(req.headers.authorization).toBe(exampleSignalToken);
});
it('should be case insensitive in the scheme', () => {
const middleware = bearerTokenMiddleware(config);
const bearerToken = `bEaReR ${exampleSignalToken}`;
req.headers = { authorization: bearerToken };
middleware(req, res, next);
expect(req.headers.authorization).toBe(exampleSignalToken);
});
});

View File

@ -0,0 +1,27 @@
import type { Request, Response, NextFunction } from 'express';
import type { IUnleashConfig } from '../types';
export const bearerTokenMiddleware = ({
getLogger,
flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>) => {
const logger = getLogger('/middleware/bearer-token-middleware.ts');
logger.debug('Enabling bearer token middleware');
return (req: Request, _: Response, next: NextFunction) => {
if (
req.path.startsWith('/api/signal-endpoint/') ||
flagResolver.isEnabled('bearerTokenMiddleware')
) {
const authHeader = req.headers.authorization;
if (authHeader) {
req.headers.authorization = authHeader.replace(
/^Bearer\s+/i,
'',
);
}
}
next();
};
};

View File

@ -89,13 +89,22 @@ export const createOpenApiSchema = ({
title: 'Unleash API',
version: apiVersion,
},
security: [{ apiKey: [] }],
security: [{ apiKey: [] }, { bearerToken: [] }],
components: {
securitySchemes: {
// https://swagger.io/docs/specification/authentication/api-keys/
apiKey: {
type: 'apiKey',
in: 'header',
name: 'Authorization',
description: 'API key needed to access this API',
},
// https://swagger.io/docs/specification/authentication/bearer-authentication/
bearerToken: {
type: 'http',
scheme: 'bearer',
description:
'API key needed to access this API, in Bearer token format',
},
},
schemas: mapValues(schemas, removeJsonSchemaProps),

View File

@ -56,7 +56,8 @@ export type IFlagKey =
| 'returnGlobalFrontendApiCache'
| 'projectOverviewRefactor'
| 'variantDependencies'
| 'newContextFieldsUI';
| 'newContextFieldsUI'
| 'bearerTokenMiddleware';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -277,6 +278,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_VARIANT_DEPENDENCIES,
false,
),
bearerTokenMiddleware: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_BEARER_TOKEN_MIDDLEWARE,
false,
),
};
export const defaultExperimentalOptions: IExperimentalOptions = {