Skip to content

[SDK-4135] Add Pushed Authorization Requests #470

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 3 commits into from
May 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,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.
*/
Expand Down
9 changes: 9 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,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);
Expand Down
14 changes: 12 additions & 2 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand Down Expand Up @@ -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) {
Expand All @@ -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'), {
Expand Down
9 changes: 8 additions & 1 deletion lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ class ResponseContext {
: undefined),
};

const authParams = {
let authParams = {
...options.authorizationParams,
...authVerification,
};
Expand All @@ -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'
Expand Down
37 changes: 37 additions & 0 deletions test/client.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,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__',
Expand Down
1 change: 1 addition & 0 deletions test/fixture/well-known.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
34 changes: 34 additions & 0 deletions test/#.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('/#', {
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,
Expand Down