Skip to content

Commit 836811a

Browse files
authored
Merge pull request #262 from TimoGlastra/fix/oid4vp-error-response
feat: support authorization error response for oid4vp response
2 parents 661430a + d9524be commit 836811a

6 files changed

Lines changed: 97 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@openid4vc/openid4vp": patch
3+
---
4+
5+
Support parsing OpenID4VP Authorization Error Responses. When the wallet returns an authorization error response (e.g. it detected an error with the request, or is unavailable) instead of a successful response containing a `vp_token`, `parseOpenid4VpAuthorizationResponsePayload` (and `parseOpenid4vpAuthorizationResponse`) now throws an `Openid4vpAuthorizationResponseError` with the parsed `errorResponse`, instead of a confusing zod error about the missing `vp_token`.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Oauth2Error, type Oauth2ErrorOptions } from '@openid4vc/oauth2'
2+
import type { Openid4vpAuthorizationErrorResponse } from './z-authorization-response'
3+
4+
/**
5+
* Error thrown when the wallet returns an OpenID4VP Authorization Error Response
6+
* (e.g. the wallet detected an error with the request, or is unavailable) instead
7+
* of a successful Authorization Response containing a `vp_token`.
8+
*/
9+
export class Openid4vpAuthorizationResponseError extends Oauth2Error {
10+
public constructor(
11+
message: string,
12+
public readonly errorResponse: Openid4vpAuthorizationErrorResponse,
13+
options?: Oauth2ErrorOptions
14+
) {
15+
super(`${message}\n${JSON.stringify(errorResponse, null, 2)}`, options)
16+
}
17+
}

packages/openid4vp/src/authorization-response/__tests__/parse-authorization-response-payload.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, expect, test } from 'vitest'
2+
import { Openid4vpAuthorizationResponseError } from '../Openid4vpAuthorizationResponseError'
23
import { parseOpenid4VpAuthorizationResponsePayload } from '../parse-authorization-response-payload'
34

45
describe('parseOpenid4VpAuthorizationResponsePayload', () => {
@@ -26,4 +27,41 @@ describe('parseOpenid4VpAuthorizationResponsePayload', () => {
2627
},
2728
})
2829
})
30+
31+
test('should throw an Openid4vpAuthorizationResponseError when the wallet returns an authorization error response', () => {
32+
// Non-normative example of an Authorization Error Response from the OpenID4VP spec.
33+
const parsedPayload = Object.fromEntries(
34+
new URLSearchParams(
35+
'error=invalid_request&error_description=unsupported%20client_id_prefix&state=eyJhb...6-sVA'
36+
).entries()
37+
)
38+
39+
let error: unknown
40+
try {
41+
parseOpenid4VpAuthorizationResponsePayload(parsedPayload)
42+
} catch (e) {
43+
error = e
44+
}
45+
46+
expect(error).toBeInstanceOf(Openid4vpAuthorizationResponseError)
47+
expect((error as Openid4vpAuthorizationResponseError).errorResponse).toEqual({
48+
error: 'invalid_request',
49+
error_description: 'unsupported client_id_prefix',
50+
state: 'eyJhb...6-sVA',
51+
})
52+
})
53+
54+
test('should throw an Openid4vpAuthorizationResponseError for an error response without additional parameters', () => {
55+
let error: unknown
56+
try {
57+
parseOpenid4VpAuthorizationResponsePayload({ error: 'wallet_unavailable' })
58+
} catch (e) {
59+
error = e
60+
}
61+
62+
expect(error).toBeInstanceOf(Openid4vpAuthorizationResponseError)
63+
expect((error as Openid4vpAuthorizationResponseError).errorResponse).toEqual({
64+
error: 'wallet_unavailable',
65+
})
66+
})
2967
})

packages/openid4vp/src/authorization-response/parse-authorization-response-payload.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
import { parseWithErrorHandling } from '@openid4vc/utils'
2-
import { zOpenid4vpAuthorizationResponse } from './z-authorization-response'
2+
import { Openid4vpAuthorizationResponseError } from './Openid4vpAuthorizationResponseError'
3+
import { zOpenid4vpAuthorizationErrorResponse, zOpenid4vpAuthorizationResponse } from './z-authorization-response'
34

45
export function parseOpenid4VpAuthorizationResponsePayload(payload: Record<string, unknown>) {
6+
// A wallet can return an authorization error response (e.g. if it detected an error
7+
// with the request, or is unavailable) instead of a successful authorization response.
8+
// We detect this based on the presence of the `error` parameter and throw a dedicated
9+
// error, instead of a confusing zod error about the missing `vp_token`.
10+
if (typeof payload.error === 'string') {
11+
const errorResponse = parseWithErrorHandling(
12+
zOpenid4vpAuthorizationErrorResponse,
13+
payload,
14+
'Failed to parse openid4vp authorization error response.'
15+
)
16+
17+
throw new Openid4vpAuthorizationResponseError(
18+
'The wallet returned an openid4vp authorization error response.',
19+
errorResponse
20+
)
21+
}
22+
523
return parseWithErrorHandling(
624
zOpenid4vpAuthorizationResponse,
725
payload,

packages/openid4vp/src/authorization-response/z-authorization-response.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { zOauth2ErrorResponse } from '@openid4vc/oauth2'
12
import { zStringToJson } from '@openid4vc/utils'
23
import { z } from 'zod'
34
import { zPexPresentationSubmission } from '../models/z-pex'
@@ -13,6 +14,20 @@ export const zOpenid4vpAuthorizationResponse = z
1314
token_type: z.string().optional(),
1415
access_token: z.string().optional(),
1516
expires_in: z.coerce.number().optional(),
17+
18+
// This allows for discriminating between error and success responses.
19+
error: z.optional(z.never()),
1620
})
1721
.loose()
1822
export type Openid4vpAuthorizationResponse = z.infer<typeof zOpenid4vpAuthorizationResponse>
23+
24+
export const zOpenid4vpAuthorizationErrorResponse = z
25+
.object({
26+
...zOauth2ErrorResponse.shape,
27+
state: z.string().optional(),
28+
29+
// This allows for discriminating between error and success responses.
30+
vp_token: z.optional(z.never()),
31+
})
32+
.loose()
33+
export type Openid4vpAuthorizationErrorResponse = z.infer<typeof zOpenid4vpAuthorizationErrorResponse>

packages/openid4vp/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export {
3838
type CreateOpenid4vpAuthorizationResponseResult,
3939
createOpenid4vpAuthorizationResponse,
4040
} from './authorization-response/create-authorization-response'
41+
export { Openid4vpAuthorizationResponseError } from './authorization-response/Openid4vpAuthorizationResponseError'
4142
export {
4243
type ParsedOpenid4vpAuthorizationResponse,
4344
type ParseOpenid4vpAuthorizationResponseOptions,
@@ -62,7 +63,9 @@ export type {
6263
ValidateOpenid4VpPexAuthorizationResponseResult,
6364
} from './authorization-response/validate-authorization-response-result'
6465
export {
66+
type Openid4vpAuthorizationErrorResponse,
6567
type Openid4vpAuthorizationResponse,
68+
zOpenid4vpAuthorizationErrorResponse,
6669
zOpenid4vpAuthorizationResponse,
6770
} from './authorization-response/z-authorization-response'
6871
export {

0 commit comments

Comments
 (0)