Skip to content
Open
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
24 changes: 24 additions & 0 deletions lib/workflows/profiles/native-oid4vp-standard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
75 changes: 75 additions & 0 deletions test/unit/workflows/61-oid4vp-standard-deny-consent.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});