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 <kunickiaj@gmail.com>
This commit is contained in:
Adam Kunicki
2022-11-11 20:48:15 -08:00
parent afa90a3e51
commit 89d705e806
7 changed files with 57 additions and 44 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Add support for custom JWT header name in GCP IAP auth.
+1
View File
@@ -26,6 +26,7 @@ auth:
providers:
gcp-iap:
audience: '/projects/<project number>/global/backendServices/<backend service id>'
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.
@@ -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',
});
});
@@ -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<TokenPayload>,
): Promise<GcpIapResult> {
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;
@@ -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 },
});
},
);
});
@@ -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<GcpIapResult>;
private readonly signInResolver: SignInResolver<GcpIapResult>;
private readonly tokenValidator: (token: string) => Promise<TokenPayload>;
private readonly resolverContext: AuthResolverContext;
private readonly jwtHeader: string;
constructor(options: {
authHandler: AuthHandler<GcpIapResult>;
signInResolver: SignInResolver<GcpIapResult>;
tokenValidator: (token: string) => Promise<TokenPayload>;
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<void> {
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,
});
};
},
@@ -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.