Skip to content

Commit aa029fe

Browse files
committed
chore: vp_token repairs
1 parent e99d181 commit aa029fe

4 files changed

Lines changed: 169 additions & 45 deletions

File tree

packages/siop-oid4vp/lib/authorization-response/Payload.ts

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,88 @@ import { AuthorizationRequest } from '../authorization-request'
33
import { IDToken } from '../id-token'
44
import { RequestObject } from '../request-object'
55
import { assertValidResponseOpts } from './Opts'
6-
import { AuthorizationRequestPayload, AuthorizationResponsePayload, IDTokenPayload, SIOPErrors } from '../types'
6+
import {
7+
AuthorizationRequestPayload,
8+
AuthorizationResponsePayload,
9+
DcqlPresentationEntry,
10+
DcqlVpToken,
11+
DcqlVpTokenInput,
12+
IDTokenPayload,
13+
NonEmptyArray,
14+
SIOPErrors
15+
} from '../types'
716
import { AuthorizationResponseOpts } from './types'
817

18+
19+
/**
20+
* Checks if an object is array-like (has only numeric string keys: "0", "1", "2", etc.)
21+
* This handles objects that were serialized arrays: { "0": val1, "1": val2 }
22+
*/
23+
const isArrayLikeObject = (value: unknown): value is Record<string, DcqlPresentationEntry> => {
24+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
25+
return false
26+
}
27+
const keys = Object.keys(value)
28+
return keys.length > 0 && keys.every(key => /^\d+$/.test(key))
29+
}
30+
31+
/**
32+
* Normalizes a credential query value to an array.
33+
* Handles three input formats:
34+
* 1. Single value: "credential" -> ["credential"]
35+
* 2. Array: ["cred1", "cred2"] -> ["cred1", "cred2"]
36+
* 3. Array-like object: {"0": "cred1", "1": "cred2"} -> ["cred1", "cred2"]
37+
*/
38+
const normalizeToArray = (
39+
credentialQueryId: string,
40+
value: DcqlPresentationEntry | DcqlPresentationEntry[] | Record<string, DcqlPresentationEntry>
41+
): NonEmptyArray<DcqlPresentationEntry> => {
42+
let presentationsArray: DcqlPresentationEntry[]
43+
44+
if (Array.isArray(value)) {
45+
presentationsArray = value
46+
} else if (isArrayLikeObject(value)) {
47+
// Convert array-like object to array by sorting keys numerically
48+
const sortedKeys = Object.keys(value).sort((a, b) => Number(a) - Number(b))
49+
presentationsArray = sortedKeys.map(key => value[key])
50+
} else {
51+
// Single value, wrap in array
52+
presentationsArray = [value]
53+
}
54+
55+
if (presentationsArray.length === 0) {
56+
throw new Error(
57+
`DCQL presentations for credential query '${credentialQueryId}' cannot be empty`
58+
)
59+
}
60+
61+
// Cast to NonEmptyArray since we verified length > 0
62+
return presentationsArray as NonEmptyArray<DcqlPresentationEntry>
63+
}
64+
65+
/**
66+
* Converts a DCQL presentation input (which may have mixed formats) to the canonical
67+
* format where all credential queries map to non-empty arrays of presentations.
68+
*
69+
* This ensures consistent handling of:
70+
* - Single presentations: { "PID": "eyJ..." } -> { "PID": ["eyJ..."] }
71+
* - Array presentations: { "PID": ["eyJ..."] } -> { "PID": ["eyJ..."] }
72+
* - Array-like objects: { "PID": {"0": "eyJ..."} } -> { "PID": ["eyJ..."] }
73+
*/
74+
const toCanonicalDcqlPresentation = (input: DcqlVpTokenInput): DcqlVpToken => {
75+
return Object.fromEntries(
76+
Object.entries(input).map(([credentialQueryId, value]) => {
77+
const presentationsArray = normalizeToArray(credentialQueryId, value)
78+
return [credentialQueryId, presentationsArray]
79+
})
80+
) as DcqlVpToken
81+
}
82+
83+
984
export const createResponsePayload = async (
1085
authorizationRequest: AuthorizationRequest,
1186
responseOpts: AuthorizationResponseOpts,
12-
idTokenPayload?: IDTokenPayload,
87+
idTokenPayload?: IDTokenPayload
1388
): Promise<AuthorizationResponsePayload | undefined> => {
1489
assertValidResponseOpts(responseOpts)
1590
if (!authorizationRequest) {
@@ -20,15 +95,21 @@ export const createResponsePayload = async (
2095
const state: string | undefined = authorizationRequest.getMergedProperty('state')
2196

2297
const responsePayload: AuthorizationResponsePayload = {
23-
...(responseOpts.accessToken && { access_token: responseOpts.accessToken, expires_in: responseOpts.expiresIn || 3600 }),
98+
...(responseOpts.accessToken && {
99+
access_token: responseOpts.accessToken,
100+
expires_in: responseOpts.expiresIn || 3600
101+
}),
24102
...(responseOpts.tokenType && { token_type: responseOpts.tokenType }),
25103
...(responseOpts.refreshToken && { refresh_token: responseOpts.refreshToken }),
26104
...(responseOpts.isFirstParty && { is_first_party: responseOpts.isFirstParty }),
27-
state,
105+
state
28106
}
29107

30108
if (responseOpts.dcqlResponse?.dcqlPresentation) {
31-
responsePayload.vp_token = DcqlPresentation.encode(responseOpts.dcqlResponse.dcqlPresentation as DcqlPresentation)
109+
const canonicalPresentation = toCanonicalDcqlPresentation(
110+
responseOpts.dcqlResponse.dcqlPresentation
111+
)
112+
responsePayload.vp_token = DcqlPresentation.encode(canonicalPresentation)
32113
}
33114

34115
if (idTokenPayload) {
@@ -46,7 +127,7 @@ export const createResponsePayload = async (
46127
*/
47128
export const mergeOAuth2AndOpenIdInRequestPayload = async (
48129
payload: AuthorizationRequestPayload,
49-
requestObject?: RequestObject,
130+
requestObject?: RequestObject
50131
): Promise<AuthorizationRequestPayload> => {
51132
const payloadCopy = JSON.parse(JSON.stringify(payload))
52133

packages/siop-oid4vp/lib/authorization-response/types.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,22 @@ import {
66
HasherSync,
77
MdocOid4vpIssuerSigned,
88
PresentationSubmission,
9-
W3CVerifiablePresentation,
9+
W3CVerifiablePresentation
1010
} from '@sphereon/ssi-types'
1111
import { DcqlQuery } from 'dcql'
1212
import { AuthorizationResponse } from './AuthorizationResponse'
1313
import {
1414
CreateJwtCallback,
15+
DcqlVpTokenInput,
16+
ResponseIss,
1517
ResponseMode,
1618
ResponseRegistrationOpts,
1719
ResponseType,
1820
ResponseURIType,
1921
SupportedVersion,
2022
VerifiablePresentationWithFormat,
2123
Verification,
22-
VerifyJwtCallback,
23-
ResponseIss
24+
VerifyJwtCallback
2425
} from '../types'
2526

2627
export interface AuthorizationResponseOpts {
@@ -42,7 +43,7 @@ export interface AuthorizationResponseOpts {
4243
}
4344

4445
export interface DcqlResponseOpts {
45-
dcqlPresentation: Record<string, string | Record<string, unknown> | Array<string | Record<string, unknown>>>
46+
dcqlPresentation: DcqlVpTokenInput
4647
}
4748

4849
export interface DcqlQueryPayloadOpts {

packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -874,38 +874,76 @@ export const AuthorizationResponseOptsSchemaObj = {
874874
"type": "object",
875875
"properties": {
876876
"dcqlPresentation": {
877-
"type": "object",
878-
"additionalProperties": {
879-
"anyOf": [
880-
{
881-
"type": "string"
882-
},
883-
{
884-
"type": "object",
885-
"additionalProperties": {}
886-
},
887-
{
888-
"type": "array",
889-
"items": {
890-
"anyOf": [
891-
{
892-
"type": "string"
893-
},
894-
{
895-
"type": "object",
896-
"additionalProperties": {}
897-
}
898-
]
899-
}
900-
}
901-
]
902-
}
877+
"$ref": "#/definitions/DcqlVpTokenInput"
903878
}
904879
},
905880
"required": [
906881
"dcqlPresentation"
907882
],
908883
"additionalProperties": false
884+
},
885+
"DcqlVpTokenInput": {
886+
"type": "object",
887+
"additionalProperties": {
888+
"anyOf": [
889+
{
890+
"$ref": "#/definitions/DcqlPresentationEntry"
891+
},
892+
{
893+
"type": "array",
894+
"items": {
895+
"$ref": "#/definitions/DcqlPresentationEntry"
896+
}
897+
},
898+
{
899+
"type": "object",
900+
"additionalProperties": {
901+
"$ref": "#/definitions/DcqlPresentationEntry"
902+
}
903+
}
904+
]
905+
}
906+
},
907+
"DcqlPresentationEntry": {
908+
"anyOf": [
909+
{
910+
"type": "string"
911+
},
912+
{
913+
"type": "object",
914+
"additionalProperties": {
915+
"$ref": "#/definitions/Json"
916+
}
917+
}
918+
]
919+
},
920+
"Json": {
921+
"anyOf": [
922+
{
923+
"type": "string"
924+
},
925+
{
926+
"type": "number"
927+
},
928+
{
929+
"type": "boolean"
930+
},
931+
{
932+
"type": "null"
933+
},
934+
{
935+
"type": "object",
936+
"additionalProperties": {
937+
"$ref": "#/definitions/Json"
938+
}
939+
},
940+
{
941+
"type": "array",
942+
"items": {
943+
"$ref": "#/definitions/Json"
944+
}
945+
}
946+
]
909947
}
910948
}
911949
};

packages/siop-oid4vp/lib/types/SIOP.types.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { JarmClientMetadata } from '@sphereon/jarm'
22
import { DynamicRegistrationClientMetadata, SigningAlgo } from '@sphereon/oid4vc-common'
33
import {
44
AdditionalClaims,
5-
CompactSdJwtVc,
65
Format,
7-
MdocOid4vpMdocVpToken,
86
W3CVerifiableCredential,
97
W3CVerifiablePresentation,
108
WrappedVerifiablePresentation
@@ -37,9 +35,12 @@ import {
3735
CreateAuthorizationResponsePayloadSchema,
3836
CreateAuthorizationResponseSchema,
3937
QRCodeOptsPayloadSchema,
40-
QRCodeOptsSchema, RequestErrorPayloadSchema, RequestErrorSchema,
38+
QRCodeOptsSchema,
39+
RequestErrorPayloadSchema,
40+
RequestErrorSchema,
4141
VerifiedDataOptsSchema
4242
} from '../schemas'
43+
import { Json } from './Json.types'
4344

4445
export const DEFAULT_EXPIRATION_TIME = 10 * 60
4546

@@ -178,6 +179,14 @@ export interface IDTokenPayload extends JWTPayload {
178179
}
179180
}
180181

182+
export type NonEmptyArray<T> = [T, ...T[]]
183+
export type DcqlPresentationEntry = string | Record<string, Json>
184+
export type DcqlVpToken = Record<string, NonEmptyArray<DcqlPresentationEntry>>
185+
export type DcqlVpTokenInput = Record<
186+
string,
187+
DcqlPresentationEntry | DcqlPresentationEntry[] | Record<string, DcqlPresentationEntry>
188+
>
189+
181190
export type EncodedDcqlPresentationVpToken = string
182191

183192
export interface AuthorizationResponsePayload {
@@ -187,12 +196,7 @@ export interface AuthorizationResponsePayload {
187196
expires_in?: number
188197
state?: string
189198
id_token?: string
190-
vp_token?:
191-
| Array<W3CVerifiablePresentation | CompactSdJwtVc | MdocOid4vpMdocVpToken>
192-
| W3CVerifiablePresentation
193-
| CompactSdJwtVc
194-
| MdocOid4vpMdocVpToken
195-
| EncodedDcqlPresentationVpToken
199+
vp_token?: EncodedDcqlPresentationVpToken
196200
is_first_party?: boolean
197201
// eslint-disable-next-line @typescript-eslint/no-explicit-any
198202
[x: string]: any

0 commit comments

Comments
 (0)