passing pinniped authprovider #handler responds with Id token test

Signed-off-by: Ruben Vallejo <rvallejo@vmware.com>
This commit is contained in:
Ruben Vallejo
2023-07-24 11:52:18 -04:00
parent 07df9ce153
commit 295dae8ab5
4 changed files with 269 additions and 12 deletions
+2 -1
View File
@@ -66,12 +66,13 @@
"fs-extra": "10.1.0",
"google-auth-library": "^8.0.0",
"jose": "^4.6.0",
"jwt-decode": "^3.1.0",
"jwt-decode": "^3.1.2",
"knex": "^2.0.0",
"lodash": "^4.17.21",
"luxon": "^3.0.0",
"minimatch": "^5.0.0",
"morgan": "^1.10.0",
"njwt": "^2.0.0",
"node-cache": "^5.1.2",
"node-fetch": "^2.6.7",
"openid-client": "^5.2.1",
@@ -14,17 +14,20 @@
* limitations under the License.
*/
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import { OAuthStartRequest } from '../../lib/oauth';
import { OAuthStartRequest, OAuthState, encodeState } from '../../lib/oauth';
import { AuthResolverContext } from '../types';
import { PinnipedAuthProvider, PinnipedOptions } from './provider';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { ClientMetadata, IssuerMetadata } from 'openid-client';
import express from 'express';
import nJwt from 'njwt';
import { UnsecuredJWT } from 'jose';
describe('PinnipedAuthProvider', () => {
let provider: PinnipedAuthProvider;
let startRequest: OAuthStartRequest;
let fakeSession: Record<string, any>;
let provider: PinnipedAuthProvider;
const worker = setupServer();
setupRequestMockHandlers(worker);
@@ -61,20 +64,62 @@ describe('PinnipedAuthProvider', () => {
clientSecret: 'secret.test',
callbackUrl: 'https://federationDomain.test/callback',
resolverContext: {} as AuthResolverContext,
tokenSignedResponseAlg: 'none',
authHandler: async () => ({
profile: {},
}),
};
// const idToken: string = nJwt
// .create(
// {
// iss: 'https://pinniped.test',
// sub: 'test',
// aud: clientMetadata.clientId,
// claims: {
// given_name: 'Givenname',
// family_name: 'Familyname',
// email: 'user@example.com',
// },
// },
// Buffer.from('signing key'),
// )
// .compact();
const sub = 'test';
const iss = 'https://pinniped.test';
const iat = Date.now();
const aud = clientMetadata.clientId;
const exp = Date.now() + 10000;
const idToken = new UnsecuredJWT({ iss, sub, aud, iat, exp })
.setIssuer(iss)
.setAudience(aud)
.setSubject(sub)
.setIssuedAt(iat)
.setExpirationTime(exp)
.encode();
beforeEach(() => {
jest.clearAllMocks();
worker.use(
rest.post('https://pinniped.test/oauth2/token', (req, res, ctx) =>
res(
req.headers.get('Authorization')
? ctx.json({
access_token: 'accessToken',
refresh_token: 'refreshToken',
id_token: idToken,
})
: ctx.status(401),
),
),
);
fakeSession = {};
startRequest = {
session: fakeSession,
method: 'GET',
url: 'test',
} as unknown as OAuthStartRequest;
const handler = jest.fn((_req, res, ctx) => {
return res(
ctx.status(200),
@@ -82,14 +127,12 @@ describe('PinnipedAuthProvider', () => {
ctx.json(issuerMetadata),
);
});
worker.use(
rest.all(
'https://federationDomain.test/.well-known/openid-configuration',
handler,
),
);
provider = new PinnipedAuthProvider(clientMetadata);
});
@@ -123,7 +166,7 @@ describe('PinnipedAuthProvider', () => {
expect(strategy._issuer.token_endpoint).toBe(issuerMetadata.token_endpoint);
});
describe('/start', () => {
describe('#start', () => {
it('redirects to authorization endpoint returned from federationDomain config value', async () => {
const startResponse = await provider.start(startRequest);
const url = new URL(startResponse.url);
@@ -170,5 +213,105 @@ describe('PinnipedAuthProvider', () => {
} as unknown as OAuthStartRequest),
).rejects.toThrow('authentication requires session support');
});
// false passing test: passes because we compare two falsy values undefined and undefined
// need to add the logic that makes this true
it.skip('adds session ID handle to state param', async () => {
const startResponse = await provider.start(startRequest);
// stateParam is empty string
const stateParam = new URL(startResponse.url).searchParams.get('state');
const state = Object.fromEntries(
new URLSearchParams(Buffer.from(stateParam!, 'hex').toString('utf-8')),
);
// handle is currently undefined
const { handle } = fakeSession['oidc:pinniped.test'].state;
console.log(`This is the param:`, stateParam);
// state.handle = undefined
expect(state.handle ?? '').toEqual(handle);
});
});
describe('#handler', () => {
let handlerRequest: express.Request;
beforeEach(() => {
provider = new PinnipedAuthProvider(clientMetadata);
const testState = encodeState({
nonce: 'nonce',
env: 'development',
origin: 'undefined',
});
handlerRequest = {
method: 'GET',
url: `https://test?code=authorization_code&state=${testState}`,
session: {
'oidc:pinniped.test': {
state: testState,
},
},
} as unknown as express.Request;
worker.use(
rest.post('https://pinniped.test/oauth2/token', (req, res, ctx) =>
res(
req.headers.get('Authorization')
? ctx.json({
access_token: 'accessToken',
refresh_token: 'refreshToken',
id_token: idToken,
})
: 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),
),
),
);
});
it('responds with ID token', async () => {
const { response } = await provider.handler(handlerRequest);
expect(response.providerInfo.idToken).toBe(idToken);
});
it.only('decodes profile from ID token', async () => {
const { response } = await provider.handler(handlerRequest);
expect(response.profile).toStrictEqual({
displayName: 'Givenname Familyname',
email: 'user@example.com',
});
});
it('fails when request has no state', async () => {
return expect(
provider.handler({
method: 'GET',
url: `https://test?code=authorization_code}`,
session: {
['oidc:pinniped.test']: {
state: { handle: 'sessionid', code_verifier: 'foo' },
},
},
} as unknown as express.Request),
).rejects.toThrow(
'Authentication rejected, state missing from the response',
);
});
});
});
@@ -23,13 +23,13 @@ import {
import {
OAuthHandlers,
OAuthProviderOptions,
OAuthRefreshRequest,
OAuthResponse,
OAuthStartRequest,
encodeState,
} from '../../lib/oauth';
import {
executeFrameHandlerStrategy,
executeRedirectStrategy,
PassportDoneCallback,
} from '../../lib/passport';
import { AuthResolverContext, OAuthStartResponse } from '../types';
@@ -38,6 +38,9 @@ import { OidcAuthResult } from '../oidc';
import { OAuthAdapter, OAuthEnvironmentHandler } from '../../lib/oauth';
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
import { AuthHandler, SignInResolver } from '../types';
import { BACKSTAGE_SESSION_EXPIRATION } from '../../lib/session';
import { InternalOAuthError } from 'passport-oauth2';
import jwtDecoder from 'jwt-decode';
type OidcImpl = {
strategy: OidcStrategy<UserinfoResponse, Client>;
@@ -72,6 +75,7 @@ export class PinnipedAuthProvider implements OAuthHandlers {
private readonly signInResolver?: SignInResolver<OidcAuthResult>;
private readonly authHandler: AuthHandler<OidcAuthResult>;
private readonly resolverContext: AuthResolverContext;
// private readonly state?;
constructor(options: PinnipedOptions) {
this.implementation = this.setupStrategy(options);
@@ -92,6 +96,7 @@ export class PinnipedAuthProvider implements OAuthHandlers {
scope: req.scope || this.scope || 'openid profile email',
state: encodeState(req.state),
};
// this.state = options.state
return new Promise((resolve, reject) => {
strategy.redirect = (url: string, status?: number) => {
resolve({ url, status: status ?? undefined });
@@ -103,6 +108,102 @@ export class PinnipedAuthProvider implements OAuthHandlers {
});
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken?: string }> {
const { strategy } = await this.implementation;
// we are passed a state inside of a session object
// const options: Record<string, string> = {
// state: encodeState(req.state),
// };
console.log(req);
// return {
// response: {
// profile: {},
// providerInfo: { accessToken: '', scope: '' },
// },
// };
// const stateParam = new URL(startResponse.url).searchParams.get('state');
// const state = Object.fromEntries(
// new URLSearchParams(Buffer.from(stateParam!, 'hex').toString('utf-8')),
// );
return new Promise((resolve, reject) => {
strategy.success = (
user: {
tokenset: {
id_token: string;
};
},
info: { refreshToken: string },
) => {
// const identity: Record<string, string> = jwtDecoder(
// user.tokenset.id_token,
// );
// const identity2 =
// console.log(identity);
resolve({
response: {
profile: {},
providerInfo: {
idToken: user.tokenset.id_token,
accessToken: '',
scope: '',
},
},
refreshToken: info.refreshToken,
});
};
strategy.fail = info => {
if (info.message) {
reject(new Error(`Authentication rejected, ${info.message ?? ''}`));
} else {
console.log('what the heckhappened');
}
};
strategy.error = (error: InternalOAuthError) => {
let message = `Authentication failed, ${error.message}`;
if (error.oauthError?.data) {
try {
const errorData = JSON.parse(error.oauthError.data);
if (errorData.message) {
message += ` - ${errorData.message}`;
}
} catch (parseError) {
message += ` - ${error.oauthError}`;
}
}
reject(new Error(message));
};
strategy.redirect = () => {
reject(new Error('Unexpected redirect'));
};
strategy.authenticate(req);
});
}
// async refresh(req: OAuthRefreshRequest) {
// const { client } = await this.implementation;
// const tokenset = await client.refresh(req.refreshToken);
// if (!tokenset.access_token) {
// throw new Error('Refresh failed');
// }
// const userinfo = client.issuer.userinfo_endpoint
// ? await client.userinfo(tokenset.access_token)
// : { sub: '' };
// return {
// response: await this.handleResult({ tokenset, userinfo }),
// refreshToken: tokenset.refresh_token,
// };
// }
private async setupStrategy(options: PinnipedOptions): Promise<OidcImpl> {
const issuer = await Issuer.discover(
`${options.federationDomain}/.well-known/openid-configuration`,
+16 -4
View File
@@ -5031,13 +5031,14 @@ __metadata:
fs-extra: 10.1.0
google-auth-library: ^8.0.0
jose: ^4.6.0
jwt-decode: ^3.1.0
jwt-decode: ^3.1.2
knex: ^2.0.0
lodash: ^4.17.21
luxon: ^3.0.0
minimatch: ^5.0.0
morgan: ^1.10.0
msw: ^1.0.0
njwt: ^2.0.0
node-cache: ^5.1.2
node-fetch: ^2.6.7
openid-client: ^5.2.1
@@ -18284,7 +18285,7 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^15.6.1":
"@types/node@npm:^15.0.1, @types/node@npm:^15.6.1":
version: 15.14.9
resolution: "@types/node@npm:15.14.9"
checksum: 49f7f0522a3af4b8389aee660e88426490cd54b86356672a1fedb49919a8797c00d090ec2dcc4a5df34edc2099d57fc2203d796c4e7fbd382f2022ccd789eee7
@@ -24426,7 +24427,7 @@ __metadata:
languageName: node
linkType: hard
"ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11":
"ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11, ecdsa-sig-formatter@npm:^1.0.5":
version: 1.0.11
resolution: "ecdsa-sig-formatter@npm:1.0.11"
dependencies:
@@ -31177,7 +31178,7 @@ __metadata:
languageName: node
linkType: hard
"jwt-decode@npm:*, jwt-decode@npm:^3.1.0":
"jwt-decode@npm:*, jwt-decode@npm:^3.1.0, jwt-decode@npm:^3.1.2":
version: 3.1.2
resolution: "jwt-decode@npm:3.1.2"
checksum: 20a4b072d44ce3479f42d0d2c8d3dabeb353081ba4982e40b83a779f2459a70be26441be6c160bfc8c3c6eadf9f6380a036fbb06ac5406b5674e35d8c4205eeb
@@ -33704,6 +33705,17 @@ __metadata:
languageName: node
linkType: hard
"njwt@npm:^2.0.0":
version: 2.0.0
resolution: "njwt@npm:2.0.0"
dependencies:
"@types/node": ^15.0.1
ecdsa-sig-formatter: ^1.0.5
uuid: ^8.3.2
checksum: 3c6c33b2fd044bca7468171f5dca064f5a4f59ce0e63b567df62c1a8d720e3c3d65921d5e99ae72eb22fde3285ef42b6009b4c4469f06e3a0e66d88a6f393373
languageName: node
linkType: hard
"no-case@npm:^3.0.4":
version: 3.0.4
resolution: "no-case@npm:3.0.4"