diff --git a/packages/reverse-proxy-service/.env.example b/packages/reverse-proxy-service/.env.example index 15e908ff3..290c5a565 100644 --- a/packages/reverse-proxy-service/.env.example +++ b/packages/reverse-proxy-service/.env.example @@ -23,3 +23,6 @@ KEYCLOAK_SERVER_URL= KEYCLOAK_ORIGINAL_HOST= KEYCLOAK_SECRET= KEYCLOAK_PUBKEY= + +## User blacklist (optional, loaded once at startup) +BLACKLIST_FILE_PATH=blacklist.txt diff --git a/packages/reverse-proxy-service/.gitignore b/packages/reverse-proxy-service/.gitignore index dd10836f5..59d01bc24 100644 --- a/packages/reverse-proxy-service/.gitignore +++ b/packages/reverse-proxy-service/.gitignore @@ -3,6 +3,7 @@ dist/ build/ tmp/ +blacklist.txt # Logs logs diff --git a/packages/reverse-proxy-service/README.md b/packages/reverse-proxy-service/README.md index 52f45ba86..d37b9520a 100644 --- a/packages/reverse-proxy-service/README.md +++ b/packages/reverse-proxy-service/README.md @@ -7,6 +7,30 @@ Currently, the reverse-proxy contains middleware rules for: - CouchDB: An open-source document-oriented NoSQL database. - Keycloak Auth: An auth middleware to apply Keycloak SSO Auth to some restricted URLs - A no-cors proxy middleware: Used for API Catalog +- User blacklist: Optional file-backed blocklist checked before upstream proxy + +## User blacklist + +When `BLACKLIST_FILE_PATH` is set, the service loads a text file of blocked user identifiers once at startup. Restart the service to pick up file changes. + +### File format + +See [`blacklist.example.txt`](blacklist.example.txt). One value per line; empty lines and lines starting with `#` are ignored. Each line is matched if it equals the user's `uid` or `email` (case-insensitive) from their token. + +### How identity is resolved + +| Route | Identity source | +|-------|-----------------| +| `/api/couchdb`, `/api/no-cors-proxy` | `Authorization: Bearer` access token (verified with Keycloak public key) | +| SPA catch-all (`GET *`) | OIDC session `idToken` when the user is logged in | + +Requests without a verifiable identity are not blocked by the blacklist (other auth rules may still apply). + +Blocked users receive `403` with `{ "error": "Access denied" }`. + +### Deployment + +Mount the blacklist file at the path given by `BLACKLIST_FILE_PATH` (for example via a Kubernetes ConfigMap). ## License diff --git a/packages/reverse-proxy-service/blacklist.example.txt b/packages/reverse-proxy-service/blacklist.example.txt new file mode 100644 index 000000000..fc7179e78 --- /dev/null +++ b/packages/reverse-proxy-service/blacklist.example.txt @@ -0,0 +1,3 @@ +# One identifier per line (uid or email) +jdoe +blocked.user@redhat.com diff --git a/packages/reverse-proxy-service/src/blacklist/blacklist.ts b/packages/reverse-proxy-service/src/blacklist/blacklist.ts new file mode 100644 index 000000000..0dcb241f1 --- /dev/null +++ b/packages/reverse-proxy-service/src/blacklist/blacklist.ts @@ -0,0 +1,23 @@ +import { BLACKLIST_FILE_PATH } from '../setup/env'; +import { loadBlacklistFromFile } from './loadBlacklistFromFile'; +import { BlacklistIndex } from './types'; + +const emptyIndex = (): BlacklistIndex => ({ entries: new Set() }); + +let blacklistIndex: BlacklistIndex = emptyIndex(); + +export function isBlacklistEnabled(): boolean { + return Boolean(BLACKLIST_FILE_PATH?.trim()); +} + +export function getBlacklistIndex(): BlacklistIndex { + return blacklistIndex; +} + +export async function initBlacklist(): Promise { + if (!isBlacklistEnabled()) { + blacklistIndex = emptyIndex(); + return; + } + blacklistIndex = await loadBlacklistFromFile(BLACKLIST_FILE_PATH as string); +} diff --git a/packages/reverse-proxy-service/src/blacklist/extractUserClaims.ts b/packages/reverse-proxy-service/src/blacklist/extractUserClaims.ts new file mode 100644 index 000000000..abd0f25d4 --- /dev/null +++ b/packages/reverse-proxy-service/src/blacklist/extractUserClaims.ts @@ -0,0 +1,8 @@ +import { UserClaims } from './types'; + +export function extractUserClaims(payload: Record): UserClaims { + return { + uid: typeof payload.uid === 'string' ? payload.uid : undefined, + email: typeof payload.email === 'string' ? payload.email : undefined, + }; +} diff --git a/packages/reverse-proxy-service/src/blacklist/isUserBlacklisted.ts b/packages/reverse-proxy-service/src/blacklist/isUserBlacklisted.ts new file mode 100644 index 000000000..11e3e57c8 --- /dev/null +++ b/packages/reverse-proxy-service/src/blacklist/isUserBlacklisted.ts @@ -0,0 +1,24 @@ +import logger from '../setup/logger'; +import { BlacklistIndex, UserClaims } from './types'; + +export function isUserBlacklisted( + index: BlacklistIndex, + user: UserClaims, +): boolean { + const { entries } = index; + if (entries.size === 0) { + return false; + } + + if (user.uid && entries.has(user.uid)) { + logger.info('user is blacklisted by uid', { user }); + return true; + } + + if (user.email && entries.has(user.email.toLowerCase())) { + logger.info('user is blacklisted by email', { user }); + return true; + } + + return false; +} diff --git a/packages/reverse-proxy-service/src/blacklist/loadBlacklistFromFile.ts b/packages/reverse-proxy-service/src/blacklist/loadBlacklistFromFile.ts new file mode 100644 index 000000000..3f68e5e7d --- /dev/null +++ b/packages/reverse-proxy-service/src/blacklist/loadBlacklistFromFile.ts @@ -0,0 +1,20 @@ +import { readFile } from 'fs/promises'; +import logger from '../setup/logger'; +import { parseBlacklistFile } from './parseBlacklistFile'; +import { BlacklistIndex } from './types'; + +const emptyIndex = (): BlacklistIndex => ({ entries: new Set() }); + +export async function loadBlacklistFromFile( + path: string +): Promise { + try { + const content = await readFile(path, 'utf8'); + const index = parseBlacklistFile(content); + logger.info(`blacklist loaded: ${index.entries.size} entries`); + return index; + } catch (err) { + logger.error('failed to load blacklist file', { path, err }); + return emptyIndex(); + } +} diff --git a/packages/reverse-proxy-service/src/blacklist/parseBlacklistFile.ts b/packages/reverse-proxy-service/src/blacklist/parseBlacklistFile.ts new file mode 100644 index 000000000..a3cd9f3c2 --- /dev/null +++ b/packages/reverse-proxy-service/src/blacklist/parseBlacklistFile.ts @@ -0,0 +1,15 @@ +import { BlacklistIndex } from './types'; + +export function parseBlacklistFile(content: string): BlacklistIndex { + const entries = new Set(); + + for (const line of content.split(/\r?\n/)) { + const trimmed = line.trim(); + if (trimmed.length === 0 || trimmed.startsWith('#')) { + continue; + } + entries.add(trimmed); + } + + return { entries }; +} diff --git a/packages/reverse-proxy-service/src/blacklist/types.ts b/packages/reverse-proxy-service/src/blacklist/types.ts new file mode 100644 index 000000000..5499fe7a3 --- /dev/null +++ b/packages/reverse-proxy-service/src/blacklist/types.ts @@ -0,0 +1,8 @@ +export type BlacklistIndex = { + entries: Set; +}; + +export type UserClaims = { + uid?: string; + email?: string; +}; diff --git a/packages/reverse-proxy-service/src/middleware/blacklistBearer.ts b/packages/reverse-proxy-service/src/middleware/blacklistBearer.ts new file mode 100644 index 000000000..1ee149c80 --- /dev/null +++ b/packages/reverse-proxy-service/src/middleware/blacklistBearer.ts @@ -0,0 +1,46 @@ +import { NextFunction, Request, Response } from 'express'; +import { + getBlacklistIndex, + isBlacklistEnabled, +} from '../blacklist/blacklist'; +import { extractUserClaims } from '../blacklist/extractUserClaims'; +import { isUserBlacklisted } from '../blacklist/isUserBlacklisted'; +import logger from '../setup/logger'; +import { parseBearerToken } from '../utils/parseBearerToken'; +import { verifyJwtToken } from '../utils/verifyJwtToken'; + +const blacklistBearer = ( + req: Request, + res: Response, + next: NextFunction +): void => { + if (!isBlacklistEnabled()) { + next(); + return; + } + + const token = parseBearerToken(req.headers?.authorization); + if (!token) { + next(); + return; + } + + verifyJwtToken(token, (err: Error | null, tokenParsed: Record) => { + if (err) { + next(); + return; + } + + const claims = extractUserClaims(tokenParsed); + if (isUserBlacklisted(getBlacklistIndex(), claims)) { + res.status(403).json({ error: 'Access denied' }); + return; + } + + res.locals.user = tokenParsed; + res.locals.authenticated = true; + next(); + }); +}; + +export default blacklistBearer; diff --git a/packages/reverse-proxy-service/src/middleware/blacklistOidc.ts b/packages/reverse-proxy-service/src/middleware/blacklistOidc.ts new file mode 100644 index 000000000..c10fc4ce1 --- /dev/null +++ b/packages/reverse-proxy-service/src/middleware/blacklistOidc.ts @@ -0,0 +1,51 @@ +import { NextFunction, Request, Response } from 'express'; +import { extractUserClaims } from '../blacklist/extractUserClaims'; +import { + getBlacklistIndex, + isBlacklistEnabled, +} from '../blacklist/blacklist'; +import { isUserBlacklisted } from '../blacklist/isUserBlacklisted'; +import logger from '../setup/logger'; +import { verifyJwtToken } from '../utils/verifyJwtToken'; + +const blacklistOidc = ( + req: Request, + res: Response, + next: NextFunction +): void => { + if (!isBlacklistEnabled()) { + next(); + return; + } + + if (!req.oidc?.isAuthenticated()) { + next(); + return; + } + + const idToken = req.oidc.idToken; + if (!idToken) { + next(); + return; + } + + verifyJwtToken(idToken, (err: Error | null, tokenParsed: Record) => { + if (err) { + logger.warn('blacklist: failed to verify OIDC idToken', { + message: err.message, + }); + next(); + return; + } + + const claims = extractUserClaims(tokenParsed); + if (isUserBlacklisted(getBlacklistIndex(), claims)) { + res.status(403).json({ error: 'Access denied' }); + return; + } + + next(); + }); +}; + +export default blacklistOidc; diff --git a/packages/reverse-proxy-service/src/middleware/checkSSO.ts b/packages/reverse-proxy-service/src/middleware/checkSSO.ts index 65dee1361..0a4499624 100644 --- a/packages/reverse-proxy-service/src/middleware/checkSSO.ts +++ b/packages/reverse-proxy-service/src/middleware/checkSSO.ts @@ -4,7 +4,7 @@ import getApplicationCache from '../utils/applicationCache'; export default async (req: Request, res: Response, next: NextFunction) => { const apps = await getApplicationCache(); - const app = apps.find((app) => req.url.startsWith(app.path)); + const app = apps?.find((app) => req.url.startsWith(app.path)); if (app && app.authenticate) { return requiresAuth()(req, res, next); diff --git a/packages/reverse-proxy-service/src/middleware/jwtAuth.ts b/packages/reverse-proxy-service/src/middleware/jwtAuth.ts index b2284c645..e966c6379 100644 --- a/packages/reverse-proxy-service/src/middleware/jwtAuth.ts +++ b/packages/reverse-proxy-service/src/middleware/jwtAuth.ts @@ -1,25 +1,27 @@ import { NextFunction, Request, Response } from 'express'; +import { parseBearerToken } from '../utils/parseBearerToken'; import { verifyJwtToken } from '../utils/verifyJwtToken'; const jwtAuth = (req: Request, res: Response, next: NextFunction): void => { try { - if (!req.headers?.authorization) { - throw new Error('Request is not authenticated'); + if (res.locals.user) { + next(); + return; } - const authorization = req.headers.authorization; - const [authType, token] = authorization.split(' '); - if (authType === 'Bearer') { - verifyJwtToken(token, (err: any, tokenParsed: any) => { - if (err) { - throw new Error(err.message); - } - res.locals.user = tokenParsed; - res.locals.authenticated = true; - next(); - }); - } else { - throw new Error(`Authentication method not supported`); + + const token = parseBearerToken(req.headers?.authorization); + if (!token) { + throw new Error('Request is not authenticated'); } + + verifyJwtToken(token, (err: any, tokenParsed: any) => { + if (err) { + throw new Error(err.message); + } + res.locals.user = tokenParsed; + res.locals.authenticated = true; + next(); + }); } catch (err: any) { res.status(403).json({ error: err.message }); } diff --git a/packages/reverse-proxy-service/src/restricted-spas/index.ts b/packages/reverse-proxy-service/src/restricted-spas/index.ts index 5a7f193b4..e50e48f8e 100644 --- a/packages/reverse-proxy-service/src/restricted-spas/index.ts +++ b/packages/reverse-proxy-service/src/restricted-spas/index.ts @@ -1,4 +1,5 @@ import { Router } from 'express'; +import blacklistOidc from '../middleware/blacklistOidc'; import checkSSO from '../middleware/checkSSO'; import resolver from './resolver'; @@ -13,6 +14,6 @@ router.get('/red-hat-in-vehicle-development-guide', (_, res) => { res.redirect('/in-vehicle-development-guide', 301); }); /* Proxy all other apps using resolver */ -router.get('*', [checkSSO], resolver); +router.get('*', [checkSSO, blacklistOidc], resolver); export default router; diff --git a/packages/reverse-proxy-service/src/server.ts b/packages/reverse-proxy-service/src/server.ts index c48d8e8fe..64d596ef9 100644 --- a/packages/reverse-proxy-service/src/server.ts +++ b/packages/reverse-proxy-service/src/server.ts @@ -3,6 +3,7 @@ import cors from 'cors'; import expressWinston from 'express-winston'; import session from 'express-session'; import rateLimit from 'express-rate-limit'; +import blacklistBearer from './middleware/blacklistBearer'; import jwtAuth from './middleware/jwtAuth'; import couchDBRouter from './couchdb'; import noCorsRouter from './no-cors-proxy'; @@ -11,6 +12,7 @@ import winstonInstance from './setup/logger'; import store from './setup/store'; import { COOKIE_SECRET } from './setup/env'; import oidcAuth from './middleware/oidcAuth'; +import { initBlacklist } from './blacklist/blacklist'; import { updateApplicationCache } from './utils/applicationCache'; const getServer = async () => { @@ -39,9 +41,11 @@ const getServer = async () => { res.json({ message: 'This is a proxy service.' }); }); - server.use('/api/couchdb', [cors(), jwtAuth], couchDBRouter); + await initBlacklist(); - server.use('/api/no-cors-proxy', [cors()], noCorsRouter); + server.use('/api/couchdb', [cors(), blacklistBearer, jwtAuth], couchDBRouter); + + server.use('/api/no-cors-proxy', [cors(), blacklistBearer], noCorsRouter); /* Setting up session for keycloak */ server.use( diff --git a/packages/reverse-proxy-service/src/setup/env.ts b/packages/reverse-proxy-service/src/setup/env.ts index ee61e5a2d..37b009193 100644 --- a/packages/reverse-proxy-service/src/setup/env.ts +++ b/packages/reverse-proxy-service/src/setup/env.ts @@ -31,3 +31,6 @@ export const { COUCHDB_HOST, COUCHDB_SECRET } = process.env; /* SPASHIP environment variables */ export const { SPASHIP_ROUTER_HOST } = process.env; + +/* User blacklist */ +export const { BLACKLIST_FILE_PATH } = process.env; diff --git a/packages/reverse-proxy-service/src/spec/blacklistBearer.spec.ts b/packages/reverse-proxy-service/src/spec/blacklistBearer.spec.ts new file mode 100644 index 000000000..812bd155b --- /dev/null +++ b/packages/reverse-proxy-service/src/spec/blacklistBearer.spec.ts @@ -0,0 +1,90 @@ +import { NextFunction, Request, Response } from 'express'; +import blacklistBearer from '../middleware/blacklistBearer'; +import * as blacklistModule from '../blacklist/blacklist'; +import * as verifyJwtModule from '../utils/verifyJwtToken'; + +jest.mock('../blacklist/blacklist'); +jest.mock('../utils/verifyJwtToken'); + +const mockIsBlacklistEnabled = blacklistModule.isBlacklistEnabled as jest.Mock; +const mockGetBlacklistIndex = blacklistModule.getBlacklistIndex as jest.Mock; +const mockVerifyJwtToken = verifyJwtModule.verifyJwtToken as jest.Mock; + +function runMiddleware( + req: Partial, + locals: Record = {} +): Promise { + return new Promise((resolve) => { + const next: NextFunction = () => resolve(res); + const json = jest.fn(() => resolve(res)); + const status = jest.fn(() => ({ json })); + const res = { status, locals } as unknown as Response; + blacklistBearer(req as Request, res, next); + }); +} + +describe('blacklistBearer', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockIsBlacklistEnabled.mockReturnValue(true); + mockGetBlacklistIndex.mockReturnValue({ + entries: new Set(['jdoe']), + }); + }); + + it('passes through when blacklist is disabled', async () => { + mockIsBlacklistEnabled.mockReturnValue(false); + const res = await runMiddleware({ headers: {} }); + expect(res.status).not.toHaveBeenCalled(); + }); + + it('passes through when no Bearer token', async () => { + const res = await runMiddleware({ headers: {} }); + expect(mockVerifyJwtToken).not.toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + it('passes through when JWT verification fails', async () => { + mockVerifyJwtToken.mockImplementation( + (_token: string, cb: (err: Error) => void) => { + cb(new Error('invalid')); + } + ); + const res = await runMiddleware({ + headers: { authorization: 'Bearer bad-token' }, + }); + expect(res.status).not.toHaveBeenCalled(); + }); + + it('returns 403 when user is blacklisted', async () => { + mockVerifyJwtToken.mockImplementation( + (_token: string, cb: (err: null, payload: object) => void) => { + cb(null, { uid: 'jdoe', email: 'jdoe@example.com' }); + } + ); + const res = await runMiddleware({ + headers: { authorization: 'Bearer valid-token' }, + }); + expect(res.status).toHaveBeenCalledWith(403); + const json = (res.status as jest.Mock).mock.results[0].value.json; + expect(json).toHaveBeenCalledWith({ error: 'Access denied' }); + }); + + it('sets res.locals.user when not blacklisted', async () => { + const payload = { + uid: 'allowed', + email: 'allowed@example.com', + }; + mockVerifyJwtToken.mockImplementation( + (_token: string, cb: (err: null, p: object) => void) => { + cb(null, payload); + } + ); + const res = await runMiddleware({ + headers: { authorization: 'Bearer valid-token' }, + }); + expect(res.status).not.toHaveBeenCalled(); + expect(res.locals.user).toEqual(payload); + expect(res.locals.authenticated).toBe(true); + }); +}); diff --git a/packages/reverse-proxy-service/src/spec/blacklistOidc.spec.ts b/packages/reverse-proxy-service/src/spec/blacklistOidc.spec.ts new file mode 100644 index 000000000..9b5485040 --- /dev/null +++ b/packages/reverse-proxy-service/src/spec/blacklistOidc.spec.ts @@ -0,0 +1,99 @@ +import { NextFunction, Request, Response } from 'express'; +import blacklistOidc from '../middleware/blacklistOidc'; +import * as blacklistModule from '../blacklist/blacklist'; +import * as verifyJwtModule from '../utils/verifyJwtToken'; + +jest.mock('../blacklist/blacklist'); +jest.mock('../utils/verifyJwtToken'); + +const mockIsBlacklistEnabled = blacklistModule.isBlacklistEnabled as jest.Mock; +const mockGetBlacklistIndex = blacklistModule.getBlacklistIndex as jest.Mock; +const mockVerifyJwtToken = verifyJwtModule.verifyJwtToken as jest.Mock; + +function runMiddleware(req: Partial): Promise { + return new Promise((resolve) => { + const next: NextFunction = () => resolve(res); + const json = jest.fn(() => resolve(res)); + const status = jest.fn(() => ({ json })); + const res = { status } as unknown as Response; + blacklistOidc(req as Request, res, next); + }); +} + +describe('blacklistOidc', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockIsBlacklistEnabled.mockReturnValue(true); + mockGetBlacklistIndex.mockReturnValue({ + entries: new Set(['550e8400-e29b-41d4-a716-446655440000']), + }); + }); + + it('passes through when blacklist is disabled', async () => { + mockIsBlacklistEnabled.mockReturnValue(false); + const res = await runMiddleware({ + oidc: { isAuthenticated: () => true } as Request['oidc'], + }); + expect(res.status).not.toHaveBeenCalled(); + }); + + it('passes through when user is not authenticated', async () => { + const res = await runMiddleware({ + oidc: { + isAuthenticated: () => false, + idToken: undefined, + } as Request['oidc'], + }); + expect(mockVerifyJwtToken).not.toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + it('passes through when idToken verification fails', async () => { + mockVerifyJwtToken.mockImplementation( + (_token: string, cb: (err: Error) => void) => { + cb(new Error('invalid')); + } + ); + const res = await runMiddleware({ + oidc: { + isAuthenticated: () => true, + idToken: 'bad-id-token', + } as Request['oidc'], + }); + expect(res.status).not.toHaveBeenCalled(); + }); + + it('returns 403 when user is blacklisted', async () => { + mockVerifyJwtToken.mockImplementation( + (_token: string, cb: (err: null, payload: object) => void) => { + cb(null, { + uid: '550e8400-e29b-41d4-a716-446655440000', + }); + } + ); + const res = await runMiddleware({ + oidc: { + isAuthenticated: () => true, + idToken: 'valid-id-token', + } as Request['oidc'], + }); + expect(res.status).toHaveBeenCalledWith(403); + const json = (res.status as jest.Mock).mock.results[0].value.json; + expect(json).toHaveBeenCalledWith({ error: 'Access denied' }); + }); + + it('passes through when user is not blacklisted', async () => { + mockVerifyJwtToken.mockImplementation( + (_token: string, cb: (err: null, payload: object) => void) => { + cb(null, { uid: 'allowed', email: 'allowed@example.com' }); + } + ); + const res = await runMiddleware({ + oidc: { + isAuthenticated: () => true, + idToken: 'valid-id-token', + } as Request['oidc'], + }); + expect(res.status).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/reverse-proxy-service/src/spec/parseBlacklistFile.spec.ts b/packages/reverse-proxy-service/src/spec/parseBlacklistFile.spec.ts new file mode 100644 index 000000000..74445fa33 --- /dev/null +++ b/packages/reverse-proxy-service/src/spec/parseBlacklistFile.spec.ts @@ -0,0 +1,50 @@ +import { parseBlacklistFile } from '../blacklist/parseBlacklistFile'; +import { isUserBlacklisted } from '../blacklist/isUserBlacklisted'; + +describe('parseBlacklistFile', () => { + it('ignores empty lines and comments', () => { + const index = parseBlacklistFile(` +# comment +jdoe + +blocked@example.com + `); + expect(index.entries.size).toBe(2); + expect(index.entries.has('jdoe')).toBe(true); + expect(index.entries.has('blocked@example.com')).toBe(true); + }); + + it('keeps values containing colons as literal entries', () => { + const index = parseBlacklistFile('user:name@example.com'); + expect(index.entries.has('user:name@example.com')).toBe(true); + }); +}); + +describe('isUserBlacklisted', () => { + const index = parseBlacklistFile('jdoe\nblocked@example.com\nuuid-123'); + + it('matches uid', () => { + expect(isUserBlacklisted(index, { uid: 'jdoe' })).toBe(true); + }); + + it('matches email case-insensitively', () => { + expect(isUserBlacklisted(index, { email: 'Blocked@Example.com' })).toBe( + true + ); + }); + + it('returns false when no field matches', () => { + expect( + isUserBlacklisted(index, { + uid: 'other', + email: 'other@example.com', + }) + ).toBe(false); + }); + + it('returns false for empty blacklist', () => { + expect(isUserBlacklisted({ entries: new Set() }, { uid: 'jdoe' })).toBe( + false + ); + }); +}); diff --git a/packages/reverse-proxy-service/src/utils/parseBearerToken.ts b/packages/reverse-proxy-service/src/utils/parseBearerToken.ts new file mode 100644 index 000000000..841e52d3d --- /dev/null +++ b/packages/reverse-proxy-service/src/utils/parseBearerToken.ts @@ -0,0 +1,12 @@ +export function parseBearerToken( + authorization: string | undefined +): string | undefined { + if (!authorization) { + return undefined; + } + const [authType, token] = authorization.split(' '); + if (authType !== 'Bearer' || !token) { + return undefined; + } + return token; +}