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:
@@ -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: {},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user