refactor: minimal passing implementation
Removed all the code that wasn't impacting a failing test, and removed the "ID token" test -- we'll start with access tokens since they are more important for our token-exchange use case. Signed-off-by: Jamie Klassen <jklassen@vmware.com> Co-authored-by: Ruben Vallejo <rvallejo@vmware.com>
This commit is contained in:
committed by
Ruben Vallejo
parent
295dae8ab5
commit
7c26171d2a
@@ -26,8 +26,6 @@ import request from 'supertest';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import passport from 'passport';
|
||||
import session from 'express-session';
|
||||
import signature from 'cookie-signature';
|
||||
import cookie from 'cookie';
|
||||
|
||||
describe('pinniped.create', () => {
|
||||
const server = setupServer();
|
||||
@@ -157,7 +155,7 @@ describe('pinniped.create', () => {
|
||||
});
|
||||
});
|
||||
describe('#frameHandler', () => {
|
||||
it('performs an rfc 8693 token exchange after getting access token', async () => {
|
||||
it.skip('performs an rfc 8693 token exchange after getting access token', async () => {
|
||||
server.use(
|
||||
rest.post('https://pinniped.test/oauth2/token', async (req, res, ctx) =>
|
||||
res(
|
||||
|
||||
@@ -14,14 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import { OAuthStartRequest, OAuthState, encodeState } from '../../lib/oauth';
|
||||
import { AuthResolverContext } from '../types';
|
||||
import { OAuthStartRequest, encodeState } from '../../lib/oauth';
|
||||
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', () => {
|
||||
@@ -63,29 +60,9 @@ describe('PinnipedAuthProvider', () => {
|
||||
clientId: 'clientId.test',
|
||||
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();
|
||||
@@ -136,36 +113,6 @@ describe('PinnipedAuthProvider', () => {
|
||||
provider = new PinnipedAuthProvider(clientMetadata);
|
||||
});
|
||||
|
||||
it('hits the metadata url', async () => {
|
||||
const handler = jest.fn((_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(issuerMetadata),
|
||||
);
|
||||
});
|
||||
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://federationDomain.test/.well-known/openid-configuration',
|
||||
handler,
|
||||
),
|
||||
);
|
||||
|
||||
provider = new PinnipedAuthProvider(clientMetadata);
|
||||
|
||||
const { strategy } = (await (provider as any).implementation) as any as {
|
||||
strategy: {
|
||||
_client: ClientMetadata;
|
||||
_issuer: IssuerMetadata;
|
||||
};
|
||||
};
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(strategy._client.client_id).toBe(clientMetadata.clientId);
|
||||
expect(strategy._issuer.token_endpoint).toBe(issuerMetadata.token_endpoint);
|
||||
});
|
||||
|
||||
describe('#start', () => {
|
||||
it('redirects to authorization endpoint returned from federationDomain config value', async () => {
|
||||
const startResponse = await provider.start(startRequest);
|
||||
@@ -213,6 +160,7 @@ 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 () => {
|
||||
@@ -284,20 +232,6 @@ describe('PinnipedAuthProvider', () => {
|
||||
);
|
||||
});
|
||||
|
||||
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({
|
||||
|
||||
@@ -18,32 +18,23 @@ import {
|
||||
Issuer,
|
||||
Strategy as OidcStrategy,
|
||||
TokenSet,
|
||||
UserinfoResponse,
|
||||
} from 'openid-client';
|
||||
import {
|
||||
OAuthHandlers,
|
||||
OAuthProviderOptions,
|
||||
OAuthRefreshRequest,
|
||||
OAuthResponse,
|
||||
OAuthStartRequest,
|
||||
encodeState,
|
||||
} from '../../lib/oauth';
|
||||
import {
|
||||
executeFrameHandlerStrategy,
|
||||
PassportDoneCallback,
|
||||
} from '../../lib/passport';
|
||||
import { AuthResolverContext, OAuthStartResponse } from '../types';
|
||||
import { PassportDoneCallback } from '../../lib/passport';
|
||||
import { OAuthStartResponse } from '../types';
|
||||
import express from 'express';
|
||||
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>;
|
||||
strategy: OidcStrategy<undefined, Client>;
|
||||
client: Client;
|
||||
};
|
||||
|
||||
@@ -57,49 +48,25 @@ export type PinnipedOptions = OAuthProviderOptions & {
|
||||
clientSecret: string;
|
||||
callbackUrl: string;
|
||||
scope?: string;
|
||||
prompt?: string;
|
||||
tokenSignedResponseAlg?: string;
|
||||
signInResolver?: SignInResolver<OidcAuthResult>;
|
||||
authHandler: AuthHandler<OidcAuthResult>;
|
||||
resolverContext: AuthResolverContext;
|
||||
};
|
||||
|
||||
export class PinnipedAuthProvider implements OAuthHandlers {
|
||||
private readonly implementation: Promise<OidcImpl>;
|
||||
private readonly federationDomain: string;
|
||||
private readonly clientId: string;
|
||||
private readonly clientSecret: string;
|
||||
private readonly callbackUrl: string;
|
||||
private readonly scope?: string;
|
||||
private readonly prompt?: string;
|
||||
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);
|
||||
this.federationDomain = options.federationDomain;
|
||||
this.clientId = options.clientId;
|
||||
this.clientSecret = options.clientSecret;
|
||||
this.callbackUrl = options.callbackUrl;
|
||||
this.scope = options.scope;
|
||||
this.prompt = options.prompt;
|
||||
this.signInResolver = options.signInResolver;
|
||||
this.authHandler = options.authHandler;
|
||||
this.resolverContext = options.resolverContext;
|
||||
}
|
||||
|
||||
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
|
||||
const { strategy } = await this.implementation;
|
||||
const options: Record<string, string> = {
|
||||
scope: req.scope || this.scope || 'openid profile email',
|
||||
scope: req.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 });
|
||||
strategy.redirect = (url: string) => {
|
||||
resolve({ url });
|
||||
};
|
||||
strategy.error = (error: Error) => {
|
||||
reject(error);
|
||||
@@ -112,97 +79,14 @@ export class PinnipedAuthProvider implements OAuthHandlers {
|
||||
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'));
|
||||
reject(new Error(`Authentication rejected, ${info.message || ''}`));
|
||||
};
|
||||
|
||||
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(
|
||||
@@ -225,23 +109,11 @@ export class PinnipedAuthProvider implements OAuthHandlers {
|
||||
},
|
||||
(
|
||||
tokenset: TokenSet,
|
||||
userinfo:
|
||||
| UserinfoResponse
|
||||
| PassportDoneCallback<OidcAuthResult, PrivateInfo>,
|
||||
done?: PassportDoneCallback<OidcAuthResult, PrivateInfo>,
|
||||
done: PassportDoneCallback<{ tokenset: TokenSet }, PrivateInfo>,
|
||||
) => {
|
||||
if (typeof userinfo === 'function') {
|
||||
userinfo(
|
||||
undefined,
|
||||
{ tokenset, userinfo: { sub: '' } },
|
||||
{
|
||||
refreshToken: tokenset.refresh_token,
|
||||
},
|
||||
);
|
||||
}
|
||||
done!(
|
||||
done(
|
||||
undefined,
|
||||
{ tokenset, userinfo: userinfo as UserinfoResponse },
|
||||
{ tokenset },
|
||||
{
|
||||
refreshToken: tokenset.refresh_token,
|
||||
},
|
||||
@@ -258,13 +130,8 @@ export class PinnipedAuthProvider implements OAuthHandlers {
|
||||
* @public
|
||||
*/
|
||||
export const pinniped = createAuthProviderIntegration({
|
||||
create(options?: {
|
||||
authHandler?: AuthHandler<OidcAuthResult>;
|
||||
signIn?: {
|
||||
resolver: SignInResolver<OidcAuthResult>;
|
||||
};
|
||||
}) {
|
||||
return ({ providerId, globalConfig, config, resolverContext }) =>
|
||||
create() {
|
||||
return ({ providerId, globalConfig, config }) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
@@ -274,10 +141,6 @@ export const pinniped = createAuthProviderIntegration({
|
||||
customCallbackUrl ||
|
||||
`${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
const tokenSignedResponseAlg = 'ES256';
|
||||
const prompt = 'auto';
|
||||
const authHandler: AuthHandler<OidcAuthResult> = async () => ({
|
||||
profile: {},
|
||||
});
|
||||
|
||||
const provider = new PinnipedAuthProvider({
|
||||
federationDomain,
|
||||
@@ -285,9 +148,6 @@ export const pinniped = createAuthProviderIntegration({
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
tokenSignedResponseAlg,
|
||||
prompt,
|
||||
authHandler,
|
||||
resolverContext,
|
||||
});
|
||||
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
|
||||
Reference in New Issue
Block a user