diff --git a/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts index ff7d722d48..235cac8446 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts @@ -27,6 +27,8 @@ export interface Config { token: string; subject: string; }>; + customHeader?: string; + customCookieAuthName?: string; }; /** * The backstage token expiration. diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts index 9efab07ba5..be91583e16 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts @@ -239,4 +239,83 @@ describe('helpers', () => { token: token, }); }); + + it('works for regular tokens, through custom header auth', async () => { + jest.useFakeTimers({ + now: 1600000004000, + }); + + const helper = AuthHelper.fromConfig( + new ConfigReader({ teamName: 'mock-team', customHeader: 'X-Auth-Token' }), + { cache }, + ); + const token = await tokenFactory.userToken(); + const request = createRequest({ + headers: { ['X-Auth-Token']: token }, + }); + + const expected = { + cfIdentity: { + email: 'hello@example.com', + groups: [{ id: '123', email: 'foo@bar.com', name: 'foo' }], + id: '1234567890', + name: 'User Name', + }, + claims: { + iss: `https://mock-team.cloudflareaccess.com`, + sub: '1234567890', + name: 'User Name', + iat: 1600000000, + exp: 1600000005, + }, + expiresInSeconds: 5, + }; + + await expect(helper.authenticate(request)).resolves.toEqual({ + ...expected, + token: token, + }); + expect(cache.set).toHaveBeenCalledTimes(1); + expect(cache.set.mock.calls[0][0]).toBe( + 'providers/cloudflare-access/profile-v1/1234567890', + ); + expect(JSON.parse(cache.set.mock.calls[0][1] as string)).toEqual(expected); + }); + + it('works for regular tokens, through custom cookie auth name', async () => { + jest.useFakeTimers({ + now: 1600000004000, + }); + + const helper = AuthHelper.fromConfig( + new ConfigReader({ teamName: 'mock-team', customCookieAuthName: 'CF_Custom_Auth' }), + { cache }, + ); + const token = await tokenFactory.userToken(); + const request = createRequest({ + cookies: { 'CF_Custom_Auth': token }, + }); + + const expected = { + cfIdentity: { + email: 'hello@example.com', + groups: [{ id: '123', email: 'foo@bar.com', name: 'foo' }], + id: '1234567890', + name: 'User Name', + }, + claims: { + iss: `https://mock-team.cloudflareaccess.com`, + sub: '1234567890', + name: 'User Name', + iat: 1600000000, + exp: 1600000005, + }, + expiresInSeconds: 5, + }; + + await expect(helper.authenticate(request)).resolves.toEqual({ + ...expected, + token: token, + }); + }); }); diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.ts index c1a14ae6b4..298fd82a89 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.ts @@ -40,6 +40,8 @@ export class AuthHelper { options?: { cache?: CacheService }, ): AuthHelper { const teamName = config.getString('teamName'); + const customHeader = config.getString('customHeader'); + const customCookieAuthName = config.getString('customCookieAuthName'); const serviceTokens = ( config.getOptionalConfigArray('serviceTokens') ?? [] )?.map(cfg => { @@ -53,7 +55,7 @@ export class AuthHelper { new URL(`https://${teamName}.cloudflareaccess.com/cdn-cgi/access/certs`), ); - return new AuthHelper(teamName, serviceTokens, keySet, options?.cache); + return new AuthHelper(teamName, serviceTokens, keySet, options?.cache, customHeader, customCookieAuthName); } private constructor( @@ -61,20 +63,23 @@ export class AuthHelper { private readonly serviceTokens: ServiceToken[], private readonly keySet: ReturnType, private readonly cache?: CacheService, - ) {} + private readonly customHeader?: string, + private readonly customCookieAuthName?: string, + ) { } async authenticate(req: express.Request): Promise { // JWTs generated by Access are available in a request header as // Cf-Access-Jwt-Assertion and as cookies as CF_Authorization. - let jwt = req.header(CF_JWT_HEADER); + let jwt = req.header(this.customHeader || CF_JWT_HEADER); if (!jwt) { - jwt = req.cookies.CF_Authorization; + jwt = req.cookies[this.customCookieAuthName || COOKIE_AUTH_NAME]; } if (!jwt) { // Only throw if both are not provided by Cloudflare Access since either // can be used. throw new AuthenticationError( - `Missing ${CF_JWT_HEADER} from Cloudflare Access`, + `Missing ${this.customHeader || CF_JWT_HEADER} and + ${this.customCookieAuthName || COOKIE_AUTH_NAME} from Cloudflare Access`, ); } @@ -155,8 +160,8 @@ export class AuthHelper { ): Promise { const headers = new Headers(); // set both headers just the way inbound responses are set - headers.set(CF_JWT_HEADER, jwt); - headers.set('cookie', `${COOKIE_AUTH_NAME}=${jwt}`); + headers.set(this.customHeader || CF_JWT_HEADER, jwt); + headers.set('cookie', `${this.customCookieAuthName || COOKIE_AUTH_NAME}=${jwt}`); try { const res = await fetch( `https://${this.teamName}.cloudflareaccess.com/cdn-cgi/access/get-identity`,