diff --git a/plugins/auth-backend/src/providers/pinniped/provider.test.ts b/plugins/auth-backend/src/providers/pinniped/provider.test.ts index 90fdf06239..d109ede98b 100644 --- a/plugins/auth-backend/src/providers/pinniped/provider.test.ts +++ b/plugins/auth-backend/src/providers/pinniped/provider.test.ts @@ -14,7 +14,12 @@ * limitations under the License. */ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; -import { OAuthRefreshRequest, OAuthStartRequest, encodeState, readState } from '../../lib/oauth'; +import { + OAuthRefreshRequest, + OAuthStartRequest, + encodeState, + readState, +} from '../../lib/oauth'; import { PinnipedAuthProvider, PinnipedOptions } from './provider'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; @@ -70,7 +75,7 @@ describe('PinnipedAuthProvider', () => { iat: Date.now(), aud: clientMetadata.clientId, exp: Date.now() + 10000, - } + }; const idToken = new UnsecuredJWT(testTokenMetadata) .setIssuer(testTokenMetadata.iss) @@ -102,41 +107,39 @@ describe('PinnipedAuthProvider', () => { ), ), rest.all( - 'https://federationDomain.test/.well-known/openid-configuration', (_req, res, ctx) => - res( + 'https://federationDomain.test/.well-known/openid-configuration', + (_req, res, ctx) => + res( ctx.status(200), ctx.set('Content-Type', 'application/json'), ctx.json(issuerMetadata), - ) - + ), ), 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), - ), + 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), ), - 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), - ), ), ); @@ -147,7 +150,7 @@ describe('PinnipedAuthProvider', () => { url: 'test', state: oauthState, } as unknown as OAuthStartRequest; - + provider = new PinnipedAuthProvider(clientMetadata); }); @@ -169,15 +172,18 @@ describe('PinnipedAuthProvider', () => { }); 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) + 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(decodedState).toMatchObject({nonce: 'nonce', - env: 'env', audience:'test-cluster' }) - }) + expect(decodedState).toMatchObject({ + nonce: 'nonce', + env: 'env', + audience: 'test-cluster', + }); + }); it('passes client ID from config', async () => { const startResponse = await provider.start(startRequest); @@ -214,7 +220,12 @@ describe('PinnipedAuthProvider', () => { const scopes = searchParams.get('scope')?.split(' ') ?? []; expect(scopes).toEqual( - expect.arrayContaining(['openid', 'pinniped:request-audience', 'username']), + expect.arrayContaining([ + 'openid', + 'pinniped:request-audience', + 'username', + 'offline_access', + ]), ); }); @@ -241,10 +252,12 @@ describe('PinnipedAuthProvider', () => { let handlerRequest: express.Request; beforeEach(() => { - //we want to somehow pass an authentication header in this request for testing purposes + // we want to somehow pass an authentication header in this request for testing purposes handlerRequest = { method: 'GET', - url: `https://test?code=authorization_code&state=${encodeState(oauthState)}`, + url: `https://test?code=authorization_code&state=${encodeState( + oauthState, + )}`, session: { 'oidc:pinniped.test': { state: encodeState(oauthState), @@ -252,18 +265,27 @@ describe('PinnipedAuthProvider', () => { }, } as unknown as express.Request; }); - - it('exchanges authorization code for a valid access_token', async() => { - const handlerResponse = await provider.handler(handlerRequest); - const accessToken = handlerResponse.response.providerInfo.accessToken - - expect(accessToken).toEqual('accessToken') - }) - it('request errors out with missing authorization_code parameter in the request_url', async() => { - handlerRequest.url = "https://test.com" - return expect(provider.handler(handlerRequest)).rejects.toThrow('Unexpected redirect') - }) + it('exchanges authorization code for a access_token', async () => { + const handlerResponse = await provider.handler(handlerRequest); + const accessToken = handlerResponse.response.providerInfo.accessToken; + + expect(accessToken).toEqual('accessToken'); + }); + + it('exchanges authorization code for a refresh_token', async () => { + const handlerResponse = await provider.handler(handlerRequest); + const refreshToken = handlerResponse.refreshToken; + + expect(refreshToken).toEqual('refreshToken'); + }); + + it('request errors out with missing authorization_code parameter in the request_url', async () => { + handlerRequest.url = 'https://test.com'; + return expect(provider.handler(handlerRequest)).rejects.toThrow( + 'Unexpected redirect', + ); + }); it('fails when request has no state in req_url', async () => { return expect( @@ -290,8 +312,8 @@ describe('PinnipedAuthProvider', () => { ).rejects.toThrow('authentication requires session support'); }); - //if no valid key is in the jwks array or even an unsigned jwt - //have pinniped reject your clientid and secret possibly as a unit test + // if no valid key is in the jwks array or even an unsigned jwt + // have pinniped reject your clientid and secret possibly as a unit test }); describe('#refresh', () => { @@ -299,24 +321,20 @@ describe('PinnipedAuthProvider', () => { beforeEach(() => { refreshRequest = { - refreshToken: 'otherRefreshToken' + refreshToken: 'otherRefreshToken', } as unknown as OAuthRefreshRequest; }); - it('gets new refresh token', async() => { + it('gets new refresh token', async () => { const { refreshToken } = await provider.refresh(refreshRequest); - - expect(refreshToken).toBe('refreshToken') - }) - it('gets an access_token', async() => { - const { response } = await provider.refresh(refreshRequest) + expect(refreshToken).toBe('refreshToken'); + }); - expect(response.providerInfo.accessToken).toBe("accessToken") - }) - //find out when refresh requests are even made? - //so far looks like the response should be exactly like the one returned by the handler - //what are we exchanging exactly to get this refresh token?? + it('gets an access_token', async () => { + const { response } = await provider.refresh(refreshRequest); - }) + expect(response.providerInfo.accessToken).toBe('accessToken'); + }); + }); }); diff --git a/plugins/auth-backend/src/providers/pinniped/provider.ts b/plugins/auth-backend/src/providers/pinniped/provider.ts index ebc94da58f..c4ea1e40e3 100644 --- a/plugins/auth-backend/src/providers/pinniped/provider.ts +++ b/plugins/auth-backend/src/providers/pinniped/provider.ts @@ -62,11 +62,12 @@ export class PinnipedAuthProvider implements OAuthHandlers { async start(req: OAuthStartRequest): Promise { const { strategy } = await this.implementation; - const stringifiedAudience = req.query?.audience as string - const state = {...req.state, audience: stringifiedAudience } + const stringifiedAudience = req.query?.audience as string; + const state = { ...req.state, audience: stringifiedAudience }; const options: Record = { - scope: req.scope || 'openid pinniped:request-audience username', + scope: + req.scope || 'openid pinniped:request-audience username offline_access', state: encodeState(state), }; return new Promise((resolve, reject) => { @@ -87,10 +88,10 @@ export class PinnipedAuthProvider implements OAuthHandlers { // the query string inside the req should contain a code and a state, we can change the stub to reject any auth code, - //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" + // 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 => { @@ -102,6 +103,7 @@ export class PinnipedAuthProvider implements OAuthHandlers { }, profile: {}, }, + refreshToken: user.tokenset.refresh_token, }); }; strategy.fail = info => { @@ -120,13 +122,15 @@ export class PinnipedAuthProvider implements OAuthHandlers { }); } - async refresh(req: OAuthRefreshRequest): Promise<{ response: OAuthResponse; refreshToken?: string }> { + async refresh( + req: OAuthRefreshRequest, + ): Promise<{ response: OAuthResponse; refreshToken?: string }> { const { client } = await this.implementation; const tokenset = await client.refresh(req.refreshToken); return new Promise((resolve, reject) => { - if(!tokenset.access_token){ - reject(new Error('Refresh Failed')) + if (!tokenset.access_token) { + reject(new Error('Refresh Failed')); } resolve({ @@ -139,7 +143,7 @@ export class PinnipedAuthProvider implements OAuthHandlers { }, refreshToken: tokenset.refresh_token, }); - }) + }); } private async setupStrategy(options: PinnipedOptions): Promise {