From 6ac4f7fc819630df82f5c71b00ff31152d532478 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Wed, 12 Apr 2023 10:55:37 +0100 Subject: [PATCH 1/2] add PAR --- index.d.ts | 5 +++++ lib/client.js | 9 +++++++++ lib/config.js | 14 ++++++++++++-- lib/context.js | 9 ++++++++- test/client.tests.js | 37 ++++++++++++++++++++++++++++++++++++ test/fixture/well-known.json | 1 + 6 files changed, 72 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 4d30c274..66a3ddf3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -477,6 +477,11 @@ interface ConfigParams { */ authRequired?: boolean; + /** + * Perform a Pushed Authorization Request at the issuer's pushed_authorization_request_endpoint at login. + */ + pushedAuthorizationRequests?: boolean; + /** * Configuration for the login, logout, callback and postLogoutRedirect routes. */ diff --git a/lib/client.js b/lib/client.js index 19908ec3..376ae983 100644 --- a/lib/client.js +++ b/lib/client.js @@ -83,6 +83,15 @@ async function get(config) { ); } + if ( + config.pushedAuthorizationRequests && + !issuer.pushed_authorization_request_endpoint + ) { + throw new TypeError( + 'pushed_authorization_request_endpoint must be configured on the issuer to use pushedAuthorizationRequests' + ); + } + let jwks; if (config.clientAssertionSigningKey) { const jwk = JWK.asKey(config.clientAssertionSigningKey).toJWK(true); diff --git a/lib/config.js b/lib/config.js index 98572ef3..308d14c3 100644 --- a/lib/config.js +++ b/lib/config.js @@ -185,6 +185,7 @@ const paramsSchema = Joi.object({ issuerBaseURL: Joi.string().uri().required(), legacySameSiteCookie: Joi.boolean().optional().default(true), authRequired: Joi.boolean().optional().default(true), + pushedAuthorizationRequests: Joi.boolean().optional().default(false), routes: Joi.object({ login: Joi.alternatives([ Joi.string().uri({ relativeOnly: true }), @@ -212,7 +213,10 @@ const paramsSchema = Joi.object({ ) .optional() .default((parent) => { - if (parent.authorizationParams.response_type === 'id_token') { + if ( + parent.authorizationParams.response_type === 'id_token' && + !parent.pushedAuthorizationRequests + ) { return 'none'; } if (parent.clientAssertionSigningKey) { @@ -230,7 +234,13 @@ const paramsSchema = Joi.object({ 'any.only': 'Public code flow clients are not supported.', }), } - ), + ) + .when(Joi.ref('pushedAuthorizationRequests'), { + is: true, + then: Joi.string().invalid('none').messages({ + 'any.only': 'Public PAR clients are not supported.', + }), + }), clientAssertionSigningKey: Joi.any() .optional() .when(Joi.ref('clientAuthMethod'), { diff --git a/lib/context.js b/lib/context.js index 66437d09..5592ac2c 100644 --- a/lib/context.js +++ b/lib/context.js @@ -240,7 +240,7 @@ class ResponseContext { : undefined), }; - const authParams = { + let authParams = { ...options.authorizationParams, ...authVerification, }; @@ -259,6 +259,13 @@ class ResponseContext { ); } + if (config.pushedAuthorizationRequests) { + const { request_uri } = await client.pushedAuthorizationRequest( + authParams + ); + authParams = { request_uri }; + } + transient.store(config.transactionCookie.name, req, res, { sameSite: options.authorizationParams.response_mode === 'form_post' diff --git a/test/client.tests.js b/test/client.tests.js index e184540a..e554646a 100644 --- a/test/client.tests.js +++ b/test/client.tests.js @@ -270,6 +270,43 @@ describe('client initialization', function () { }); }); + describe('client respects pushedAuthorizationRequests configuration', function () { + it('should fail if configured with PAR and issuer has no PAR endpoint', async function () { + const config = getConfig({ + secret: '__test_session_secret__', + clientID: '__test_client_id__', + clientSecret: '__test_client_secret__', + issuerBaseURL: 'https://par-test.auth0.com', + baseURL: 'https://example.org', + pushedAuthorizationRequests: true, + }); + const { pushed_authorization_request_endpoint, ...rest } = wellKnown; + nock('https://par-test.auth0.com') + .persist() + .get('/.well-known/openid-configuration') + .reply(200, rest); + await expect(getClient(config)).to.be.rejectedWith( + `pushed_authorization_request_endpoint must be configured on the issuer to use pushedAuthorizationRequests` + ); + }); + + it('should succeed if configured with PAR and issuer has PAR endpoint', async function () { + const config = getConfig({ + secret: '__test_session_secret__', + clientID: '__test_client_id__', + clientSecret: '__test_client_secret__', + issuerBaseURL: 'https://par-test.auth0.com', + baseURL: 'https://example.org', + pushedAuthorizationRequests: true, + }); + nock('https://par-test.auth0.com') + .persist() + .get('/.well-known/openid-configuration') + .reply(200, wellKnown); + await expect(getClient(config)).to.be.fulfilled; + }); + }); + describe('client respects clientAssertionSigningAlg configuration', function () { const config = { secret: '__test_session_secret__', diff --git a/test/fixture/well-known.json b/test/fixture/well-known.json index e288a04f..0c56ca9d 100644 --- a/test/fixture/well-known.json +++ b/test/fixture/well-known.json @@ -2,6 +2,7 @@ "issuer": "https://op.example.com/", "authorization_endpoint": "https://op.example.com/authorize", "token_endpoint": "https://op.example.com/oauth/token", + "pushed_authorization_request_endpoint": "https://op.example.com/oauth/par", "userinfo_endpoint": "https://op.example.com/userinfo", "mfa_challenge_endpoint": "https://op.example.com/mfa/challenge", "jwks_uri": "https://op.example.com/.well-known/jwks.json", From 768d8eda0069ad53860a1dff54297978f5df8ccf Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Fri, 5 May 2023 08:32:35 +0100 Subject: [PATCH 2/2] Add login test --- test/login.tests.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/login.tests.js b/test/login.tests.js index 42d4b0f3..255804a2 100644 --- a/test/login.tests.js +++ b/test/login.tests.js @@ -328,6 +328,40 @@ describe('auth', () => { ); }); + it('should redirect to the authorize url when pushed authorize requests enabled', async () => { + nock(defaultConfig.issuerBaseURL) + .post('/oauth/par', { + client_id: '__test_client_id__', + client_secret: 'test-client-secret', + nonce: /.+/, + redirect_uri: 'https://example.org/callback', + response_mode: 'form_post', + response_type: 'id_token', + scope: 'openid profile email', + state: /.+/, + }) + .reply(201, { request_uri: 'foo', expires_in: 100 }); + + server = await createServer( + auth({ + ...defaultConfig, + clientSecret: 'test-client-secret', + pushedAuthorizationRequests: true, + clientAuthMethod: 'client_secret_post', + }) + ); + const res = await request.get('/login', { + baseUrl, + followRedirect: false, + }); + console.log(res); + assert.equal(res.statusCode, 302); + + const parsed = url.parse(res.headers.location, true); + assert.equal(parsed.query.request_uri, 'foo'); + assert.equal(parsed.query.client_id, '__test_client_id__'); + }); + it('should allow custom login route with additional login params', async () => { const router = auth({ ...defaultConfig,