refactor provider unit tests
Signed-off-by: Ruben Vallejo <rvallejo@vmware.com>
This commit is contained in:
@@ -1,392 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { pinniped } from '.';
|
||||
import { AuthProviderRouteHandlers } from '../types';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { rest } from 'msw';
|
||||
import { Server } from 'http';
|
||||
import { AddressInfo } from 'net';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import passport from 'passport';
|
||||
import session from 'express-session';
|
||||
import Router from 'express-promise-router';
|
||||
// import fetch from 'node-fetch';
|
||||
import { SignJWT, exportJWK, generateKeyPair, importJWK } from 'jose';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
describe('pinniped.create', () => {
|
||||
const fakePinnipedSupervisor = setupServer();
|
||||
setupRequestMockHandlers(fakePinnipedSupervisor);
|
||||
const nonce = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 16 bytes of zeros in base64
|
||||
const state = Buffer.from(
|
||||
`nonce=${encodeURIComponent(nonce)}&env=development`,
|
||||
).toString('hex');
|
||||
|
||||
let app: express.Express;
|
||||
let provider: AuthProviderRouteHandlers;
|
||||
let backstageServer: Server;
|
||||
let appUrl: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const secret = 'secret';
|
||||
app = express()
|
||||
.use(cookieParser(secret))
|
||||
.use(
|
||||
session({
|
||||
secret,
|
||||
saveUninitialized: false,
|
||||
resave: false,
|
||||
cookie: { secure: false },
|
||||
}),
|
||||
)
|
||||
.use(passport.initialize())
|
||||
.use(passport.session());
|
||||
await new Promise(resolve => {
|
||||
backstageServer = app.listen(0, '0.0.0.0', () => {
|
||||
appUrl = `http://127.0.0.1:${
|
||||
(backstageServer.address() as AddressInfo).port
|
||||
}`;
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
fakePinnipedSupervisor.use(
|
||||
rest.all(`${appUrl}/*`, req => req.passthrough()),
|
||||
rest.all(
|
||||
'https://pinniped.test/.well-known/openid-configuration',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
issuer: 'https://pinniped.test',
|
||||
authorization_endpoint: 'https://pinniped.test/oauth2/authorize',
|
||||
token_endpoint: 'https://pinniped.test/oauth2/token',
|
||||
jwks_uri: 'https://pinniped.test/jwks.json',
|
||||
response_types_supported: ['code', 'access_token'],
|
||||
response_modes_supported: ['query', 'form_post'],
|
||||
subject_types_supported: ['public'],
|
||||
token_endpoint_auth_methods_supported: ['client_secret_basic'],
|
||||
id_token_signing_alg_values_supported: [
|
||||
'RS256',
|
||||
'RS512',
|
||||
'HS256',
|
||||
'ES256',
|
||||
],
|
||||
token_endpoint_auth_signing_alg_values_supported: [
|
||||
'RS256',
|
||||
'RS512',
|
||||
'HS256',
|
||||
],
|
||||
request_object_signing_alg_values_supported: [
|
||||
'RS256',
|
||||
'RS512',
|
||||
'HS256',
|
||||
],
|
||||
scopes_supported: [
|
||||
'openid',
|
||||
'offline_access',
|
||||
'pinniped:request-audience',
|
||||
'username',
|
||||
'groups',
|
||||
],
|
||||
claims_supported: [
|
||||
'username',
|
||||
'groups',
|
||||
'additionalClaims',
|
||||
'sub',
|
||||
],
|
||||
code_challenge_methods_supported: ['S256'],
|
||||
'discovery.supervisor.pinniped.dev/v1alpha1': {
|
||||
pinniped_identity_providers_endpoint:
|
||||
'https://pinniped.test/v1alpha1/pinniped_identity_providers',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
provider = pinniped.create()({
|
||||
providerId: 'pinniped',
|
||||
globalConfig: {
|
||||
baseUrl: `${appUrl}/api/auth`,
|
||||
appUrl,
|
||||
isOriginAllowed: _ => true,
|
||||
},
|
||||
config: new ConfigReader({
|
||||
development: {
|
||||
federationDomain: 'https://pinniped.test',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
},
|
||||
}),
|
||||
logger: getVoidLogger(),
|
||||
resolverContext: {
|
||||
issueToken: async _ => ({ token: '' }),
|
||||
findCatalogUser: async _ => ({
|
||||
entity: {
|
||||
apiVersion: '',
|
||||
kind: '',
|
||||
metadata: { name: '' },
|
||||
},
|
||||
}),
|
||||
signInWithCatalogUser: async _ => ({ token: '' }),
|
||||
},
|
||||
});
|
||||
const router = Router();
|
||||
router
|
||||
.use('/api/auth/pinniped/start', provider.start.bind(provider))
|
||||
.use(
|
||||
'/api/auth/pinniped/handler/frame',
|
||||
provider.frameHandler.bind(provider),
|
||||
);
|
||||
app.use(router);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
backstageServer.close();
|
||||
});
|
||||
|
||||
// also include an audience parameter and only assert on the id_token
|
||||
|
||||
// repurpose this test
|
||||
it('/handler/frame exchanges authorization codes from /start for access tokens', async () => {
|
||||
const agent = request.agent('');
|
||||
// make a /start request
|
||||
// add query parameter for the audience
|
||||
const startResponse = await agent.get(
|
||||
`${appUrl}/api/auth/pinniped/start?env=development`,
|
||||
);
|
||||
// follow the redirect to pinniped authorization endpoint
|
||||
fakePinnipedSupervisor.use(
|
||||
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()),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
const authorizationResponse = await agent.get(
|
||||
startResponse.header.location,
|
||||
);
|
||||
|
||||
// follow the redirect back to /handler/frame
|
||||
const testTokenMetadata = {
|
||||
sub: 'test',
|
||||
iss: 'https://pinniped.test',
|
||||
iat: Date.now(),
|
||||
aud: 'clientId',
|
||||
exp: Date.now() + 10000,
|
||||
};
|
||||
|
||||
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 jwt = 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));
|
||||
|
||||
fakePinnipedSupervisor.use(
|
||||
rest.post('https://pinniped.test/oauth2/token', async (req, res, ctx) =>
|
||||
res(
|
||||
// TODO verify client ID + secret, etc -- real token endpoint
|
||||
new URLSearchParams(await req.text()).get('code') ===
|
||||
'authorization_code'
|
||||
? ctx.json({
|
||||
access_token: 'accessToken',
|
||||
id_token: jwt,
|
||||
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,
|
||||
);
|
||||
// our assertion doesnt include a scope but the return type does...might need to change our assertion?
|
||||
expect(handlerResponse.text).toContain(
|
||||
encodeURIComponent(
|
||||
JSON.stringify({
|
||||
type: 'authorization_response',
|
||||
response: {
|
||||
providerInfo: {
|
||||
idToken: 'accessToken',
|
||||
scope: 'testScope',
|
||||
},
|
||||
profile: {},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}, 70000);
|
||||
|
||||
describe('#frameHandler', () => {
|
||||
it.skip('performs an rfc 8693 token exchange after getting access token', async () => {
|
||||
const agent = request.agent('');
|
||||
|
||||
// make a start request with an audience query
|
||||
const startResponse = await agent.get(
|
||||
`${appUrl}/api/auth/pinniped/start?env=development&aud=testCluster`,
|
||||
);
|
||||
|
||||
const testTokenMetadata = {
|
||||
sub: 'test',
|
||||
iss: 'https://pinniped.test',
|
||||
iat: Date.now(),
|
||||
aud: 'clientId',
|
||||
exp: Date.now() + 10000,
|
||||
};
|
||||
|
||||
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 jwt = 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));
|
||||
|
||||
// follow the redirect to pinniped authorization endpoint
|
||||
fakePinnipedSupervisor.use(
|
||||
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.post('https://pinniped.test/oauth2/token', async (req, res, ctx) =>
|
||||
res(
|
||||
ctx.json(
|
||||
new URLSearchParams(await req.text()).get('grant_type') ===
|
||||
'urn:ietf:params:oauth:grant-type:token-exchange'
|
||||
? { access_token: 'accessToken', scope: 'test-scope' }
|
||||
: {
|
||||
accessToken: 'accessToken',
|
||||
scope: 'test-scope',
|
||||
idToken: jwt,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
rest.get('https://pinniped.test/jwks.json', async (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ keys: [{ ...publicKey }] })),
|
||||
),
|
||||
);
|
||||
|
||||
// fakePinnipedSupervisor.use(
|
||||
// rest.post('https://pinniped.test/oauth2/token', async (req, res, ctx) =>
|
||||
// res(
|
||||
// ctx.json(
|
||||
// new URLSearchParams(await req.text()).get('grant_type') ===
|
||||
// 'urn:ietf:params:oauth:grant-type:token-exchange'
|
||||
// ? { access_token: 'accessToken' }
|
||||
// : { id_token: 'clusterToken' },
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
|
||||
const authorizationResponse = await agent.get(
|
||||
startResponse.header.location,
|
||||
);
|
||||
|
||||
const handlerResponse = await agent.get(
|
||||
authorizationResponse.header.location,
|
||||
);
|
||||
|
||||
// const responsePromise = request(app)
|
||||
// .get(
|
||||
// '/api/auth/pinniped/handler/frame?' +
|
||||
// 'code=pin_ac_xU69qZGejOCu8Loz5iOD6Bm25SgQewmT0VVE1hOAQzA.WzxrI9bCder5UJHtCOX_yEnsM2OVh8pVSFI7NPs5yUM&' +
|
||||
// 'scope=openid+pinniped%3Arequest-audience+username&' +
|
||||
// `state=${state}`,
|
||||
// )
|
||||
// .set(
|
||||
// 'Cookie',
|
||||
// `pinniped-nonce=${nonce}; ` +
|
||||
// 'connect.sid=s:p3_hKHiFr_i58jyTPIZxtWN9pejiOujD.SN2irLt6oIL18v0GzGCPO1sibEmzybiVlT9ca3ZjT68',
|
||||
// );
|
||||
|
||||
// const reqUrl = new URL(responsePromise.url);
|
||||
// reqUrl.search = '';
|
||||
|
||||
// fakePinnipedSupervisor.use(
|
||||
// rest.all(reqUrl.toString(), req => req.passthrough()),
|
||||
// );
|
||||
|
||||
expect(handlerResponse.text).toContain(
|
||||
encodeURIComponent(
|
||||
JSON.stringify({
|
||||
type: 'authorization_response',
|
||||
response: {
|
||||
providerInfo: {
|
||||
accessToken: 'accessToken',
|
||||
scope: 'test-scope',
|
||||
idToken: jwt,
|
||||
},
|
||||
profile: {},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,69 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
// import { OidcAuthResult } from '../oidc';
|
||||
// import { OidcAuthProvider } from '../oidc/provider';
|
||||
// import { OAuthAdapter, OAuthEnvironmentHandler } from '../../lib/oauth';
|
||||
// import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
|
||||
// import { AuthHandler, SignInResolver } from '../types';
|
||||
// import { PinnipedAuthProvider } from './provider';
|
||||
|
||||
/**
|
||||
* Auth provider integration for Pinniped
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
// export const pinniped = createAuthProviderIntegration({
|
||||
// create(options?: {
|
||||
// authHandler?: AuthHandler<OidcAuthResult>;
|
||||
// signIn?: {
|
||||
// resolver: SignInResolver<OidcAuthResult>;
|
||||
// };
|
||||
// }) {
|
||||
// return ({ providerId, globalConfig, config, resolverContext }) =>
|
||||
// OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
// const clientId = envConfig.getString('clientId');
|
||||
// const clientSecret = envConfig.getString('clientSecret');
|
||||
// const customCallbackUrl = envConfig.getOptionalString('callbackUrl');
|
||||
// const callbackUrl =
|
||||
// customCallbackUrl ||
|
||||
// `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
// const metadataUrl = `${envConfig.getString(
|
||||
// 'federationDomain',
|
||||
// )}/.well-known/openid-configuration`;
|
||||
// const federationDomain = envConfig.getString('federationDomain');
|
||||
// const tokenSignedResponseAlg = 'ES256';
|
||||
// const prompt = 'auto';
|
||||
// const authHandler: AuthHandler<OidcAuthResult> = async ({
|
||||
// userinfo,
|
||||
// }) => ({
|
||||
// profile: {},
|
||||
// });
|
||||
|
||||
// // const provider = new OidcAuthProvider({
|
||||
// // clientId,
|
||||
// // clientSecret,
|
||||
// // callbackUrl,
|
||||
// // tokenSignedResponseAlg,
|
||||
// // metadataUrl,
|
||||
// // prompt,
|
||||
// // signInResolver: options?.signIn?.resolver,
|
||||
// // authHandler,
|
||||
// // resolverContext,
|
||||
// // });
|
||||
|
||||
// const provider = new PinnipedAuthProvider({
|
||||
// federationDomain,
|
||||
// clientId,
|
||||
// clientSecret,
|
||||
// });
|
||||
|
||||
// return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
// providerId,
|
||||
// callbackUrl,
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
|
||||
export { pinniped } from './provider';
|
||||
|
||||
@@ -26,14 +26,27 @@ import { rest } from 'msw';
|
||||
import express from 'express';
|
||||
import { UnsecuredJWT } from 'jose';
|
||||
import { OAuthState } from '../../lib/oauth';
|
||||
import { Server } from 'http';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import session from 'express-session';
|
||||
import passport from 'passport';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import Router from 'express-promise-router';
|
||||
import { pinniped } from '.';
|
||||
import { AuthProviderRouteHandlers } from '../types';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { AddressInfo } from 'net';
|
||||
import request from 'supertest';
|
||||
import { SignJWT, exportJWK, generateKeyPair, importJWK } from 'jose';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
describe('PinnipedAuthProvider', () => {
|
||||
let provider: PinnipedAuthProvider;
|
||||
let startRequest: OAuthStartRequest;
|
||||
let fakeSession: Record<string, any>;
|
||||
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
const fakePinnipedSupervisor = setupServer();
|
||||
setupRequestMockHandlers(fakePinnipedSupervisor);
|
||||
|
||||
const issuerMetadata = {
|
||||
issuer: 'https://pinniped.test',
|
||||
@@ -63,8 +76,8 @@ describe('PinnipedAuthProvider', () => {
|
||||
|
||||
const clientMetadata: PinnipedOptions = {
|
||||
federationDomain: 'https://federationDomain.test',
|
||||
clientId: 'clientId.test',
|
||||
clientSecret: 'secret.test',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'secret',
|
||||
callbackUrl: 'https://federationDomain.test/callback',
|
||||
tokenSignedResponseAlg: 'none',
|
||||
};
|
||||
@@ -96,7 +109,7 @@ describe('PinnipedAuthProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
worker.use(
|
||||
fakePinnipedSupervisor.use(
|
||||
rest.all(
|
||||
'https://federationDomain.test/.well-known/openid-configuration',
|
||||
(_req, res, ctx) =>
|
||||
@@ -148,6 +161,24 @@ describe('PinnipedAuthProvider', () => {
|
||||
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 = {};
|
||||
@@ -196,7 +227,7 @@ describe('PinnipedAuthProvider', () => {
|
||||
const startResponse = await provider.start(startRequest);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
expect(searchParams.get('client_id')).toBe('clientId.test');
|
||||
expect(searchParams.get('client_id')).toBe('clientId');
|
||||
});
|
||||
|
||||
it('passes callback URL', async () => {
|
||||
@@ -259,7 +290,6 @@ describe('PinnipedAuthProvider', () => {
|
||||
let handlerRequest: express.Request;
|
||||
|
||||
beforeEach(() => {
|
||||
// we want to somehow pass an authentication header in this request for testing purposes
|
||||
handlerRequest = {
|
||||
method: 'GET',
|
||||
url: `https://test?code=authorization_code&state=${encodeState(
|
||||
@@ -345,9 +375,6 @@ describe('PinnipedAuthProvider', () => {
|
||||
} as unknown as OAuthStartRequest),
|
||||
).rejects.toThrow('authentication requires session support');
|
||||
});
|
||||
|
||||
// if no valid key is in the jwks array or even an unsigned jwt
|
||||
// have pinniped reject your clientid and secret possibly as a unit test
|
||||
});
|
||||
|
||||
describe('#refresh', () => {
|
||||
@@ -370,5 +397,170 @@ describe('PinnipedAuthProvider', () => {
|
||||
|
||||
expect(response.providerInfo.accessToken).toBe('accessToken');
|
||||
});
|
||||
|
||||
it('gets an id_token', async () => {
|
||||
const { response } = await provider.refresh(refreshRequest);
|
||||
|
||||
expect(response.providerInfo.idToken).toBe(idToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pinniped.create', () => {
|
||||
let app: express.Express;
|
||||
let providerRouteHandler: AuthProviderRouteHandlers;
|
||||
let backstageServer: Server;
|
||||
let appUrl: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const secret = 'secret';
|
||||
app = express()
|
||||
.use(cookieParser(secret))
|
||||
.use(
|
||||
session({
|
||||
secret,
|
||||
saveUninitialized: false,
|
||||
resave: false,
|
||||
cookie: { secure: false },
|
||||
}),
|
||||
)
|
||||
.use(passport.initialize())
|
||||
.use(passport.session());
|
||||
await new Promise(resolve => {
|
||||
backstageServer = app.listen(0, '0.0.0.0', () => {
|
||||
appUrl = `http://127.0.0.1:${
|
||||
(backstageServer.address() as AddressInfo).port
|
||||
}`;
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
fakePinnipedSupervisor.use(
|
||||
rest.all(`${appUrl}/*`, req => req.passthrough()),
|
||||
);
|
||||
providerRouteHandler = pinniped.create()({
|
||||
providerId: 'pinniped',
|
||||
globalConfig: {
|
||||
baseUrl: `${appUrl}/api/auth`,
|
||||
appUrl,
|
||||
isOriginAllowed: _ => true,
|
||||
},
|
||||
config: new ConfigReader({
|
||||
development: {
|
||||
federationDomain: 'https://federationDomain.test',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
},
|
||||
}),
|
||||
logger: getVoidLogger(),
|
||||
resolverContext: {
|
||||
issueToken: async _ => ({ token: '' }),
|
||||
findCatalogUser: async _ => ({
|
||||
entity: {
|
||||
apiVersion: '',
|
||||
kind: '',
|
||||
metadata: { name: '' },
|
||||
},
|
||||
}),
|
||||
signInWithCatalogUser: async _ => ({ token: '' }),
|
||||
},
|
||||
});
|
||||
const router = Router();
|
||||
router
|
||||
.use(
|
||||
'/api/auth/pinniped/start',
|
||||
providerRouteHandler.start.bind(providerRouteHandler),
|
||||
)
|
||||
.use(
|
||||
'/api/auth/pinniped/handler/frame',
|
||||
providerRouteHandler.frameHandler.bind(providerRouteHandler),
|
||||
);
|
||||
app.use(router);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
backstageServer.close();
|
||||
});
|
||||
|
||||
it('/handler/frame exchanges authorization codes from #start for Cluster Specific ID tokens', 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
|
||||
const startResponse = await agent.get(
|
||||
`${appUrl}/api/auth/pinniped/start?env=development&audience=test_cluster`,
|
||||
);
|
||||
// follow redirect to authorization endpoint
|
||||
const authorizationResponse = await agent.get(
|
||||
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,
|
||||
);
|
||||
|
||||
expect(handlerResponse.text).toContain(
|
||||
encodeURIComponent(
|
||||
JSON.stringify({
|
||||
type: 'authorization_response',
|
||||
response: {
|
||||
providerInfo: {
|
||||
accessToken: 'accessToken',
|
||||
scope: 'testScope',
|
||||
idToken: clusterScopedIdToken,
|
||||
},
|
||||
profile: {},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}, 70000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,6 @@ import { OAuthStartResponse } from '../types';
|
||||
import express from 'express';
|
||||
import { OAuthAdapter, OAuthEnvironmentHandler } from '../../lib/oauth';
|
||||
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
type OidcImpl = {
|
||||
strategy: OidcStrategy<undefined, Client>;
|
||||
@@ -55,23 +54,15 @@ export type PinnipedOptions = OAuthProviderOptions & {
|
||||
|
||||
export class PinnipedAuthProvider implements OAuthHandlers {
|
||||
private readonly implementation: Promise<OidcImpl>;
|
||||
private readonly clientId: string;
|
||||
private readonly clientSecret: string;
|
||||
private readonly federationDomain: string;
|
||||
|
||||
constructor(options: PinnipedOptions) {
|
||||
this.implementation = this.setupStrategy(options);
|
||||
this.clientId = options.clientId;
|
||||
this.clientSecret = options.clientSecret;
|
||||
this.federationDomain = options.federationDomain;
|
||||
}
|
||||
|
||||
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
|
||||
const { strategy } = await this.implementation;
|
||||
|
||||
const stringifiedAudience = req.query?.audience as string;
|
||||
const state = { ...req.state, audience: stringifiedAudience };
|
||||
|
||||
const options: Record<string, string> = {
|
||||
scope:
|
||||
req.scope || 'openid pinniped:request-audience username offline_access',
|
||||
@@ -88,33 +79,10 @@ export class PinnipedAuthProvider implements OAuthHandlers {
|
||||
});
|
||||
}
|
||||
|
||||
private async rfc8693TokenExchange({ accessToken, audience }) {
|
||||
const tokenEndpoint = `${this.federationDomain}/oauth2/token`;
|
||||
const authString: string = `${this.clientId}:${this.clientSecret}`;
|
||||
const encodedAuthString = Buffer.from(authString, 'base64');
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'x-www-form-urlencoded',
|
||||
Authorization: `Basic ${encodedAuthString}`,
|
||||
},
|
||||
body: `grant_type=urn:ietf:params:oauth:grant-type:token-exchange
|
||||
&subject_token=${accessToken}
|
||||
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
|
||||
&requested_token_type=urn:ietf:params:oauth:token-type:jwt
|
||||
&audience=${audience}`,
|
||||
};
|
||||
const response = await fetch(tokenEndpoint, requestOptions);
|
||||
return response.idToken;
|
||||
}
|
||||
|
||||
async handler(
|
||||
req: express.Request,
|
||||
): Promise<{ response: OAuthResponse; refreshToken?: string }> {
|
||||
const { strategy, client } = await this.implementation;
|
||||
|
||||
// if we dont add a base url our integration fails with invalid_url error in integration test
|
||||
const { searchParams } = new URL(req.url, 'https://pinniped.com');
|
||||
const stateParam = searchParams.get('state');
|
||||
const audience = stateParam ? readState(stateParam).audience : undefined;
|
||||
@@ -179,7 +147,8 @@ export class PinnipedAuthProvider implements OAuthHandlers {
|
||||
response: {
|
||||
providerInfo: {
|
||||
accessToken: tokenset.access_token!,
|
||||
scope: 'none',
|
||||
scope: tokenset.scope!,
|
||||
idToken: tokenset.id_token,
|
||||
},
|
||||
profile: {},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user