Skip to content

Commit

Permalink
refactor(JAR): authorization requests with JAR now require a client_i…
Browse files Browse the repository at this point in the history
…d parameter
  • Loading branch information
panva committed Jan 16, 2024
1 parent a91add8 commit 9131cd5
Show file tree
Hide file tree
Showing 5 changed files with 12 additions and 401 deletions.
69 changes: 2 additions & 67 deletions lib/actions/authorization/check_client.js
Original file line number Diff line number Diff line change
@@ -1,76 +1,11 @@
import presence from '../../helpers/validate_presence.js';
import * as base64url from '../../helpers/base64url.js';
import instance from '../../helpers/weak_cache.js';
import { PUSHED_REQUEST_URN } from '../../consts/index.js';
import {
InvalidClient, InvalidRequestObject,
} from '../../helpers/errors.js';

import rejectRequestAndUri from './reject_request_and_uri.js';
import loadPushedAuthorizationRequest from './load_pushed_authorization_request.js';
import { InvalidClient } from '../../helpers/errors.js';

/*
* Checks client_id
* - value presence in provided params
* - value being resolved as a client
*/
export default async function checkClient(ctx, next) {
const { oidc: { params } } = ctx;
const { pushedAuthorizationRequests } = instance(ctx.oidc.provider).configuration('features');

try {
presence(ctx, 'client_id');
} catch (err) {
const { request_uri: requestUri } = params;
let { request } = params;

if (
!(
pushedAuthorizationRequests.enabled
&& requestUri
&& requestUri.startsWith(PUSHED_REQUEST_URN)
)
&& request === undefined
) {
throw err;
}

rejectRequestAndUri(ctx, () => {});

if (requestUri) {
const loadedRequestObject = await loadPushedAuthorizationRequest(ctx);
({ request } = loadedRequestObject);
}

const parts = request.split('.');
let decoded;
let clientId;

try {
if (parts.length !== 3 && parts.length !== 5) {
throw new Error();
}
parts.forEach((part, i, { length }) => {
if (length === 3 && i === 1) { // JWT Payload
decoded = JSON.parse(base64url.decodeToBuffer(part));
} else if (length === 5 && i === 0) { // JWE Header
decoded = JSON.parse(base64url.decodeToBuffer(part));
}
});
} catch (error) {
throw new InvalidRequestObject(`Request Object is not a valid ${parts.length === 5 ? 'JWE' : 'JWT'}`);
}

if (decoded) {
clientId = decoded.iss;
}

if (typeof clientId !== 'string' || !clientId) {
throw err;
}

params.client_id = clientId;
}
presence(ctx, 'client_id');

const client = await ctx.oidc.provider.Client.find(ctx.oidc.params.client_id);

Expand Down
18 changes: 7 additions & 11 deletions lib/actions/authorization/fetch_request_uri.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,12 @@ export default async function fetchRequestUri(ctx, next) {
throw new InvalidRequestUri('invalid request_uri scheme');
}

let loadedRequestObject = ctx.oidc.entities.PushedAuthorizationRequest;
if (
!loadedRequestObject
&& pushedAuthorizationRequests.enabled
&& params.request_uri.startsWith(PUSHED_REQUEST_URN)
) {
loadedRequestObject = await loadPushedAuthorizationRequest(ctx);
} else if (!loadedRequestObject && !requestObjects.requestUri) {
let pushedAuthorizationRequest;
if (pushedAuthorizationRequests.enabled && params.request_uri.startsWith(PUSHED_REQUEST_URN)) {
pushedAuthorizationRequest = await loadPushedAuthorizationRequest(ctx);
} else if (!requestObjects.requestUri) {
throw new RequestUriNotSupported();
} else if (!loadedRequestObject && ctx.oidc.client.requestUris) {
} else if (ctx.oidc.client.requestUris) {
if (!ctx.oidc.client.requestUriAllowed(params.request_uri)) {
throw new InvalidRequestUri('provided request_uri is not allowed');
}
Expand All @@ -48,8 +44,8 @@ export default async function fetchRequestUri(ctx, next) {
}

try {
if (loadedRequestObject) {
params.request = loadedRequestObject.request;
if (pushedAuthorizationRequest) {
params.request = pushedAuthorizationRequest.request;
} else {
const cache = instance(ctx.oidc.provider).requestUriCache;
params.request = await cache.resolve(params.request_uri);
Expand Down
108 changes: 0 additions & 108 deletions test/encryption/encryption.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as url from 'node:url';

import { expect } from 'chai';
import sinon from 'sinon';
import {
compactDecrypt, CompactEncrypt, decodeJwt, decodeProtectedHeader,
} from 'jose';
Expand Down Expand Up @@ -168,113 +167,6 @@ describe('encryption', () => {
});

describe('Request Object encryption', () => {
describe('JAR only request', () => {
it('fails without any other params even if client_id is replicated in the header', async function () {
const spy = sinon.spy();
this.provider.once('authorization.error', spy);

const signed = await JWT.sign({
client_id: 'client',
response_type: 'code',
redirect_uri: 'https://client.example.com/cb',
scope: 'openid',
}, Buffer.from('secret'), 'HS256', { issuer: 'client', audience: this.provider.issuer });

let [key] = i(this.provider).keystore.selectForEncrypt({ kty: 'RSA', alg: 'RSA-OAEP' });
key = await i(this.provider).keystore.getKeyObject(key, 'RSA-OAEP');

const encrypted = await new CompactEncrypt(encoder.encode(signed))
.setProtectedHeader({ enc: 'A128CBC-HS256', alg: 'RSA-OAEP', client_id: 'client' })
.encrypt(key);

return this.wrap({
route,
verb,
auth: {
request: encrypted,
},
})
.expect(400)
.expect(() => {
expect(spy.calledOnce).to.be.true;
expect(spy.args[0][1]).to.have.property('message', 'invalid_request');
expect(spy.args[0][1]).to.have.property('error_description', "missing required parameter 'client_id'");
});
});

it('works without any other params if iss is replicated in the header', async function () {
const signed = await JWT.sign({
client_id: 'client',
response_type: 'code',
redirect_uri: 'https://client.example.com/cb',
scope: 'openid',
}, Buffer.from('secret'), 'HS256', { issuer: 'client', audience: this.provider.issuer });

let [key] = i(this.provider).keystore.selectForEncrypt({ kty: 'RSA', alg: 'RSA-OAEP' });
key = await i(this.provider).keystore.getKeyObject(key, 'RSA-OAEP');

const encrypted = await new CompactEncrypt(encoder.encode(signed))
.setProtectedHeader({ enc: 'A128CBC-HS256', alg: 'RSA-OAEP', iss: 'client' })
.encrypt(key);

return this.wrap({
route,
verb,
auth: {
request: encrypted,
},
})
.expect(303)
.expect((response) => {
const expected = url.parse('https://client.example.com/cb', true);
const actual = url.parse(response.headers.location, true);
['protocol', 'host', 'pathname'].forEach((attr) => {
expect(actual[attr]).to.equal(expected[attr]);
});
expect(actual.query).to.have.property('code');
});
});

it('handles invalid JWE', async function () {
const spy = sinon.spy();
this.provider.once('authorization.error', spy);

const signed = await JWT.sign({
client_id: 'client',
response_type: 'code',
redirect_uri: 'https://client.example.com/cb',
scope: 'openid',
}, Buffer.from('secret'), 'HS256', { issuer: 'client', audience: this.provider.issuer });

let [key] = i(this.provider).keystore.selectForEncrypt({ kty: 'RSA', alg: 'RSA-OAEP' });
key = await i(this.provider).keystore.getKeyObject(key, 'RSA-OAEP');

const encrypted = await new CompactEncrypt(encoder.encode(signed))
.setProtectedHeader({ enc: 'A128CBC-HS256', alg: 'RSA-OAEP', client_id: 'client' })
.encrypt(key);

return this.wrap({
route,
verb,
auth: {
request: encrypted.split('.').map((part, i) => {
if (i === 0) {
return 'foo';
}

return part;
}).join('.'),
},
})
.expect(400)
.expect(() => {
expect(spy.calledOnce).to.be.true;
expect(spy.args[0][1]).to.have.property('message', 'invalid_request_object');
expect(spy.args[0][1]).to.have.property('error_description', 'Request Object is not a valid JWE');
});
});
});

it('handles enc unsupported algs', async function () {
const signed = await JWT.sign({
client_id: 'client',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,30 +236,6 @@ describe('Pushed Request Object', () => {

expect(await this.provider.PushedAuthorizationRequest.find(id)).not.to.be.ok;
});

it('allows the request_uri to be used without passing client_id to the request', async function () {
const { body: { request_uri } } = await this.agent.post('/request')
.auth(clientId, 'secret')
.type('form')
.send({
scope: 'openid',
response_type: 'code',
client_id: clientId,
iss: clientId,
aud: this.provider.issuer,
});

const auth = new this.AuthorizationRequest({
client_id: undefined,
state: undefined,
redirect_uri: undefined,
request_uri,
});

return this.wrap({ route: '/auth', verb: 'get', auth })
.expect(303)
.expect(auth.validatePresence(['code']));
});
});
});
});
Expand Down Expand Up @@ -616,6 +592,7 @@ describe('Pushed Request Object', () => {

it('handles expired or invalid pushed authorization request object', async function () {
const auth = new this.AuthorizationRequest({
client_id: clientId,
request_uri: 'urn:ietf:params:oauth:request_uri:foobar',
});

Expand All @@ -627,54 +604,6 @@ describe('Pushed Request Object', () => {
.expect(auth.validateError('invalid_request_uri'))
.expect(auth.validateErrorDescription('request_uri is invalid or expired'));
});

it('handles expired or invalid pushed authorization request object (when no client_id in the request)', async function () {
const renderSpy = sinon.spy(i(this.provider).configuration(), 'renderError');

const auth = new this.AuthorizationRequest({
client_id: undefined,
request_uri: 'urn:ietf:params:oauth:request_uri:foobar',
});

return this.wrap({ route: '/auth', verb: 'get', auth })
.expect(() => {
renderSpy.restore();
})
.expect(400)
.expect(() => {
expect(renderSpy.calledOnce).to.be.true;
const renderArgs = renderSpy.args[0];
expect(renderArgs[1]).to.have.property('error', 'invalid_request_uri');
expect(renderArgs[1]).to.have.property('error_description', 'request_uri is invalid or expired');
});
});

it('allows the request_uri to be used without passing client_id to the request', async function () {
const { body: { request_uri } } = await this.agent.post('/request')
.auth(clientId, 'secret')
.type('form')
.send({
request: await JWT.sign({
jti: randomBytes(16).toString('base64url'),
scope: 'openid',
response_type: 'code',
client_id: clientId,
iss: clientId,
aud: this.provider.issuer,
}, this.key, 'HS256', { expiresIn: 30 }),
});

const auth = new this.AuthorizationRequest({
client_id: undefined,
state: undefined,
redirect_uri: undefined,
request_uri,
});

return this.wrap({ route: '/auth', verb: 'get', auth })
.expect(303)
.expect(auth.validatePresence(['code']));
});
});
});
});
Expand Down
Loading

0 comments on commit 9131cd5

Please sign in to comment.