From cc2f0365adcb045a70820aae00f8fe63165f53af Mon Sep 17 00:00:00 2001 From: Graham Allen Date: Tue, 15 Sep 2026 22:25:58 -0700 Subject: [PATCH] handle user denying consent during presentation --- .../profiles/native-oid4vp-standard.js | 24 ++++++ .../61-oid4vp-standard-deny-consent.test.js | 75 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 test/unit/workflows/61-oid4vp-standard-deny-consent.test.js diff --git a/lib/workflows/profiles/native-oid4vp-standard.js b/lib/workflows/profiles/native-oid4vp-standard.js index 28d6019e..462eed86 100644 --- a/lib/workflows/profiles/native-oid4vp-standard.js +++ b/lib/workflows/profiles/native-oid4vp-standard.js @@ -230,6 +230,30 @@ export async function handleAuthorizationResponse({ responseBody = result.payload; } + if(typeof responseBody?.error === 'string') { + if(responseBody.state !== authorizationRequest.state) { + throw new Error( + 'Parameter \'state\' failed to match authorization request'); + } + + const step = exchange.step ?? 'default'; + return { + updatedExchange: { + ...exchange, + sequence: exchange.sequence + 1, + updatedAt: new Date(), + state: 'invalid', + variables: { + ...exchange.variables, + results: { + ...exchange.variables?.results, + [step]: {errors: [responseBody.error]} + } + } + } + }; + } + // Handle dc_api response (unencrypted, may come as JSON from DC API) // Extract vp_token and presentation_submission let vpToken; diff --git a/test/unit/workflows/61-oid4vp-standard-deny-consent.test.js b/test/unit/workflows/61-oid4vp-standard-deny-consent.test.js new file mode 100644 index 00000000..3893462c --- /dev/null +++ b/test/unit/workflows/61-oid4vp-standard-deny-consent.test.js @@ -0,0 +1,75 @@ +/*! + * Copyright 2023 - 2026 California Department of Motor Vehicles + * Copyright 2023 - 2026 Digital Bazaar, Inc. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +/* + * When the user declines to share, the wallet returns an OAuth error (e.g. + * `access_denied`) to the response endpoint instead of a vp_token. + * `handleAuthorizationResponse` must treat that as a failed (invalid) exchange + * with the error recorded — after validating `state` — rather than falling + * through to vp_token extraction. + */ +import expect from 'expect.js'; +import { + handleAuthorizationResponse +} from '../../../lib/workflows/profiles/native-oid4vp-standard.js'; + +function buildExchange({step = 'default', state = 's1'} = {}) { + return { + id: 'ex-1', + step, + sequence: 0, + variables: { + authorizationRequest: { + response_mode: 'direct_post', + state + } + } + }; +} + +describe('native-oid4vp-standard deny-consent handling', () => { + it('marks the exchange invalid when the wallet returns an error', + async () => { + const exchange = buildExchange(); + const {updatedExchange} = await handleAuthorizationResponse({ + workflow: {}, + exchange, + responseBody: {error: 'access_denied', state: 's1'} + }); + expect(updatedExchange.state).to.equal('invalid'); + expect(updatedExchange.sequence).to.equal(1); + expect(updatedExchange.variables.results.default.errors) + .to.eql(['access_denied']); + }); + + it('records the error under the current step', async () => { + const exchange = buildExchange({step: 'theStep'}); + const {updatedExchange} = await handleAuthorizationResponse({ + workflow: {}, + exchange, + responseBody: {error: 'access_denied', state: 's1'} + }); + expect(updatedExchange.variables.results.theStep.errors) + .to.eql(['access_denied']); + }); + + it('throws when the returned state does not match', async () => { + const exchange = buildExchange({state: 's1'}); + let threw = false; + try { + await handleAuthorizationResponse({ + workflow: {}, + exchange, + responseBody: {error: 'access_denied', state: 'WRONG'} + }); + } catch(e) { + threw = true; + expect(e.message).to.contain('state'); + } + expect(threw).to.equal(true); + }); +});