introduce audience parameter into start method and pass it into oauth state, fix integration tests

Signed-off-by: Ruben Vallejo <rvallejo@vmware.com>
This commit is contained in:
Ruben Vallejo
2023-08-22 13:06:05 -04:00
parent 07dcd84379
commit 9fedd45be6
3 changed files with 43 additions and 35 deletions
+12 -5
View File
@@ -92,11 +92,18 @@ export type OAuthProviderInfo = {
scope: string;
};
/**
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type OAuthState = _OAuthState;
/** @public */
export type OAuthState = {
/* A type for the serialized value in the `state` parameter of the OAuth authorization flow
*/
nonce: string;
env: string;
origin?: string;
scope?: string;
redirectUrl?: string;
flow?: string;
audience? : string;
};
/**
* @public
@@ -21,7 +21,6 @@ import { rest } from 'msw';
import express from 'express';
import { UnsecuredJWT } from 'jose';
import { OAuthState } from '../../lib/oauth';
import { EntityAzurePipelinesContent } from '@backstage/plugin-azure-devops';
describe('PinnipedAuthProvider', () => {
let provider: PinnipedAuthProvider;
@@ -137,19 +136,15 @@ describe('PinnipedAuthProvider', () => {
expect(searchParams.get('response_type')).toBe('code');
});
it('passes default audience as a scope parameter in the redirect url if not defined in the request', async () => {
it('passes audience query parameter into OAuthState in the redirect url when defined in the request', async () => {
startRequest.query = { audience: 'test-cluster'}
const startResponse = await provider.start(startRequest)
const { searchParams } = new URL(startResponse.url)
const stateParam = searchParams.get('state');
const decodedState = readState(stateParam!);
expect(searchParams.get('scope')).toBe('pinniped:request-audience username')
})
it('passes audience as a scope parameter in the redirect url when defined in the request', async () => {
startRequest.scope = 'pinniped:request-audience testusername'
const startResponse = await provider.start(startRequest)
const { searchParams } = new URL(startResponse.url)
expect(searchParams.get('scope')).toBe('pinniped:request-audience testusername')
expect(decodedState).toMatchObject({nonce: 'nonce',
env: 'env', audience:'test-cluster' })
})
it('passes client ID from config', async () => {
@@ -187,7 +182,7 @@ describe('PinnipedAuthProvider', () => {
const scopes = searchParams.get('scope')?.split(' ') ?? [];
expect(scopes).toEqual(
expect.arrayContaining(['pinniped:request-audience', 'username']),
expect.arrayContaining(['openid', 'pinniped:request-audience', 'username']),
);
});
@@ -213,22 +208,23 @@ describe('PinnipedAuthProvider', () => {
describe('#handler', () => {
let handlerRequest: express.Request;
const testState = {
nonce: 'nonce',
env: 'development',
origin: 'undefined',
audience: 'test-cluster'
};
beforeEach(() => {
provider = new PinnipedAuthProvider(clientMetadata);
const testState = encodeState({
nonce: 'nonce',
env: 'development',
origin: 'undefined',
});
//we want to somehow pass an authentication header in this request for testing purposes
handlerRequest = {
method: 'GET',
url: `https://test?code=authorization_code&state=${testState}&scope=pinniped:request-audience username`,
url: `https://test?code=authorization_code&state=${encodeState(testState)}`,
session: {
'oidc:pinniped.test': {
state: testState,
state: encodeState(testState),
},
},
} as unknown as express.Request;
@@ -288,7 +284,8 @@ describe('PinnipedAuthProvider', () => {
expect(accessToken).toEqual('accessToken')
})
it('responds with the correct audience as scope', async() => {
//scope cannot be passed along in the redirect url and is different from audience
it.skip('responds with the correct audience as scope', async() => {
const handlerResponse = await provider.handler(handlerRequest);
const audience = handlerResponse.response.providerInfo.scope
@@ -296,7 +293,7 @@ describe('PinnipedAuthProvider', () => {
})
it('request errors out with missing authorization_code parameter in the request_url', async() => {
handlerRequest.url = "test"
handlerRequest.url = "https://test.com"
return expect(provider.handler(handlerRequest)).rejects.toThrow('Unexpected redirect')
})
@@ -304,7 +301,7 @@ describe('PinnipedAuthProvider', () => {
return expect(
provider.handler({
method: 'GET',
url: 'test',
url: 'https://test.com',
} as unknown as OAuthStartRequest),
).rejects.toThrow('authentication requires session support');
});
@@ -25,6 +25,7 @@ import {
OAuthResponse,
OAuthStartRequest,
encodeState,
readState,
} from '../../lib/oauth';
import { PassportDoneCallback } from '../../lib/passport';
import { OAuthStartResponse } from '../types';
@@ -68,9 +69,12 @@ export class PinnipedAuthProvider implements OAuthHandlers {
// `{"type":"authorization_response","error":{"name":"OPError","message":"invalid_scope (The requested scope is invalid, unknown, or malformed. The OAuth 2.0 Client is not allowed to request scope 'openid+pinniped:request-audience+username'.)"}}`
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',
state: encodeState(req.state),
scope: req.scope || 'openid pinniped:request-audience username',
state: encodeState(state),
};
return new Promise((resolve, reject) => {
strategy.redirect = (url: string) => {
@@ -89,11 +93,11 @@ export class PinnipedAuthProvider implements OAuthHandlers {
const { strategy } = await this.implementation;
// the query string inside the req should contain a code and a state, we can change the stub to reject any auth code,
// can this query string also include scope? It already does get a scope in its redirect url when start is called by default.
// but when the fake supervisor hits the handler a scope must be returned by the oauth2/authorize endpoint for it to be present in the req
const { searchParams } = new URL(req.url, 'https://pinniped.com');
const audience = searchParams.get('scope') ?? 'none';
//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 : "none"
return new Promise((resolve, reject) => {
strategy.success = user => {
@@ -101,7 +105,7 @@ export class PinnipedAuthProvider implements OAuthHandlers {
response: {
providerInfo: {
accessToken: user.tokenset.access_token,
scope: audience,
scope: 'none',
},
profile: {},
},