diff --git a/plugins/auth-backend/src/providers/pinniped/index.test.ts b/plugins/auth-backend/src/providers/pinniped/index.test.ts deleted file mode 100644 index 19ba3c1b10..0000000000 --- a/plugins/auth-backend/src/providers/pinniped/index.test.ts +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { pinniped } from '.'; -import { AuthProviderRouteHandlers } from '../types'; -import { getVoidLogger } from '@backstage/backend-common'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; -import { ConfigReader } from '@backstage/config'; -import { setupServer } from 'msw/node'; -import { rest } from 'msw'; -import { Server } from 'http'; -import { AddressInfo } from 'net'; -import express from 'express'; -import request from 'supertest'; -import cookieParser from 'cookie-parser'; -import passport from 'passport'; -import session from 'express-session'; -import Router from 'express-promise-router'; -// import fetch from 'node-fetch'; -import { SignJWT, exportJWK, generateKeyPair, importJWK } from 'jose'; -import { v4 as uuid } from 'uuid'; - -describe('pinniped.create', () => { - const fakePinnipedSupervisor = setupServer(); - setupRequestMockHandlers(fakePinnipedSupervisor); - const nonce = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 16 bytes of zeros in base64 - const state = Buffer.from( - `nonce=${encodeURIComponent(nonce)}&env=development`, - ).toString('hex'); - - let app: express.Express; - let provider: AuthProviderRouteHandlers; - let backstageServer: Server; - let appUrl: string; - - beforeEach(async () => { - const secret = 'secret'; - app = express() - .use(cookieParser(secret)) - .use( - session({ - secret, - saveUninitialized: false, - resave: false, - cookie: { secure: false }, - }), - ) - .use(passport.initialize()) - .use(passport.session()); - await new Promise(resolve => { - backstageServer = app.listen(0, '0.0.0.0', () => { - appUrl = `http://127.0.0.1:${ - (backstageServer.address() as AddressInfo).port - }`; - resolve(null); - }); - }); - fakePinnipedSupervisor.use( - rest.all(`${appUrl}/*`, req => req.passthrough()), - rest.all( - 'https://pinniped.test/.well-known/openid-configuration', - (_, res, ctx) => - res( - ctx.json({ - issuer: 'https://pinniped.test', - authorization_endpoint: 'https://pinniped.test/oauth2/authorize', - token_endpoint: 'https://pinniped.test/oauth2/token', - jwks_uri: 'https://pinniped.test/jwks.json', - response_types_supported: ['code', 'access_token'], - response_modes_supported: ['query', 'form_post'], - subject_types_supported: ['public'], - token_endpoint_auth_methods_supported: ['client_secret_basic'], - id_token_signing_alg_values_supported: [ - 'RS256', - 'RS512', - 'HS256', - 'ES256', - ], - token_endpoint_auth_signing_alg_values_supported: [ - 'RS256', - 'RS512', - 'HS256', - ], - request_object_signing_alg_values_supported: [ - 'RS256', - 'RS512', - 'HS256', - ], - scopes_supported: [ - 'openid', - 'offline_access', - 'pinniped:request-audience', - 'username', - 'groups', - ], - claims_supported: [ - 'username', - 'groups', - 'additionalClaims', - 'sub', - ], - code_challenge_methods_supported: ['S256'], - 'discovery.supervisor.pinniped.dev/v1alpha1': { - pinniped_identity_providers_endpoint: - 'https://pinniped.test/v1alpha1/pinniped_identity_providers', - }, - }), - ), - ), - ); - - provider = pinniped.create()({ - providerId: 'pinniped', - globalConfig: { - baseUrl: `${appUrl}/api/auth`, - appUrl, - isOriginAllowed: _ => true, - }, - config: new ConfigReader({ - development: { - federationDomain: 'https://pinniped.test', - clientId: 'clientId', - clientSecret: 'clientSecret', - }, - }), - logger: getVoidLogger(), - resolverContext: { - issueToken: async _ => ({ token: '' }), - findCatalogUser: async _ => ({ - entity: { - apiVersion: '', - kind: '', - metadata: { name: '' }, - }, - }), - signInWithCatalogUser: async _ => ({ token: '' }), - }, - }); - const router = Router(); - router - .use('/api/auth/pinniped/start', provider.start.bind(provider)) - .use( - '/api/auth/pinniped/handler/frame', - provider.frameHandler.bind(provider), - ); - app.use(router); - }); - - afterEach(() => { - backstageServer.close(); - }); - - // also include an audience parameter and only assert on the id_token - - // repurpose this test - it('/handler/frame exchanges authorization codes from /start for access tokens', async () => { - const agent = request.agent(''); - // make a /start request - // add query parameter for the audience - const startResponse = await agent.get( - `${appUrl}/api/auth/pinniped/start?env=development`, - ); - // follow the redirect to pinniped authorization endpoint - fakePinnipedSupervisor.use( - rest.get( - 'https://pinniped.test/oauth2/authorize', - async (req, res, ctx) => { - const callbackUrl = new URL( - req.url.searchParams.get('redirect_uri')!, - ); - callbackUrl.searchParams.set('code', 'authorization_code'); - callbackUrl.searchParams.set( - 'state', - req.url.searchParams.get('state')!, - ); - callbackUrl.searchParams.set('scope', 'test-scope'); - return res( - ctx.status(302), - ctx.set('Location', callbackUrl.toString()), - ); - }, - ), - ); - const authorizationResponse = await agent.get( - startResponse.header.location, - ); - - // follow the redirect back to /handler/frame - const testTokenMetadata = { - sub: 'test', - iss: 'https://pinniped.test', - iat: Date.now(), - aud: 'clientId', - exp: Date.now() + 10000, - }; - - const key = await generateKeyPair('ES256'); - const publicKey = await exportJWK(key.publicKey); - const privateKey = await exportJWK(key.privateKey); - publicKey.kid = privateKey.kid = uuid(); - publicKey.alg = privateKey.alg = 'ES256'; - - const jwt = await new SignJWT(testTokenMetadata) - .setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid }) - .setIssuer(testTokenMetadata.iss) - .setAudience(testTokenMetadata.aud) - .setSubject(testTokenMetadata.sub) - .setIssuedAt(testTokenMetadata.iat) - .setExpirationTime(testTokenMetadata.exp) - .sign(await importJWK(privateKey)); - - fakePinnipedSupervisor.use( - rest.post('https://pinniped.test/oauth2/token', async (req, res, ctx) => - res( - // TODO verify client ID + secret, etc -- real token endpoint - new URLSearchParams(await req.text()).get('code') === - 'authorization_code' - ? ctx.json({ - access_token: 'accessToken', - id_token: jwt, - scope: 'testScope', - }) - : ctx.status(401), - ), - ), - rest.get('https://pinniped.test/jwks.json', async (_req, res, ctx) => - res(ctx.status(200), ctx.json({ keys: [{ ...publicKey }] })), - ), - ); - - const handlerResponse = await agent.get( - authorizationResponse.header.location, - ); - // our assertion doesnt include a scope but the return type does...might need to change our assertion? - expect(handlerResponse.text).toContain( - encodeURIComponent( - JSON.stringify({ - type: 'authorization_response', - response: { - providerInfo: { - idToken: 'accessToken', - scope: 'testScope', - }, - profile: {}, - }, - }), - ), - ); - }, 70000); - - describe('#frameHandler', () => { - it.skip('performs an rfc 8693 token exchange after getting access token', async () => { - const agent = request.agent(''); - - // make a start request with an audience query - const startResponse = await agent.get( - `${appUrl}/api/auth/pinniped/start?env=development&aud=testCluster`, - ); - - const testTokenMetadata = { - sub: 'test', - iss: 'https://pinniped.test', - iat: Date.now(), - aud: 'clientId', - exp: Date.now() + 10000, - }; - - const key = await generateKeyPair('ES256'); - const publicKey = await exportJWK(key.publicKey); - const privateKey = await exportJWK(key.privateKey); - publicKey.kid = privateKey.kid = uuid(); - publicKey.alg = privateKey.alg = 'ES256'; - - const jwt = await new SignJWT(testTokenMetadata) - .setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid }) - .setIssuer(testTokenMetadata.iss) - .setAudience(testTokenMetadata.aud) - .setSubject(testTokenMetadata.sub) - .setIssuedAt(testTokenMetadata.iat) - .setExpirationTime(testTokenMetadata.exp) - .sign(await importJWK(privateKey)); - - // follow the redirect to pinniped authorization endpoint - fakePinnipedSupervisor.use( - rest.get( - 'https://pinniped.test/oauth2/authorize', - async (req, res, ctx) => { - const callbackUrl = new URL( - req.url.searchParams.get('redirect_uri')!, - ); - callbackUrl.searchParams.set('code', 'authorization_code'); - callbackUrl.searchParams.set( - 'state', - req.url.searchParams.get('state')!, - ); - callbackUrl.searchParams.set('scope', 'test-scope'); - return res( - ctx.status(302), - ctx.set('Location', callbackUrl.toString()), - ); - }, - ), - rest.post('https://pinniped.test/oauth2/token', async (req, res, ctx) => - res( - ctx.json( - new URLSearchParams(await req.text()).get('grant_type') === - 'urn:ietf:params:oauth:grant-type:token-exchange' - ? { access_token: 'accessToken', scope: 'test-scope' } - : { - accessToken: 'accessToken', - scope: 'test-scope', - idToken: jwt, - }, - ), - ), - ), - rest.get('https://pinniped.test/jwks.json', async (_req, res, ctx) => - res(ctx.status(200), ctx.json({ keys: [{ ...publicKey }] })), - ), - ); - - // fakePinnipedSupervisor.use( - // rest.post('https://pinniped.test/oauth2/token', async (req, res, ctx) => - // res( - // ctx.json( - // new URLSearchParams(await req.text()).get('grant_type') === - // 'urn:ietf:params:oauth:grant-type:token-exchange' - // ? { access_token: 'accessToken' } - // : { id_token: 'clusterToken' }, - // ), - // ), - // ), - // ); - - const authorizationResponse = await agent.get( - startResponse.header.location, - ); - - const handlerResponse = await agent.get( - authorizationResponse.header.location, - ); - - // const responsePromise = request(app) - // .get( - // '/api/auth/pinniped/handler/frame?' + - // 'code=pin_ac_xU69qZGejOCu8Loz5iOD6Bm25SgQewmT0VVE1hOAQzA.WzxrI9bCder5UJHtCOX_yEnsM2OVh8pVSFI7NPs5yUM&' + - // 'scope=openid+pinniped%3Arequest-audience+username&' + - // `state=${state}`, - // ) - // .set( - // 'Cookie', - // `pinniped-nonce=${nonce}; ` + - // 'connect.sid=s:p3_hKHiFr_i58jyTPIZxtWN9pejiOujD.SN2irLt6oIL18v0GzGCPO1sibEmzybiVlT9ca3ZjT68', - // ); - - // const reqUrl = new URL(responsePromise.url); - // reqUrl.search = ''; - - // fakePinnipedSupervisor.use( - // rest.all(reqUrl.toString(), req => req.passthrough()), - // ); - - expect(handlerResponse.text).toContain( - encodeURIComponent( - JSON.stringify({ - type: 'authorization_response', - response: { - providerInfo: { - accessToken: 'accessToken', - scope: 'test-scope', - idToken: jwt, - }, - profile: {}, - }, - }), - ), - ); - }); - }); -}); diff --git a/plugins/auth-backend/src/providers/pinniped/index.ts b/plugins/auth-backend/src/providers/pinniped/index.ts index 943b0549e8..a45064ad4d 100644 --- a/plugins/auth-backend/src/providers/pinniped/index.ts +++ b/plugins/auth-backend/src/providers/pinniped/index.ts @@ -13,69 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// import { OidcAuthResult } from '../oidc'; -// import { OidcAuthProvider } from '../oidc/provider'; -// import { OAuthAdapter, OAuthEnvironmentHandler } from '../../lib/oauth'; -// import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -// import { AuthHandler, SignInResolver } from '../types'; -// import { PinnipedAuthProvider } from './provider'; - -/** - * Auth provider integration for Pinniped - * - * @public - */ -// export const pinniped = createAuthProviderIntegration({ -// create(options?: { -// authHandler?: AuthHandler; -// signIn?: { -// resolver: SignInResolver; -// }; -// }) { -// return ({ providerId, globalConfig, config, resolverContext }) => -// OAuthEnvironmentHandler.mapConfig(config, envConfig => { -// const clientId = envConfig.getString('clientId'); -// const clientSecret = envConfig.getString('clientSecret'); -// const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); -// const callbackUrl = -// customCallbackUrl || -// `${globalConfig.baseUrl}/${providerId}/handler/frame`; -// const metadataUrl = `${envConfig.getString( -// 'federationDomain', -// )}/.well-known/openid-configuration`; -// const federationDomain = envConfig.getString('federationDomain'); -// const tokenSignedResponseAlg = 'ES256'; -// const prompt = 'auto'; -// const authHandler: AuthHandler = async ({ -// userinfo, -// }) => ({ -// profile: {}, -// }); - -// // const provider = new OidcAuthProvider({ -// // clientId, -// // clientSecret, -// // callbackUrl, -// // tokenSignedResponseAlg, -// // metadataUrl, -// // prompt, -// // signInResolver: options?.signIn?.resolver, -// // authHandler, -// // resolverContext, -// // }); - -// const provider = new PinnipedAuthProvider({ -// federationDomain, -// clientId, -// clientSecret, -// }); - -// return OAuthAdapter.fromConfig(globalConfig, provider, { -// providerId, -// callbackUrl, -// }); -// }); -// }, -// }); export { pinniped } from './provider'; diff --git a/plugins/auth-backend/src/providers/pinniped/provider.test.ts b/plugins/auth-backend/src/providers/pinniped/provider.test.ts index 9fbe0ff3e7..82312266ff 100644 --- a/plugins/auth-backend/src/providers/pinniped/provider.test.ts +++ b/plugins/auth-backend/src/providers/pinniped/provider.test.ts @@ -26,14 +26,27 @@ import { rest } from 'msw'; import express from 'express'; import { UnsecuredJWT } from 'jose'; import { OAuthState } from '../../lib/oauth'; +import { Server } from 'http'; +import cookieParser from 'cookie-parser'; +import session from 'express-session'; +import passport from 'passport'; +import { ConfigReader } from '@backstage/config'; +import Router from 'express-promise-router'; +import { pinniped } from '.'; +import { AuthProviderRouteHandlers } from '../types'; +import { getVoidLogger } from '@backstage/backend-common'; +import { AddressInfo } from 'net'; +import request from 'supertest'; +import { SignJWT, exportJWK, generateKeyPair, importJWK } from 'jose'; +import { v4 as uuid } from 'uuid'; describe('PinnipedAuthProvider', () => { let provider: PinnipedAuthProvider; let startRequest: OAuthStartRequest; let fakeSession: Record; - const worker = setupServer(); - setupRequestMockHandlers(worker); + const fakePinnipedSupervisor = setupServer(); + setupRequestMockHandlers(fakePinnipedSupervisor); const issuerMetadata = { issuer: 'https://pinniped.test', @@ -63,8 +76,8 @@ describe('PinnipedAuthProvider', () => { const clientMetadata: PinnipedOptions = { federationDomain: 'https://federationDomain.test', - clientId: 'clientId.test', - clientSecret: 'secret.test', + clientId: 'clientId', + clientSecret: 'secret', callbackUrl: 'https://federationDomain.test/callback', tokenSignedResponseAlg: 'none', }; @@ -96,7 +109,7 @@ describe('PinnipedAuthProvider', () => { beforeEach(() => { jest.clearAllMocks(); - worker.use( + fakePinnipedSupervisor.use( rest.all( 'https://federationDomain.test/.well-known/openid-configuration', (_req, res, ctx) => @@ -148,6 +161,24 @@ describe('PinnipedAuthProvider', () => { ctx.status(200), ), ), + rest.get( + 'https://pinniped.test/oauth2/authorize', + async (req, res, ctx) => { + const callbackUrl = new URL( + req.url.searchParams.get('redirect_uri')!, + ); + callbackUrl.searchParams.set('code', 'authorization_code'); + callbackUrl.searchParams.set( + 'state', + req.url.searchParams.get('state')!, + ); + callbackUrl.searchParams.set('scope', 'test-scope'); + return res( + ctx.status(302), + ctx.set('Location', callbackUrl.toString()), + ); + }, + ), ); fakeSession = {}; @@ -196,7 +227,7 @@ describe('PinnipedAuthProvider', () => { const startResponse = await provider.start(startRequest); const { searchParams } = new URL(startResponse.url); - expect(searchParams.get('client_id')).toBe('clientId.test'); + expect(searchParams.get('client_id')).toBe('clientId'); }); it('passes callback URL', async () => { @@ -259,7 +290,6 @@ describe('PinnipedAuthProvider', () => { let handlerRequest: express.Request; beforeEach(() => { - // 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( @@ -345,9 +375,6 @@ describe('PinnipedAuthProvider', () => { } as unknown as OAuthStartRequest), ).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 }); describe('#refresh', () => { @@ -370,5 +397,170 @@ describe('PinnipedAuthProvider', () => { expect(response.providerInfo.accessToken).toBe('accessToken'); }); + + it('gets an id_token', async () => { + const { response } = await provider.refresh(refreshRequest); + + expect(response.providerInfo.idToken).toBe(idToken); + }); + }); + + describe('pinniped.create', () => { + let app: express.Express; + let providerRouteHandler: AuthProviderRouteHandlers; + let backstageServer: Server; + let appUrl: string; + + beforeEach(async () => { + const secret = 'secret'; + app = express() + .use(cookieParser(secret)) + .use( + session({ + secret, + saveUninitialized: false, + resave: false, + cookie: { secure: false }, + }), + ) + .use(passport.initialize()) + .use(passport.session()); + await new Promise(resolve => { + backstageServer = app.listen(0, '0.0.0.0', () => { + appUrl = `http://127.0.0.1:${ + (backstageServer.address() as AddressInfo).port + }`; + resolve(null); + }); + }); + fakePinnipedSupervisor.use( + rest.all(`${appUrl}/*`, req => req.passthrough()), + ); + providerRouteHandler = pinniped.create()({ + providerId: 'pinniped', + globalConfig: { + baseUrl: `${appUrl}/api/auth`, + appUrl, + isOriginAllowed: _ => true, + }, + config: new ConfigReader({ + development: { + federationDomain: 'https://federationDomain.test', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + }), + logger: getVoidLogger(), + resolverContext: { + issueToken: async _ => ({ token: '' }), + findCatalogUser: async _ => ({ + entity: { + apiVersion: '', + kind: '', + metadata: { name: '' }, + }, + }), + signInWithCatalogUser: async _ => ({ token: '' }), + }, + }); + const router = Router(); + router + .use( + '/api/auth/pinniped/start', + providerRouteHandler.start.bind(providerRouteHandler), + ) + .use( + '/api/auth/pinniped/handler/frame', + providerRouteHandler.frameHandler.bind(providerRouteHandler), + ); + app.use(router); + }); + + afterEach(() => { + backstageServer.close(); + }); + + it('/handler/frame exchanges authorization codes from #start for Cluster Specific ID tokens', async () => { + const agent = request.agent(''); + const key = await generateKeyPair('ES256'); + const publicKey = await exportJWK(key.publicKey); + const privateKey = await exportJWK(key.privateKey); + publicKey.kid = privateKey.kid = uuid(); + publicKey.alg = privateKey.alg = 'ES256'; + + const signedJwt = await new SignJWT(testTokenMetadata) + .setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid }) + .setIssuer(testTokenMetadata.iss) + .setAudience(testTokenMetadata.aud) + .setSubject(testTokenMetadata.sub) + .setIssuedAt(testTokenMetadata.iat) + .setExpirationTime(testTokenMetadata.exp) + .sign(await importJWK(privateKey)); + + // make a /start request with audience parameter + const startResponse = await agent.get( + `${appUrl}/api/auth/pinniped/start?env=development&audience=test_cluster`, + ); + // follow redirect to authorization endpoint + const authorizationResponse = await agent.get( + startResponse.header.location, + ); + // follow redirect to token_endpoint + fakePinnipedSupervisor.use( + rest.post( + 'https://pinniped.test/oauth2/token', + async (req, res, ctx) => { + const formBody = new URLSearchParams(await req.text()); + const isGrantTypeTokenExchange = + formBody.get('grant_type') === + 'urn:ietf:params:oauth:grant-type:token-exchange'; + const hasValidTokenExchangeParams = + formBody.get('subject_token') === 'accessToken' && + formBody.get('audience') === 'test_cluster' && + formBody.get('subject_token_type') === + 'urn:ietf:params:oauth:token-type:access_token' && + formBody.get('requested_token_type') === + 'urn:ietf:params:oauth:token-type:jwt'; + + return res( + req.headers.get('Authorization') && + (!isGrantTypeTokenExchange || hasValidTokenExchangeParams) + ? ctx.json({ + access_token: isGrantTypeTokenExchange + ? clusterScopedIdToken + : 'accessToken', + refresh_token: 'refreshToken', + ...(!isGrantTypeTokenExchange && { id_token: signedJwt }), + scope: 'testScope', + }) + : ctx.status(401), + ); + }, + ), + rest.get('https://pinniped.test/jwks.json', async (_req, res, ctx) => + res(ctx.status(200), ctx.json({ keys: [{ ...publicKey }] })), + ), + ); + + const handlerResponse = await agent.get( + authorizationResponse.header.location, + ); + + expect(handlerResponse.text).toContain( + encodeURIComponent( + JSON.stringify({ + type: 'authorization_response', + response: { + providerInfo: { + accessToken: 'accessToken', + scope: 'testScope', + idToken: clusterScopedIdToken, + }, + profile: {}, + }, + }), + ), + ); + }, 70000); }); }); diff --git a/plugins/auth-backend/src/providers/pinniped/provider.ts b/plugins/auth-backend/src/providers/pinniped/provider.ts index e8ec21bf63..63776b5a81 100644 --- a/plugins/auth-backend/src/providers/pinniped/provider.ts +++ b/plugins/auth-backend/src/providers/pinniped/provider.ts @@ -33,7 +33,6 @@ import { OAuthStartResponse } from '../types'; import express from 'express'; import { OAuthAdapter, OAuthEnvironmentHandler } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import fetch from 'node-fetch'; type OidcImpl = { strategy: OidcStrategy; @@ -55,23 +54,15 @@ export type PinnipedOptions = OAuthProviderOptions & { export class PinnipedAuthProvider implements OAuthHandlers { private readonly implementation: Promise; - private readonly clientId: string; - private readonly clientSecret: string; - private readonly federationDomain: string; constructor(options: PinnipedOptions) { this.implementation = this.setupStrategy(options); - this.clientId = options.clientId; - this.clientSecret = options.clientSecret; - this.federationDomain = options.federationDomain; } async start(req: OAuthStartRequest): Promise { const { strategy } = await this.implementation; - const stringifiedAudience = req.query?.audience as string; const state = { ...req.state, audience: stringifiedAudience }; - const options: Record = { scope: req.scope || 'openid pinniped:request-audience username offline_access', @@ -88,33 +79,10 @@ export class PinnipedAuthProvider implements OAuthHandlers { }); } - private async rfc8693TokenExchange({ accessToken, audience }) { - const tokenEndpoint = `${this.federationDomain}/oauth2/token`; - const authString: string = `${this.clientId}:${this.clientSecret}`; - const encodedAuthString = Buffer.from(authString, 'base64'); - - const requestOptions = { - method: 'POST', - headers: { - 'Content-Type': 'x-www-form-urlencoded', - Authorization: `Basic ${encodedAuthString}`, - }, - body: `grant_type=urn:ietf:params:oauth:grant-type:token-exchange - &subject_token=${accessToken} - &subject_token_type=urn:ietf:params:oauth:token-type:access_token - &requested_token_type=urn:ietf:params:oauth:token-type:jwt - &audience=${audience}`, - }; - const response = await fetch(tokenEndpoint, requestOptions); - return response.idToken; - } - async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken?: string }> { const { strategy, client } = await this.implementation; - - // 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 : undefined; @@ -179,7 +147,8 @@ export class PinnipedAuthProvider implements OAuthHandlers { response: { providerInfo: { accessToken: tokenset.access_token!, - scope: 'none', + scope: tokenset.scope!, + idToken: tokenset.id_token, }, profile: {}, },