diff --git a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts index 5dfd6b059..0adcb8b5f 100644 --- a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts @@ -4,10 +4,11 @@ import type { AgentContext } from '@credo-ts/core' import type { AuthorizationResponsePayload, DecryptCompact } from '@sphereon/did-auth-siop' import type { Response, Router } from 'express' +import { Oauth2ErrorCodes, Oauth2ServerErrorResponseError } from '@animo-id/oauth2' import { CredoError, Hasher, JsonEncoder, Key, TypedArrayEncoder } from '@credo-ts/core' import { AuthorizationRequest, RP } from '@sphereon/did-auth-siop' -import { getRequestContext, sendErrorResponse, sendJsonResponse } from '../../shared/router' +import { getRequestContext, sendErrorResponse, sendJsonResponse, sendOauth2ErrorResponse } from '../../shared/router' import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' export interface OpenId4VcSiopAuthorizationEndpointConfig { @@ -68,6 +69,8 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc router.post(config.endpointPath, async (request: OpenId4VcVerificationRequest, response: Response, next) => { const { agentContext, verifier } = getRequestContext(request) + let jarmResponseType: string | undefined + try { const openId4VcVerifierService = agentContext.dependencyManager.resolve(OpenId4VcSiopVerifierService) @@ -95,6 +98,8 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc hasher: Hasher.hash, }) + jarmResponseType = res.type + const [header] = request.body.response.split('.') jarmHeader = JsonEncoder.fromBase64(header) // FIXME: verify the apv matches the nonce of the authorization reuqest @@ -123,6 +128,29 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc throw new CredoError('Missing verification session, cannot verify authorization response.') } + const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(verificationSession.authorizationRequestJwt) + const response_mode = await authorizationRequest.getMergedProperty('response_mode') + if (response_mode?.includes('jwt') && !jarmResponseType) { + throw new Oauth2ServerErrorResponseError({ + error: Oauth2ErrorCodes.InvalidRequest, + error_description: `JARM response is required for JWT response mode '${response_mode}'.`, + }) + } + + if (!response_mode?.includes('jwt') && jarmResponseType) { + throw new Oauth2ServerErrorResponseError({ + error: Oauth2ErrorCodes.InvalidRequest, + error_description: `Recieved JARM response which is incompatible with response mode '${response_mode}'.`, + }) + } + + if (jarmResponseType && jarmResponseType !== 'encrypted') { + throw new Oauth2ServerErrorResponseError({ + error: Oauth2ErrorCodes.InvalidRequest, + error_description: `Only encrypted JARM responses are supported, received '${jarmResponseType}'.`, + }) + } + await openId4VcVerifierService.verifyAuthorizationResponse(agentContext, { authorizationResponse: authorizationResponsePayload, verificationSession, @@ -133,6 +161,10 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc presentation_during_issuance_session: verificationSession.presentationDuringIssuanceSession, }) } catch (error) { + if (error instanceof Oauth2ServerErrorResponseError) { + return sendOauth2ErrorResponse(response, next, agentContext.config.logger, error) + } + return sendErrorResponse(response, next, agentContext.config.logger, 500, 'invalid_request', error) } }) diff --git a/packages/openid4vc/src/shared/router/context.ts b/packages/openid4vc/src/shared/router/context.ts index 81890bb63..cc383d693 100644 --- a/packages/openid4vc/src/shared/router/context.ts +++ b/packages/openid4vc/src/shared/router/context.ts @@ -83,7 +83,7 @@ export function sendErrorResponse( error: unknown, additionalPayload?: Record ) { - const body = { error: message, ...additionalPayload } + const body = { error: message, ...(error instanceof Error && { cause: error.message }), ...additionalPayload } logger.warn(`[OID4VC] Sending error response: ${JSON.stringify(body)}`, { error, }) diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index b4bb7753c..edd125832 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -37,6 +37,7 @@ import { JwsService, JwtPayload, } from '@credo-ts/core' +import { ResponseMode } from '@sphereon/did-auth-siop' import express, { type Express } from 'express' import { AskarModule } from '../../askar/src' @@ -1857,6 +1858,112 @@ describe('OpenId4Vc', () => { await holderTenant1.endSession() }) + it('e2e flow with verifier endpoints verifying a mdoc fails without direct_post.jwt', async () => { + const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() + + const selfSignedCertificate = await X509Service.createSelfSignedCertificate(issuer.agent.context, { + key: await issuer.agent.context.wallet.createKey({ keyType: KeyType.P256 }), + extensions: [], + name: 'C=DE', + }) + + await verifier.agent.x509.setTrustedCertificates([selfSignedCertificate.toString('pem')]) + + const holderKey = await holder.agent.context.wallet.createKey({ keyType: KeyType.P256 }) + const signedMdoc = await issuer.agent.mdoc.sign({ + docType: 'org.eu.university', + holderKey, + issuerCertificate: selfSignedCertificate.toString('pem'), + namespaces: { + 'eu.europa.ec.eudi.pid.1': { + university: 'innsbruck', + degree: 'bachelor', + name: 'John Doe', + not: 'disclosed', + }, + }, + }) + + const certificate = await verifier.agent.x509.createSelfSignedCertificate({ + key: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + extensions: [[{ type: 'dns', value: 'localhost:1234' }]], + }) + + const rawCertificate = certificate.toString('base64') + await holder.agent.mdoc.store(signedMdoc) + + await holder.agent.x509.addTrustedCertificate(rawCertificate) + await verifier.agent.x509.addTrustedCertificate(rawCertificate) + + const presentationDefinition = { + id: 'mDL-sample-req', + input_descriptors: [ + { + id: 'org.eu.university', + format: { + mso_mdoc: { + alg: ['ES256', 'ES384', 'ES512', 'EdDSA', 'ESB256', 'ESB320', 'ESB384', 'ESB512'], + }, + }, + constraints: { + fields: [ + { + path: ["$['eu.europa.ec.eudi.pid.1']['name']"], + intent_to_retain: false, + }, + { + path: ["$['eu.europa.ec.eudi.pid.1']['degree']"], + intent_to_retain: false, + }, + ], + limit_disclosure: 'required', + }, + }, + ], + } satisfies DifPresentationExchangeDefinitionV2 + + const { authorizationRequest } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + responseMode: 'direct_post.jwt', + verifierId: openIdVerifier.verifierId, + requestSigner: { + method: 'x5c', + x5c: [rawCertificate], + issuer: 'https://example.com/hakuna/matadata', + }, + presentationExchange: { definition: presentationDefinition }, + }) + + const resolvedAuthorizationRequest = await holder.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequest + ) + + if (!resolvedAuthorizationRequest.presentationExchange) { + throw new Error('Presentation exchange not defined') + } + + const selectedCredentials = holder.agent.modules.openId4VcHolder.selectCredentialsForRequest( + resolvedAuthorizationRequest.presentationExchange.credentialsForRequest + ) + + const requestPayload = + await resolvedAuthorizationRequest.authorizationRequest.authorizationRequest.requestObject?.getPayload() + if (!requestPayload) { + throw new Error('No payload') + } + + // setting this to direct_post to simulate the result of sending a non encrypted response to an authorization request that requires enryption + requestPayload.response_mode = ResponseMode.DIRECT_POST + + await expect( + holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) + ).rejects.toThrow(/JARM response is required/) + }) + it('e2e flow with verifier endpoints verifying a mdoc and sd-jwt (jarm)', async () => { const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier()