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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Add support for custom JWT header name in GCP IAP auth.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user