diff --git a/example/my_adapter.js b/example/my_adapter.js index 7e8ef0f33..e97e5cfeb 100644 --- a/example/my_adapter.js +++ b/example/my_adapter.js @@ -135,6 +135,7 @@ class MyAdapter { * - iat {number} - timestamp of the interaction's creation * - returnTo {string} - after resolving interactions send the user-agent to this url * - deviceCode {string} - [DeviceCode user flows only] deviceCode reference + * - parJti {string} - [PAR user flows only] PushedAuthorizationCode uid reference * - params {object} - parsed recognized parameters object * - lastSubmission {object} - previous interaction result submission * - trusted {string[]} - parameter names that come from a trusted source diff --git a/lib/actions/authorization/interactions.js b/lib/actions/authorization/interactions.js index 3deb0737a..1545404ae 100644 --- a/lib/actions/authorization/interactions.js +++ b/lib/actions/authorization/interactions.js @@ -108,7 +108,8 @@ export default async function interactions(resumeRouteName, ctx, next) { session: oidc.session, grant: oidc.grant, cid: oidc.entities.Interaction?.cid || nanoid(), - ...(oidc.deviceCode ? { deviceCode: oidc.deviceCode.jti } : undefined), + deviceCode: oidc.deviceCode?.jti, + parJti: oidc.entities.PushedAuthorizationRequest?.jti || oidc.entities.Interaction?.parJti, }); let ttl = instance(ctx.oidc.provider).configuration('ttl.Interaction'); diff --git a/lib/actions/authorization/load_pushed_authorization_request.js b/lib/actions/authorization/load_pushed_authorization_request.js index 533ff6e5e..1373a6800 100644 --- a/lib/actions/authorization/load_pushed_authorization_request.js +++ b/lib/actions/authorization/load_pushed_authorization_request.js @@ -7,12 +7,12 @@ export default async function loadPushedAuthorizationRequest(ctx) { const requestObject = await ctx.oidc.provider.PushedAuthorizationRequest.find(id, { ignoreExpiration: true, }); - if (!requestObject || requestObject.isExpired) { - throw new InvalidRequestUri('request_uri is invalid or expired'); + + if (!requestObject || !requestObject.isValid) { + throw new InvalidRequestUri('request_uri is invalid, expired, or was already used'); } - ctx.oidc.entity('PushedAuthorizationRequest', requestObject); - await requestObject.destroy(); + ctx.oidc.entity('PushedAuthorizationRequest', requestObject); return requestObject; } diff --git a/lib/actions/authorization/respond.js b/lib/actions/authorization/respond.js index 5b921f151..5b146829b 100644 --- a/lib/actions/authorization/respond.js +++ b/lib/actions/authorization/respond.js @@ -1,4 +1,5 @@ import instance from '../../helpers/weak_cache.js'; +import { InvalidRequestUri } from '../../helpers/errors.js'; /* * Based on the authorization request response mode either redirects with parameters in query or @@ -10,6 +11,20 @@ import instance from '../../helpers/weak_cache.js'; * @emits: authorization.success */ export default async function respond(ctx, next) { + let pushedAuthorizationRequest = ctx.oidc.entities.PushedAuthorizationRequest; + + if (!pushedAuthorizationRequest && ctx.oidc.entities.Interaction?.parJti) { + pushedAuthorizationRequest = await ctx.oidc.provider.PushedAuthorizationRequest.find( + ctx.oidc.entities.Interaction.parJti, + { ignoreExpiration: true }, + ); + } + + if (pushedAuthorizationRequest?.consumed) { + throw new InvalidRequestUri('request_uri is invalid, expired, or was already used'); + } + await pushedAuthorizationRequest?.consume(); + const out = await next(); const { oidc: { params } } = ctx; diff --git a/lib/models/interaction.js b/lib/models/interaction.js index 972526cd5..52c9a2e58 100644 --- a/lib/models/interaction.js +++ b/lib/models/interaction.js @@ -67,6 +67,7 @@ export default (provider) => class Interaction extends hasFormat(provider, 'Inte 'lastSubmission', 'deviceCode', 'cid', + 'parJti', ]; } }; diff --git a/lib/models/pushed_authorization_request.js b/lib/models/pushed_authorization_request.js index 143f087f6..4b0fc3547 100644 --- a/lib/models/pushed_authorization_request.js +++ b/lib/models/pushed_authorization_request.js @@ -1,8 +1,13 @@ import instance from '../helpers/weak_cache.js'; +import apply from './mixins/apply.js'; import hasFormat from './mixins/has_format.js'; +import consumable from './mixins/consumable.js'; -export default (provider) => class PushedAuthorizationRequest extends hasFormat(provider, 'PushedAuthorizationRequest', instance(provider).BaseModel) { +export default (provider) => class PushedAuthorizationRequest extends apply([ + consumable, + hasFormat(provider, 'PushedAuthorizationRequest', instance(provider).BaseModel), +]) { static get IN_PAYLOAD() { return [ ...super.IN_PAYLOAD, diff --git a/test/pushed_authorization_requests/pushed_authorization_requests.test.js b/test/pushed_authorization_requests/pushed_authorization_requests.test.js index 5c263e9b9..cd562006f 100644 --- a/test/pushed_authorization_requests/pushed_authorization_requests.test.js +++ b/test/pushed_authorization_requests/pushed_authorization_requests.test.js @@ -201,7 +201,7 @@ describe('Pushed Request Object', () => { .expect(303) .expect(auth.validatePresence(['code'])); - expect(await this.provider.PushedAuthorizationRequest.find(id)).not.to.be.ok; + expect(await this.provider.PushedAuthorizationRequest.find(id)).to.be.ok.and.have.property('consumed').and.is.ok; }); it('allows the request_uri to be used (when request object was not used but client has request_object_signing_alg for its optional use)', async function () { @@ -234,7 +234,7 @@ describe('Pushed Request Object', () => { .expect(303) .expect(auth.validatePresence(['code'])); - expect(await this.provider.PushedAuthorizationRequest.find(id)).not.to.be.ok; + expect(await this.provider.PushedAuthorizationRequest.find(id)).to.be.ok.and.have.property('consumed').and.is.ok; }); }); }); @@ -556,7 +556,7 @@ describe('Pushed Request Object', () => { .expect(303) .expect(auth.validatePresence(['code'])); - expect(await this.provider.PushedAuthorizationRequest.find(id)).not.to.be.ok; + expect(await this.provider.PushedAuthorizationRequest.find(id)).to.be.ok.and.have.property('consumed').and.is.ok; }); it('handles expired or invalid pushed authorization request object', async function () { @@ -571,7 +571,7 @@ describe('Pushed Request Object', () => { .expect(auth.validateState) .expect(auth.validateClientLocation) .expect(auth.validateError('invalid_request_uri')) - .expect(auth.validateErrorDescription('request_uri is invalid or expired')); + .expect(auth.validateErrorDescription('request_uri is invalid, expired, or was already used')); }); }); });