From 89d705e8060a6523734a33001af4c020c211e2d0 Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Fri, 11 Nov 2022 20:48:15 -0800 Subject: [PATCH] Permit customizing the header name for the IAP jwt This creates a new configuration parameter `jwtHeader` for the gcp-iap auth provider. This allows setting a custom header to look in for the IAP issued JWT. Signed-off-by: Adam Kunicki --- .changeset/great-icons-greet.md | 5 ++ docs/auth/google/gcp-iap-auth.md | 1 + .../src/providers/gcp-iap/helpers.test.ts | 6 +- .../src/providers/gcp-iap/helpers.ts | 6 +- .../src/providers/gcp-iap/provider.test.ts | 72 ++++++++++--------- .../src/providers/gcp-iap/provider.ts | 9 ++- .../src/providers/gcp-iap/types.ts | 2 +- 7 files changed, 57 insertions(+), 44 deletions(-) create mode 100644 .changeset/great-icons-greet.md diff --git a/.changeset/great-icons-greet.md b/.changeset/great-icons-greet.md new file mode 100644 index 0000000000..f32f6f1808 --- /dev/null +++ b/.changeset/great-icons-greet.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Add support for custom JWT header name in GCP IAP auth. diff --git a/docs/auth/google/gcp-iap-auth.md b/docs/auth/google/gcp-iap-auth.md index 1ebfcaf15b..90d267b327 100644 --- a/docs/auth/google/gcp-iap-auth.md +++ b/docs/auth/google/gcp-iap-auth.md @@ -26,6 +26,7 @@ auth: providers: gcp-iap: audience: '/projects//global/backendServices/' + jwtHeader: x-custom-header # Optional: Only if you are using a custom header for the IAP JWT ``` You can find the project number and service ID in the Google Cloud Console. diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts index c0e351c90b..0162366f61 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts @@ -95,19 +95,19 @@ describe('helpers', () => { parseRequestToken(7, undefined as any), ).rejects.toMatchObject({ name: 'AuthenticationError', - message: 'Missing Google IAP header: x-goog-iap-jwt-assertion', + message: 'Missing Google IAP header', }); await expect( parseRequestToken(undefined, undefined as any), ).rejects.toMatchObject({ name: 'AuthenticationError', - message: 'Missing Google IAP header: x-goog-iap-jwt-assertion', + message: 'Missing Google IAP header', }); await expect( parseRequestToken('', undefined as any), ).rejects.toMatchObject({ name: 'AuthenticationError', - message: 'Missing Google IAP header: x-goog-iap-jwt-assertion', + message: 'Missing Google IAP header', }); }); diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.ts index c7e81adc73..26d9a8c904 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/helpers.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/helpers.ts @@ -17,7 +17,7 @@ import { AuthenticationError } from '@backstage/errors'; import { OAuth2Client, TokenPayload } from 'google-auth-library'; import { AuthHandler } from '../types'; -import { GcpIapResult, IAP_JWT_HEADER } from './types'; +import { GcpIapResult } from './types'; export function createTokenValidator( audience: string, @@ -52,9 +52,7 @@ export async function parseRequestToken( tokenValidator: (token: string) => Promise, ): Promise { if (typeof jwtToken !== 'string' || !jwtToken) { - throw new AuthenticationError( - `Missing Google IAP header: ${IAP_JWT_HEADER}`, - ); + throw new AuthenticationError('Missing Google IAP header'); } let payload: TokenPayload; diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts index b1b2a3f66e..c01db3a853 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts @@ -18,6 +18,7 @@ import express from 'express'; import request from 'supertest'; import { AuthResolverContext } from '../types'; import { GcpIapProvider } from './provider'; +import { DEFAULT_IAP_JWT_HEADER } from './types'; beforeEach(() => { jest.clearAllMocks(); @@ -28,44 +29,47 @@ describe('GcpIapProvider', () => { const signInResolver = jest.fn(); const tokenValidator = jest.fn(); - it('runs the happy path', async () => { - const provider = new GcpIapProvider({ - authHandler, - signInResolver, - tokenValidator, - resolverContext: {} as AuthResolverContext, - }); + it.each([undefined, 'x-custom-header'])( + 'runs the happy path', + async jwtHeader => { + const provider = new GcpIapProvider({ + authHandler, + signInResolver, + tokenValidator, + resolverContext: {} as AuthResolverContext, + jwtHeader: jwtHeader, + }); - // { "sub": "user:default/me", "ent": ["group:default/home"] } - const backstageToken = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc'; - const iapToken = { sub: 's', email: 'e@mail.com' }; + // { "sub": "user:default/me", "ent": ["group:default/home"] } + const backstageToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc'; + const iapToken = { sub: 's', email: 'e@mail.com' }; - authHandler.mockResolvedValueOnce({ email: 'e@mail.com' }); - signInResolver.mockResolvedValueOnce({ token: backstageToken }); - tokenValidator.mockResolvedValueOnce(iapToken); + authHandler.mockResolvedValueOnce({ email: 'e@mail.com' }); + signInResolver.mockResolvedValueOnce({ token: backstageToken }); + tokenValidator.mockResolvedValueOnce(iapToken); - const app = express(); - app.use('/refresh', provider.refresh.bind(provider)); + const app = express(); + app.use('/refresh', provider.refresh.bind(provider)); - const response = await request(app) - .get('/refresh') - .set('x-goog-iap-jwt-assertion', 'token'); + const header = jwtHeader || DEFAULT_IAP_JWT_HEADER; + const response = await request(app).get('/refresh').set(header, 'token'); - expect(response.status).toBe(200); - expect(response.get('content-type')).toBe( - 'application/json; charset=utf-8', - ); - expect(response.body).toEqual({ - backstageIdentity: { - token: backstageToken, - identity: { - type: 'user', - userEntityRef: 'user:default/me', - ownershipEntityRefs: ['group:default/home'], + expect(response.status).toBe(200); + expect(response.get('content-type')).toBe( + 'application/json; charset=utf-8', + ); + expect(response.body).toEqual({ + backstageIdentity: { + token: backstageToken, + identity: { + type: 'user', + userEntityRef: 'user:default/me', + ownershipEntityRefs: ['group:default/home'], + }, }, - }, - providerInfo: { iapToken }, - }); - }); + providerInfo: { iapToken }, + }); + }, + ); }); diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index 500b062d5b..7e77853737 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -29,24 +29,27 @@ import { defaultAuthHandler, parseRequestToken, } from './helpers'; -import { GcpIapResponse, GcpIapResult, IAP_JWT_HEADER } from './types'; +import { GcpIapResponse, GcpIapResult, DEFAULT_IAP_JWT_HEADER } from './types'; export class GcpIapProvider implements AuthProviderRouteHandlers { private readonly authHandler: AuthHandler; private readonly signInResolver: SignInResolver; private readonly tokenValidator: (token: string) => Promise; private readonly resolverContext: AuthResolverContext; + private readonly jwtHeader: string; constructor(options: { authHandler: AuthHandler; signInResolver: SignInResolver; tokenValidator: (token: string) => Promise; resolverContext: AuthResolverContext; + jwtHeader?: string; }) { this.authHandler = options.authHandler; this.signInResolver = options.signInResolver; this.tokenValidator = options.tokenValidator; this.resolverContext = options.resolverContext; + this.jwtHeader = options?.jwtHeader || DEFAULT_IAP_JWT_HEADER; } async start() {} @@ -55,7 +58,7 @@ export class GcpIapProvider implements AuthProviderRouteHandlers { async refresh(req: express.Request, res: express.Response): Promise { const result = await parseRequestToken( - req.header(IAP_JWT_HEADER), + req.header(this.jwtHeader), this.tokenValidator, ); @@ -103,6 +106,7 @@ export const gcpIap = createAuthProviderIntegration({ }) { return ({ config, resolverContext }) => { const audience = config.getString('audience'); + const jwtHeader = config.getOptionalString('jwtHeader'); const authHandler = options.authHandler ?? defaultAuthHandler; const signInResolver = options.signIn.resolver; @@ -113,6 +117,7 @@ export const gcpIap = createAuthProviderIntegration({ signInResolver, tokenValidator, resolverContext, + jwtHeader, }); }; }, diff --git a/plugins/auth-backend/src/providers/gcp-iap/types.ts b/plugins/auth-backend/src/providers/gcp-iap/types.ts index 25828b7d51..6519de64ac 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/types.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/types.ts @@ -20,7 +20,7 @@ import { AuthResponse } from '../types'; /** * The header name used by the IAP. */ -export const IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion'; +export const DEFAULT_IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion'; /** * The data extracted from an IAP token.