diff --git a/.changeset/strong-chicken-kiss.md b/.changeset/strong-chicken-kiss.md new file mode 100644 index 0000000000..26f667c5ab --- /dev/null +++ b/.changeset/strong-chicken-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': minor +--- + +Support for Cloudflare Custom Headers and Custom Cookie Auth Name diff --git a/docs/auth/cloudflare/provider.md b/docs/auth/cloudflare/provider.md index 516ea9d647..a1946580a7 100644 --- a/docs/auth/cloudflare/provider.md +++ b/docs/auth/cloudflare/provider.md @@ -32,6 +32,12 @@ auth: serviceTokens: - token: '1uh2fh19efvfh129f1f919u21f2f19jf2.access' subject: 'bot-user@your-company.com' + # You can customize the header name that contains the jwt token, by default + # cf-access-jwt-assertion is used + jwtHeaderName: + # You can customize the authorization cookie name, by default + # CF_Authorization is used + authorizationCookieName: # This picks what sign in resolver(s) you want to use. signIn: resolvers: 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 1300eb8c57..21b839b8d7 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; }>; + jwtHeaderName?: string; + authorizationCookieName?: string; signIn?: { resolvers: Array< | { resolver: 'emailLocalPartMatchingUserEntityName' } 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..07e9860d98 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,89 @@ describe('helpers', () => { token: token, }); }); + + it('works for regular tokens, through jwtHeaderName header', async () => { + jest.useFakeTimers({ + now: 1600000004000, + }); + + const helper = AuthHelper.fromConfig( + new ConfigReader({ + teamName: 'mock-team', + jwtHeaderName: '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 authorizationCookieName cookie name', async () => { + jest.useFakeTimers({ + now: 1600000004000, + }); + + const helper = AuthHelper.fromConfig( + new ConfigReader({ + teamName: 'mock-team', + authorizationCookieName: 'CF_Auth', + }), + { cache }, + ); + const token = await tokenFactory.userToken(); + const request = createRequest({ + cookies: { CF_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..3077258dd6 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,10 @@ export class AuthHelper { options?: { cache?: CacheService }, ): AuthHelper { const teamName = config.getString('teamName'); + const jwtHeaderName = + config.getOptionalString('jwtHeaderName') ?? CF_JWT_HEADER; + const authorizationCookieName = + config.getOptionalString('authorizationCookieName') ?? COOKIE_AUTH_NAME; const serviceTokens = ( config.getOptionalConfigArray('serviceTokens') ?? [] )?.map(cfg => { @@ -53,12 +57,21 @@ 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, + jwtHeaderName, + authorizationCookieName, + keySet, + options?.cache, + ); } private constructor( private readonly teamName: string, private readonly serviceTokens: ServiceToken[], + private readonly jwtHeaderName: string, + private readonly authorizationCookieName: string, private readonly keySet: ReturnType, private readonly cache?: CacheService, ) {} @@ -66,15 +79,16 @@ export class AuthHelper { 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.jwtHeaderName); if (!jwt) { - jwt = req.cookies.CF_Authorization; + jwt = req.cookies[this.authorizationCookieName]; } 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.jwtHeaderName} and + ${this.authorizationCookieName} from Cloudflare Access`, ); } @@ -155,8 +169,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.jwtHeaderName, jwt); + headers.set('cookie', `${this.authorizationCookieName}=${jwt}`); try { const res = await fetch( `https://${this.teamName}.cloudflareaccess.com/cdn-cgi/access/get-identity`,