diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index bbf208ffdf..cc8472fbe5 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -76,6 +76,7 @@ exports[`should create default config 1`] = ` "adminTokenKillSwitch": false, "anonymiseEventLog": false, "automatedActions": false, + "bearerTokenMiddleware": false, "caseInsensitiveInOperators": false, "celebrateUnleash": false, "collectTrafficDataUsage": false, diff --git a/src/lib/app.ts b/src/lib/app.ts index 64acd87dc6..a593287947 100644 --- a/src/lib/app.ts +++ b/src/lib/app.ts @@ -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); } diff --git a/src/lib/middleware/bearer-token-middleware.test.ts b/src/lib/middleware/bearer-token-middleware.test.ts new file mode 100644 index 0000000000..65addd1048 --- /dev/null +++ b/src/lib/middleware/bearer-token-middleware.test.ts @@ -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); + }); +}); diff --git a/src/lib/middleware/bearer-token-middleware.ts b/src/lib/middleware/bearer-token-middleware.ts new file mode 100644 index 0000000000..1d4b1baeb0 --- /dev/null +++ b/src/lib/middleware/bearer-token-middleware.ts @@ -0,0 +1,27 @@ +import type { Request, Response, NextFunction } from 'express'; +import type { IUnleashConfig } from '../types'; + +export const bearerTokenMiddleware = ({ + getLogger, + flagResolver, +}: Pick) => { + 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(); + }; +}; diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index 154de4f64f..1433a66684 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -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), diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 325e42d416..33052b1632 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -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 = {