clean up tests
and remove tokenSignedAlg option from pinniped, since it's not actually configurable by end users. This means that all the tests use ID tokens signed with a real JWK. Signed-off-by: Jamie Klassen <jklassen@vmware.com>
This commit is contained in:
committed by
Ruben Vallejo
parent
70a3c2631f
commit
8d3e9c7277
@@ -36,22 +36,16 @@ import { AuthProviderRouteHandlers } from '../types';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { AddressInfo } from 'net';
|
||||
import request from 'supertest';
|
||||
import {
|
||||
SignJWT,
|
||||
exportJWK,
|
||||
generateKeyPair,
|
||||
importJWK,
|
||||
UnsecuredJWT,
|
||||
} from 'jose';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { JWK, SignJWT, exportJWK, generateKeyPair } from 'jose';
|
||||
|
||||
describe('PinnipedAuthProvider', () => {
|
||||
let provider: PinnipedAuthProvider;
|
||||
let startRequest: OAuthStartRequest;
|
||||
let fakeSession: Record<string, any>;
|
||||
let idToken: string;
|
||||
let publicKey: JWK;
|
||||
let oauthState: OAuthState;
|
||||
|
||||
const fakePinnipedSupervisor = setupServer();
|
||||
setupRequestMockHandlers(fakePinnipedSupervisor);
|
||||
const mswServer = setupServer();
|
||||
setupRequestMockHandlers(mswServer);
|
||||
|
||||
const issuerMetadata = {
|
||||
issuer: 'https://pinniped.test',
|
||||
@@ -79,43 +73,37 @@ describe('PinnipedAuthProvider', () => {
|
||||
request_object_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'],
|
||||
};
|
||||
|
||||
const clientMetadata: PinnipedProviderOptions = {
|
||||
const pinnipedProviderOptions: PinnipedProviderOptions = {
|
||||
federationDomain: 'https://federationDomain.test',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'secret',
|
||||
callbackUrl: 'https://federationDomain.test/callback',
|
||||
tokenSignedResponseAlg: 'none',
|
||||
};
|
||||
|
||||
const testTokenMetadata = {
|
||||
sub: 'test',
|
||||
iss: 'https://pinniped.test',
|
||||
iat: Date.now(),
|
||||
aud: clientMetadata.clientId,
|
||||
exp: Date.now() + 10000,
|
||||
};
|
||||
|
||||
const idToken = new UnsecuredJWT(testTokenMetadata)
|
||||
.setIssuer(testTokenMetadata.iss)
|
||||
.setAudience(testTokenMetadata.aud)
|
||||
.setSubject(testTokenMetadata.sub)
|
||||
.setIssuedAt(testTokenMetadata.iat)
|
||||
.setExpirationTime(testTokenMetadata.exp)
|
||||
.encode();
|
||||
|
||||
const oauthState: OAuthState = {
|
||||
nonce: 'nonce',
|
||||
env: 'env',
|
||||
origin: 'undefined',
|
||||
callbackUrl: 'https://backstage.test/callback',
|
||||
};
|
||||
|
||||
const clusterScopedIdToken = 'dummy-token';
|
||||
|
||||
beforeEach(() => {
|
||||
beforeAll(async () => {
|
||||
const keyPair = await generateKeyPair('RS256');
|
||||
const privateKey = await exportJWK(keyPair.privateKey);
|
||||
publicKey = await exportJWK(keyPair.publicKey);
|
||||
publicKey.alg = privateKey.alg = 'RS256';
|
||||
|
||||
idToken = await new SignJWT({
|
||||
sub: 'test',
|
||||
iss: 'https://pinniped.test',
|
||||
iat: Date.now(),
|
||||
aud: pinnipedProviderOptions.clientId,
|
||||
exp: Date.now() + 10000,
|
||||
})
|
||||
.setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid })
|
||||
.sign(keyPair.privateKey);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
fakePinnipedSupervisor.use(
|
||||
rest.all(
|
||||
mswServer.use(
|
||||
rest.get(
|
||||
'https://federationDomain.test/.well-known/openid-configuration',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
@@ -124,6 +112,27 @@ describe('PinnipedAuthProvider', () => {
|
||||
ctx.json(issuerMetadata),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://pinniped.test/oauth2/authorize',
|
||||
async (req, res, ctx) => {
|
||||
const callbackUrl = new URL(
|
||||
req.url.searchParams.get('redirect_uri')!,
|
||||
);
|
||||
callbackUrl.searchParams.set('code', 'authorization_code');
|
||||
callbackUrl.searchParams.set(
|
||||
'state',
|
||||
req.url.searchParams.get('state')!,
|
||||
);
|
||||
callbackUrl.searchParams.set('scope', 'test-scope');
|
||||
return res(
|
||||
ctx.status(302),
|
||||
ctx.set('Location', callbackUrl.toString()),
|
||||
);
|
||||
},
|
||||
),
|
||||
rest.get('https://pinniped.test/jwks.json', async (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ keys: [{ ...publicKey }] })),
|
||||
),
|
||||
rest.post('https://pinniped.test/oauth2/token', async (req, res, ctx) => {
|
||||
const formBody = new URLSearchParams(await req.text());
|
||||
const isGrantTypeTokenExchange =
|
||||
@@ -151,53 +160,30 @@ describe('PinnipedAuthProvider', () => {
|
||||
: ctx.status(401),
|
||||
);
|
||||
}),
|
||||
rest.get('https://pinniped.test/idp/userinfo.openid', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
iss: 'https://pinniped.test',
|
||||
sub: 'test',
|
||||
aud: clientMetadata.clientId,
|
||||
claims: {
|
||||
given_name: 'Givenname',
|
||||
family_name: 'Familyname',
|
||||
email: 'user@example.com',
|
||||
},
|
||||
}),
|
||||
ctx.status(200),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://pinniped.test/oauth2/authorize',
|
||||
async (req, res, ctx) => {
|
||||
const callbackUrl = new URL(
|
||||
req.url.searchParams.get('redirect_uri')!,
|
||||
);
|
||||
callbackUrl.searchParams.set('code', 'authorization_code');
|
||||
callbackUrl.searchParams.set(
|
||||
'state',
|
||||
req.url.searchParams.get('state')!,
|
||||
);
|
||||
callbackUrl.searchParams.set('scope', 'test-scope');
|
||||
return res(
|
||||
ctx.status(302),
|
||||
ctx.set('Location', callbackUrl.toString()),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
fakeSession = {};
|
||||
startRequest = {
|
||||
session: fakeSession,
|
||||
method: 'GET',
|
||||
url: 'test',
|
||||
state: oauthState,
|
||||
} as unknown as OAuthStartRequest;
|
||||
oauthState = {
|
||||
nonce: 'nonce',
|
||||
env: 'env',
|
||||
};
|
||||
|
||||
provider = new PinnipedAuthProvider(clientMetadata);
|
||||
provider = new PinnipedAuthProvider(pinnipedProviderOptions);
|
||||
});
|
||||
|
||||
describe('#start', () => {
|
||||
let fakeSession: Record<string, any>;
|
||||
let startRequest: OAuthStartRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeSession = {};
|
||||
startRequest = {
|
||||
session: fakeSession,
|
||||
method: 'GET',
|
||||
url: 'test',
|
||||
state: oauthState,
|
||||
} as unknown as OAuthStartRequest;
|
||||
});
|
||||
|
||||
it('redirects to authorization endpoint returned from OIDC metadata endpoint', async () => {
|
||||
const startResponse = await provider.start(startRequest);
|
||||
const url = new URL(startResponse.url);
|
||||
@@ -207,14 +193,14 @@ describe('PinnipedAuthProvider', () => {
|
||||
expect(url.pathname).toBe('/oauth2/authorize');
|
||||
});
|
||||
|
||||
it('initiates an authorization code grant', async () => {
|
||||
it('initiates authorization code grant', async () => {
|
||||
const startResponse = await provider.start(startRequest);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
expect(searchParams.get('response_type')).toBe('code');
|
||||
});
|
||||
|
||||
it('passes audience query parameter into OAuthState in the redirect url when defined in the request', async () => {
|
||||
it('persists audience parameter in oauth state', async () => {
|
||||
startRequest.query = { audience: 'test-cluster' };
|
||||
const startResponse = await provider.start(startRequest);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
@@ -235,12 +221,12 @@ describe('PinnipedAuthProvider', () => {
|
||||
expect(searchParams.get('client_id')).toBe('clientId');
|
||||
});
|
||||
|
||||
it('passes callback URL', async () => {
|
||||
it('passes callback URL from config', async () => {
|
||||
const startResponse = await provider.start(startRequest);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
expect(searchParams.get('redirect_uri')).toBe(
|
||||
'https://federationDomain.test/callback',
|
||||
'https://backstage.test/callback',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -308,21 +294,21 @@ describe('PinnipedAuthProvider', () => {
|
||||
} as unknown as express.Request;
|
||||
});
|
||||
|
||||
it('exchanges authorization code for a access_token', async () => {
|
||||
it('exchanges authorization code for access token', async () => {
|
||||
const handlerResponse = await provider.handler(handlerRequest);
|
||||
const accessToken = handlerResponse.response.providerInfo.accessToken;
|
||||
|
||||
expect(accessToken).toEqual('accessToken');
|
||||
});
|
||||
|
||||
it('exchanges authorization code for a refresh_token', async () => {
|
||||
it('exchanges authorization code for refresh token', async () => {
|
||||
const handlerResponse = await provider.handler(handlerRequest);
|
||||
const refreshToken = handlerResponse.refreshToken;
|
||||
|
||||
expect(refreshToken).toEqual('refreshToken');
|
||||
});
|
||||
|
||||
it('exchanges authorization_code for a tokenset with a defined scope', async () => {
|
||||
it('returns granted scope', async () => {
|
||||
const handlerResponse = await provider.handler(handlerRequest);
|
||||
const responseScope = handlerResponse.response.providerInfo.scope;
|
||||
|
||||
@@ -349,14 +335,14 @@ describe('PinnipedAuthProvider', () => {
|
||||
expect(responseIdToken).toEqual(clusterScopedIdToken);
|
||||
});
|
||||
|
||||
it('request errors out with missing authorization_code parameter in the request_url', async () => {
|
||||
it('fails without authorization code', async () => {
|
||||
handlerRequest.url = 'https://test.com';
|
||||
return expect(provider.handler(handlerRequest)).rejects.toThrow(
|
||||
'Unexpected redirect',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when request has no state in req_url', async () => {
|
||||
it('fails without oauth state', async () => {
|
||||
return expect(
|
||||
provider.handler({
|
||||
method: 'GET',
|
||||
@@ -397,20 +383,20 @@ describe('PinnipedAuthProvider', () => {
|
||||
expect(refreshToken).toBe('refreshToken');
|
||||
});
|
||||
|
||||
it('gets an access_token', async () => {
|
||||
it('gets access token', async () => {
|
||||
const { response } = await provider.refresh(refreshRequest);
|
||||
|
||||
expect(response.providerInfo.accessToken).toBe('accessToken');
|
||||
});
|
||||
|
||||
it('gets an id_token', async () => {
|
||||
it('gets id token', async () => {
|
||||
const { response } = await provider.refresh(refreshRequest);
|
||||
|
||||
expect(response.providerInfo.idToken).toBe(idToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pinniped.create', () => {
|
||||
describe('integration', () => {
|
||||
let app: express.Express;
|
||||
let providerRouteHandler: AuthProviderRouteHandlers;
|
||||
let backstageServer: Server;
|
||||
@@ -438,9 +424,7 @@ describe('PinnipedAuthProvider', () => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
fakePinnipedSupervisor.use(
|
||||
rest.all(`${appUrl}/*`, req => req.passthrough()),
|
||||
);
|
||||
mswServer.use(rest.all(`${appUrl}/*`, req => req.passthrough()));
|
||||
providerRouteHandler = pinniped.create()({
|
||||
providerId: 'pinniped',
|
||||
globalConfig: {
|
||||
@@ -488,24 +472,10 @@ describe('PinnipedAuthProvider', () => {
|
||||
backstageServer.close();
|
||||
});
|
||||
|
||||
it('/handler/frame exchanges authorization codes from #start for Cluster Specific ID tokens', async () => {
|
||||
it('/handler/frame exchanges authorization code from #start for Cluster Specific ID token', async () => {
|
||||
const agent = request.agent('');
|
||||
const key = await generateKeyPair('ES256');
|
||||
const publicKey = await exportJWK(key.publicKey);
|
||||
const privateKey = await exportJWK(key.privateKey);
|
||||
publicKey.kid = privateKey.kid = uuid();
|
||||
publicKey.alg = privateKey.alg = 'ES256';
|
||||
|
||||
const signedJwt = await new SignJWT(testTokenMetadata)
|
||||
.setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid })
|
||||
.setIssuer(testTokenMetadata.iss)
|
||||
.setAudience(testTokenMetadata.aud)
|
||||
.setSubject(testTokenMetadata.sub)
|
||||
.setIssuedAt(testTokenMetadata.iat)
|
||||
.setExpirationTime(testTokenMetadata.exp)
|
||||
.sign(await importJWK(privateKey));
|
||||
|
||||
// make a /start request with audience parameter
|
||||
// make /start request with audience parameter
|
||||
const startResponse = await agent.get(
|
||||
`${appUrl}/api/auth/pinniped/start?env=development&audience=test_cluster`,
|
||||
);
|
||||
@@ -514,42 +484,6 @@ describe('PinnipedAuthProvider', () => {
|
||||
startResponse.header.location,
|
||||
);
|
||||
// follow redirect to token_endpoint
|
||||
fakePinnipedSupervisor.use(
|
||||
rest.post(
|
||||
'https://pinniped.test/oauth2/token',
|
||||
async (req, res, ctx) => {
|
||||
const formBody = new URLSearchParams(await req.text());
|
||||
const isGrantTypeTokenExchange =
|
||||
formBody.get('grant_type') ===
|
||||
'urn:ietf:params:oauth:grant-type:token-exchange';
|
||||
const hasValidTokenExchangeParams =
|
||||
formBody.get('subject_token') === 'accessToken' &&
|
||||
formBody.get('audience') === 'test_cluster' &&
|
||||
formBody.get('subject_token_type') ===
|
||||
'urn:ietf:params:oauth:token-type:access_token' &&
|
||||
formBody.get('requested_token_type') ===
|
||||
'urn:ietf:params:oauth:token-type:jwt';
|
||||
|
||||
return res(
|
||||
req.headers.get('Authorization') &&
|
||||
(!isGrantTypeTokenExchange || hasValidTokenExchangeParams)
|
||||
? ctx.json({
|
||||
access_token: isGrantTypeTokenExchange
|
||||
? clusterScopedIdToken
|
||||
: 'accessToken',
|
||||
refresh_token: 'refreshToken',
|
||||
...(!isGrantTypeTokenExchange && { id_token: signedJwt }),
|
||||
scope: 'testScope',
|
||||
})
|
||||
: ctx.status(401),
|
||||
);
|
||||
},
|
||||
),
|
||||
rest.get('https://pinniped.test/jwks.json', async (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ keys: [{ ...publicKey }] })),
|
||||
),
|
||||
);
|
||||
|
||||
const handlerResponse = await agent.get(
|
||||
authorizationResponse.header.location,
|
||||
);
|
||||
@@ -569,6 +503,6 @@ describe('PinnipedAuthProvider', () => {
|
||||
}),
|
||||
),
|
||||
);
|
||||
}, 70000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,7 +49,6 @@ export type PinnipedProviderOptions = OAuthProviderOptions & {
|
||||
clientSecret: string;
|
||||
callbackUrl: string;
|
||||
scope?: string;
|
||||
tokenSignedResponseAlg?: string;
|
||||
};
|
||||
|
||||
export class PinnipedAuthProvider implements OAuthHandlers {
|
||||
@@ -169,7 +168,6 @@ export class PinnipedAuthProvider implements OAuthHandlers {
|
||||
client_secret: options.clientSecret,
|
||||
redirect_uris: [options.callbackUrl],
|
||||
response_types: ['code'],
|
||||
id_token_signed_response_alg: options.tokenSignedResponseAlg || 'ES256',
|
||||
scope: options.scope || '',
|
||||
});
|
||||
|
||||
@@ -205,14 +203,12 @@ export const pinniped = createAuthProviderIntegration({
|
||||
const callbackUrl =
|
||||
customCallbackUrl ||
|
||||
`${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
const tokenSignedResponseAlg = 'ES256';
|
||||
|
||||
const provider = new PinnipedAuthProvider({
|
||||
federationDomain,
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
tokenSignedResponseAlg,
|
||||
});
|
||||
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
|
||||
Reference in New Issue
Block a user