diff --git a/packages/siop-oid4vp/lib/__tests__/PresentationExchange.spec.ts b/packages/siop-oid4vp/lib/__tests__/PresentationExchange.spec.ts index 8d69fd5b..5f907d40 100644 --- a/packages/siop-oid4vp/lib/__tests__/PresentationExchange.spec.ts +++ b/packages/siop-oid4vp/lib/__tests__/PresentationExchange.spec.ts @@ -337,9 +337,7 @@ describe('presentation exchange manager tests', () => { const payload = await getPayloadVID1Val() // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(payload.claims?.vp_token as any).presentation_definition_uri = EXAMPLE_PD_URL - await expect(PresentationExchange.findValidPresentationDefinitions(payload)).rejects.toThrow( - SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_BY_REF_AND_VALUE_NON_EXCLUSIVE, - ) + await expect(PresentationExchange.findValidPresentationDefinitions(payload)).rejects.toThrow(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_NON_EXCLUSIVE) }) it('validatePresentationAgainstDefinition: should pass if provided VP match the PD', async function () { diff --git a/packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts b/packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts index df4d1288..45a1032f 100644 --- a/packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts +++ b/packages/siop-oid4vp/lib/authorization-response/AuthorizationResponse.ts @@ -160,7 +160,16 @@ export class AuthorizationResponse { } const verifiedIdToken = await this.idToken?.verify(verifyOpts) - const oid4vp = await verifyPresentations(this, verifyOpts) + if (this.payload.vp_token && !verifyOpts.presentationDefinitions && !verifyOpts.dcqlQuery) { + throw Promise.reject(Error('vp_token is present, but no presentation definitions or dcql query provided')) + } + + const emptyPresentationDefinitions = Array.isArray(verifyOpts.presentationDefinitions) && verifyOpts.presentationDefinitions.length === 0 + if (!this.payload.vp_token && ((verifyOpts.presentationDefinitions && !emptyPresentationDefinitions) || verifyOpts.dcqlQuery)) { + throw Promise.reject(Error('Presentation definitions or dcql query provided, but no vp_token present')) + } + + const oid4vp = this.payload.vp_token ? await verifyPresentations(this, verifyOpts) : undefined // Gather all nonces const allNonces = new Set() @@ -227,7 +236,7 @@ export class AuthorizationResponse { presentations = extractPresentationsFromVpToken(this._payload.vp_token, opts) } - if (presentations && Array.isArray(presentations) && presentations.length === 0) { + if (!presentations || (Array.isArray(presentations) && presentations.length === 0)) { return Promise.reject(Error('missing presentation(s)')) } const presentationsArray = Array.isArray(presentations) ? presentations : [presentations] diff --git a/packages/siop-oid4vp/lib/authorization-response/Dcql.ts b/packages/siop-oid4vp/lib/authorization-response/Dcql.ts index 16cf9672..2edac2ae 100644 --- a/packages/siop-oid4vp/lib/authorization-response/Dcql.ts +++ b/packages/siop-oid4vp/lib/authorization-response/Dcql.ts @@ -12,7 +12,6 @@ import { AuthorizationRequestPayload, SIOPErrors } from '../types' */ export const findValidDcqlQuery = async (authorizationRequestPayload: AuthorizationRequestPayload): Promise => { const dcqlQuery: string[] = extractDataFromPath(authorizationRequestPayload, '$.dcql_query').map((d) => d.value) - const dcqlQueryList: string[] = extractDataFromPath(authorizationRequestPayload, '$.dcql_query[*]').map((d) => d.value) const definitions = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition') const definitionsFromList = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition[*]') const definitionRefs = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition_uri') @@ -20,21 +19,17 @@ export const findValidDcqlQuery = async (authorizationRequestPayload: Authorizat const hasPD = (definitions && definitions.length > 0) || (definitionsFromList && definitionsFromList.length > 0) const hasPdRef = (definitionRefs && definitionRefs.length > 0) || (definitionRefsFromList && definitionRefsFromList.length > 0) - const hasDcql = (dcqlQuery && dcqlQuery.length > 0) || (dcqlQueryList && dcqlQueryList.length > 0) + const hasDcql = dcqlQuery && dcqlQuery.length > 0 if ([hasPD, hasPdRef, hasDcql].filter(Boolean).length > 1) { throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_NON_EXCLUSIVE) } - if (dcqlQuery.length > 1 || dcqlQueryList.length > 1) { + if (dcqlQuery.length === 0) return undefined + + if (dcqlQuery.length > 1) { throw new Error('Found multiple dcql_query in vp_token. Only one is allowed') } - const encoded = dcqlQuery.length ? dcqlQuery[0] : dcqlQueryList[0] - if (!encoded) return undefined - - const parsedDcqlQuery = DcqlQuery.parse(JSON.parse(encoded)) - DcqlQuery.validate(parsedDcqlQuery) - - return parsedDcqlQuery + return DcqlQuery.parse(JSON.parse(dcqlQuery[0])) } diff --git a/packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts b/packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts index 4ff909cf..228ec787 100644 --- a/packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts +++ b/packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts @@ -72,15 +72,6 @@ export const verifyPresentations = async ( authorizationResponse: AuthorizationResponse, verifyOpts: VerifyAuthorizationResponseOpts, ): Promise => { - if (!authorizationResponse.payload.vp_token) return null - if ( - authorizationResponse.payload.vp_token && - Array.isArray(authorizationResponse.payload.vp_token) && - authorizationResponse.payload.vp_token.length === 0 - ) { - return Promise.reject(Error('the payload is missing a vp_token')) - } - let idPayload: IDTokenPayload | undefined if (authorizationResponse.idToken) { idPayload = await authorizationResponse.idToken.payload() @@ -98,8 +89,6 @@ export const verifyPresentations = async ( let dcqlQuery = verifyOpts.dcqlQuery ?? authorizationResponse?.authorizationRequest?.payload.dcql_query if (dcqlQuery) { dcqlQuery = DcqlQuery.parse(dcqlQuery) - DcqlQuery.validate(dcqlQuery) - wrappedPresentations = extractPresentationsFromDcqlVpToken(authorizationResponse.payload.vp_token as string, { hasher: verifyOpts.hasher }) const verifiedPresentations = await Promise.all( @@ -170,12 +159,25 @@ export const verifyPresentations = async ( } } +export const extractPresentationRecordFromDcqlVpToken = ( + vpToken: DcqlVpToken.Input | string, + opts?: { hasher?: Hasher }, +): Record => { + const presentationRecord = Object.fromEntries( + Object.entries(DcqlVpToken.parse(vpToken)).map(([credentialQueryId, vp]) => [ + credentialQueryId, + CredentialMapper.toWrappedVerifiablePresentation(vp as W3CVerifiablePresentation | CompactSdJwtVc | string, { hasher: opts.hasher }), + ]), + ) + + return presentationRecord +} + export const extractPresentationsFromDcqlVpToken = ( vpToken: DcqlVpToken.Input | string, opts?: { hasher?: Hasher }, ): WrappedVerifiablePresentation[] => { - const presentations = Object.values(DcqlVpToken.parse(vpToken)) as Array - return presentations.map((vp) => CredentialMapper.toWrappedVerifiablePresentation(vp, { hasher: opts.hasher })) + return Object.values(extractPresentationRecordFromDcqlVpToken(vpToken, opts)) } export const extractPresentationsFromVpToken = ( diff --git a/packages/siop-oid4vp/lib/authorization-response/Payload.ts b/packages/siop-oid4vp/lib/authorization-response/Payload.ts index 286c3705..aff5572f 100644 --- a/packages/siop-oid4vp/lib/authorization-response/Payload.ts +++ b/packages/siop-oid4vp/lib/authorization-response/Payload.ts @@ -1,3 +1,5 @@ +import { DcqlVpToken } from 'dcql' + import { AuthorizationRequest } from '../authorization-request' import { IDToken } from '../id-token' import { RequestObject } from '../request-object' @@ -29,7 +31,7 @@ export const createResponsePayload = async ( // vp tokens if (responseOpts.dcqlQuery) { - responsePayload.vp_token = JSON.stringify(responseOpts.dcqlQuery.credentialQueryIdToPresentation) + responsePayload.vp_token = DcqlVpToken.encode(responseOpts.dcqlQuery.encodedPresentationRecord) } else { await putPresentationSubmissionInLocation(authorizationRequest, responsePayload, responseOpts, idTokenPayload) } diff --git a/packages/siop-oid4vp/lib/authorization-response/types.ts b/packages/siop-oid4vp/lib/authorization-response/types.ts index 944e0296..aa684320 100644 --- a/packages/siop-oid4vp/lib/authorization-response/types.ts +++ b/packages/siop-oid4vp/lib/authorization-response/types.ts @@ -62,11 +62,7 @@ export interface PresentationExchangeResponseOpts { } export interface DcqlQueryResponseOpts { - credentialQueryIdToPresentation: Record | string> -} - -export interface PresentationExchangeRequestOpts { - presentationVerificationCallback?: PresentationVerificationCallback + encodedPresentationRecord: Record | string> } export interface PresentationDefinitionPayloadOpts { diff --git a/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts b/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts index f4665713..6f3b54f8 100644 --- a/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts +++ b/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts @@ -2342,7 +2342,7 @@ export const AuthorizationResponseOptsSchemaObj = { "DcqlQueryResponseOpts": { "type": "object", "properties": { - "credentialQueryIdToPresentation": { + "encodedPresentationRecord": { "type": "object", "additionalProperties": { "anyOf": [ @@ -2358,7 +2358,7 @@ export const AuthorizationResponseOptsSchemaObj = { } }, "required": [ - "credentialQueryIdToPresentation" + "encodedPresentationRecord" ], "additionalProperties": false } diff --git a/packages/siop-oid4vp/lib/types/SIOP.types.ts b/packages/siop-oid4vp/lib/types/SIOP.types.ts index 3933216e..abb6bcab 100644 --- a/packages/siop-oid4vp/lib/types/SIOP.types.ts +++ b/packages/siop-oid4vp/lib/types/SIOP.types.ts @@ -167,7 +167,7 @@ export interface IDTokenPayload extends JWTPayload { } } -export type DcqlQueryVpToken = string +export type EcodedDcqlQueryVpToken = string export interface AuthorizationResponsePayload { access_token?: string @@ -181,7 +181,7 @@ export interface AuthorizationResponsePayload { | W3CVerifiablePresentation | CompactSdJwtVc | MdocOid4vpMdocVpToken - | DcqlQueryVpToken + | EcodedDcqlQueryVpToken presentation_submission?: PresentationSubmission verifiedData?: IPresentation | AdditionalClaims // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/siop-oid4vp/package.json b/packages/siop-oid4vp/package.json index ad8ea863..e0c2b482 100644 --- a/packages/siop-oid4vp/package.json +++ b/packages/siop-oid4vp/package.json @@ -18,7 +18,7 @@ "@sphereon/jarm": "workspace:*", "@sphereon/oid4vc-common": "workspace:*", "@sphereon/pex": "5.0.0-unstable.24", - "dcql": "link:../../../dcql/dcql", + "dcql": "^0.2.4", "@sphereon/pex-models": "^2.3.1", "@sphereon/ssi-types": "0.30.2-next.129", "cross-fetch": "^4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b16db23d..58027b5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -461,8 +461,8 @@ importers: specifier: ^4.0.0 version: 4.0.0(encoding@0.1.13) dcql: - specifier: link:../../../dcql/dcql - version: link:../../../dcql/dcql + specifier: ^0.2.4 + version: 0.2.4(typescript@5.4.5) debug: specifier: ^4.3.5 version: 4.3.7 @@ -4079,6 +4079,9 @@ packages: dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + dcql@0.2.4: + resolution: {integrity: sha512-miMonohTwOD3juU7yZtrjPO0WMYkpgtJVRJDA47L4z2qEttJGAylZ7chwSt3S6IfZK8GLh9cBh1YhretC4tQCw==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -8506,6 +8509,14 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + valibot@0.37.0: + resolution: {integrity: sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + valibot@0.42.1: resolution: {integrity: sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==} peerDependencies: @@ -14455,6 +14466,12 @@ snapshots: dayjs@1.11.13: {} + dcql@0.2.4(typescript@5.4.5): + dependencies: + valibot: 0.37.0(typescript@5.4.5) + transitivePeerDependencies: + - typescript + debug@2.6.9: dependencies: ms: 2.0.0 @@ -20076,6 +20093,10 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + valibot@0.37.0(typescript@5.4.5): + optionalDependencies: + typescript: 5.4.5 + valibot@0.42.1(typescript@5.5.3): optionalDependencies: typescript: 5.5.3