passing pinniped authprovider #handler responds with Id token test
Signed-off-by: Ruben Vallejo <rvallejo@vmware.com>
This commit is contained in:
@@ -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`,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user