diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts index a4867c14ca..8a661b4825 100644 --- a/demo-openid/src/Holder.ts +++ b/demo-openid/src/Holder.ts @@ -1,11 +1,14 @@ import type { - OfferedCredentialWithMetadata, - ResolvedPresentationRequest, - ResolvedCredentialOffer, + OpenId4VciResolvedCredentialOffer, + OpenId4VcSiopResolvedAuthorizationRequest, } from '@aries-framework/openid4vc' import { AskarModule } from '@aries-framework/askar' -import { W3cJwtVerifiableCredential, W3cJsonLdVerifiableCredential } from '@aries-framework/core' +import { + W3cJwtVerifiableCredential, + W3cJsonLdVerifiableCredential, + DifPresentationExchangeService, +} from '@aries-framework/core' import { OpenId4VcHolderModule } from '@aries-framework/openid4vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' @@ -36,8 +39,8 @@ export class Holder extends BaseAgent> } public async requestAndStoreCredentials( - resolvedCredentialOffer: ResolvedCredentialOffer, - credentialsToRequest: OfferedCredentialWithMetadata[] + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, + credentialsToRequest: string[] ) { const credentials = await this.agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer, @@ -65,25 +68,28 @@ export class Holder extends BaseAgent> } public async resolveProofRequest(proofRequest: string) { - const resolvedProofRequest = await this.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) - - if (resolvedProofRequest.proofType === 'authentication') - throw new Error('We only support presentation requests for now.') + const resolvedProofRequest = await this.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest(proofRequest) return resolvedProofRequest } - public async acceptPresentationRequest( - resolvedPresentationRequest: ResolvedPresentationRequest, - submissionEntryIndexes: number[] - ) { - const { presentationRequest, presentationSubmission } = resolvedPresentationRequest - const submissionResult = await this.agent.modules.openId4VcHolder.acceptPresentationRequest(presentationRequest, { - submission: presentationSubmission, - submissionEntryIndexes, + public async acceptPresentationRequest(resolvedPresentationRequest: OpenId4VcSiopResolvedAuthorizationRequest) { + const presentationExchangeService = this.agent.dependencyManager.resolve(DifPresentationExchangeService) + + if (!resolvedPresentationRequest.presentationExchange) { + throw new Error('Missing presentation exchange on resolved authorization request') + } + + const submissionResult = await this.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedPresentationRequest.authorizationRequest, + presentationExchange: { + credentials: presentationExchangeService.selectCredentialsForRequest( + resolvedPresentationRequest.presentationExchange.credentialsForRequest + ), + }, }) - return submissionResult.status + return submissionResult.serverResponse } public async exit() { diff --git a/demo-openid/src/HolderInquirer.ts b/demo-openid/src/HolderInquirer.ts index a28df6a6d0..a4fe1cf1e6 100644 --- a/demo-openid/src/HolderInquirer.ts +++ b/demo-openid/src/HolderInquirer.ts @@ -1,6 +1,11 @@ -import type { ResolvedCredentialOffer, ResolvedPresentationRequest } from '@aries-framework/openid4vc' - -import { clear } from 'console' +import type { SdJwtVcRecord, W3cCredentialRecord } from '@aries-framework/core/src' +import type { + OpenId4VcSiopResolvedAuthorizationRequest, + OpenId4VciResolvedCredentialOffer, +} from '@aries-framework/openid4vc' + +import { DifPresentationExchangeService } from '@aries-framework/core/src' +import console, { clear } from 'console' import { textSync } from 'figlet' import { prompt } from 'inquirer' @@ -26,8 +31,8 @@ enum PromptOptions { export class HolderInquirer extends BaseInquirer { public holder: Holder - public resolvedCredentialOffer?: ResolvedCredentialOffer - public resolvedPresentationRequest?: ResolvedPresentationRequest + public resolvedCredentialOffer?: OpenId4VciResolvedCredentialOffer + public resolvedPresentationRequest?: OpenId4VcSiopResolvedAuthorizationRequest public constructor(holder: Holder) { super() @@ -89,9 +94,7 @@ export class HolderInquirer extends BaseInquirer { this.resolvedCredentialOffer = resolvedCredentialOffer console.log(greenText(`Received credential offer for the following credentials.`)) - console.log( - greenText(resolvedCredentialOffer.offeredCredentials.map((credential) => credential.types.join(', ')).join('\n')) - ) + console.log(greenText(resolvedCredentialOffer.offeredCredentials.map((credential) => credential.id).join('\n'))) } public async requestCredential() { @@ -99,49 +102,49 @@ export class HolderInquirer extends BaseInquirer { throw new Error('No credential offer resolved yet.') } - const credentialsThatCanBeRequested = this.resolvedCredentialOffer.offeredCredentials.map((credential) => - credential.types.join(', ') + const credentialsThatCanBeRequested = this.resolvedCredentialOffer.offeredCredentials.map( + (credential) => credential.id ) const choice = await prompt([this.inquireOptions(credentialsThatCanBeRequested)]) const credentialToRequest = this.resolvedCredentialOffer.offeredCredentials.find( - (credential) => credential.types.join(', ') == choice.options + (credential) => credential.id === choice.options ) if (!credentialToRequest) throw new Error('Credential to request not found.') - console.log(greenText(`Requesting the following credential '${credentialToRequest.types.join(', ')}'`)) + console.log(greenText(`Requesting the following credential '${credentialToRequest.id}'`)) const credentials = await this.holder.requestAndStoreCredentials( this.resolvedCredentialOffer, - this.resolvedCredentialOffer.offeredCredentials - ) - - const credentialTypes = await Promise.all( - credentials.map((credential) => - credential.type === 'W3cCredentialRecord' - ? `${credential.credential.type.join(', ')}, CredentialType: W3cVerifiableCredential` - : this.holder.agent.sdJwtVc - .fromCompact(credential.compactSdJwtVc) - .then((a) => `${a.prettyClaims.vct}, CredentialType: SdJwtVc`) - ) + this.resolvedCredentialOffer.offeredCredentials.map((o) => o.id) ) console.log(greenText(`Received and stored the following credentials.`)) - console.log(greenText(credentialTypes.join('\n'))) + console.log('') + credentials.forEach(this.printCredential) } public async resolveProofRequest() { const proofRequestUri = await prompt([this.inquireInput('Enter proof request: ')]) this.resolvedPresentationRequest = await this.holder.resolveProofRequest(proofRequestUri.input) - const presentationDefinition = - this.resolvedPresentationRequest.presentationRequest.presentationDefinitions[0].definition - - console.log(greenText(`Presentation Purpose: '${presentationDefinition.purpose}'`)) - - if (this.resolvedPresentationRequest.presentationSubmission.areRequirementsSatisfied) { - console.log(greenText(`All requirements for creating the presentation are satisfied.`)) + const presentationDefinition = this.resolvedPresentationRequest?.presentationExchange?.definition + console.log(greenText(`Presentation Purpose: '${presentationDefinition?.purpose}'`)) + + if (this.resolvedPresentationRequest?.presentationExchange?.credentialsForRequest.areRequirementsSatisfied) { + const selectedCredentials = Object.values( + this.holder.agent.dependencyManager + .resolve(DifPresentationExchangeService) + .selectCredentialsForRequest(this.resolvedPresentationRequest.presentationExchange.credentialsForRequest) + ).flatMap((e) => e) + console.log( + greenText( + `All requirements for creating the presentation are satisfied. The following credentials will be shared`, + true + ) + ) + selectedCredentials.forEach(this.printCredential) } else { console.log(redText(`No credentials available that satisfy the proof request.`)) } @@ -150,22 +153,14 @@ export class HolderInquirer extends BaseInquirer { public async acceptPresentationRequest() { if (!this.resolvedPresentationRequest) throw new Error('No presentation request resolved yet.') - // we know that only one credential is in the wallet and it satisfies the proof request. - // The submission entry index for this credential is 0. - const credential = - this.resolvedPresentationRequest.presentationSubmission.requirements[0].submissionEntry[0] - .verifiableCredentials[0] - const submissionEntryIndexes = [0] - - console.log(greenText(`Accepting the presentation request, with the following credential.`)) - console.log(greenText(credential.credential.type.join(', '))) + console.log(greenText(`Accepting the presentation request.`)) - const status = await this.holder.acceptPresentationRequest(this.resolvedPresentationRequest, submissionEntryIndexes) + const serverResponse = await this.holder.acceptPresentationRequest(this.resolvedPresentationRequest) - if (status >= 200 && status < 300) { - console.log(`received success status code '${status}'`) + if (serverResponse.status >= 200 && serverResponse.status < 300) { + console.log(`received success status code '${serverResponse.status}'`) } else { - console.log(`received error status code '${status}'`) + console.log(`received error status code '${serverResponse.status}'`) } } @@ -188,6 +183,19 @@ export class HolderInquirer extends BaseInquirer { await runHolder() } } + + private printCredential = (credential: W3cCredentialRecord | SdJwtVcRecord) => { + if (credential.type === 'W3cCredentialRecord') { + console.log(greenText(`W3cCredentialRecord with claim format ${credential.credential.claimFormat}`, true)) + console.log(JSON.stringify(credential.credential.jsonCredential, null, 2)) + console.log('') + } else { + console.log(greenText(`SdJwtVcRecord`, true)) + const prettyClaims = this.holder.agent.sdJwtVc.fromCompact(credential.compactSdJwtVc).prettyClaims + console.log(JSON.stringify(prettyClaims, null, 2)) + console.log('') + } + } } void runHolder() diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index 56daaeb874..0eb75be86f 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -1,14 +1,15 @@ import type { DidKey } from '@aries-framework/core' import type { - CredentialHolderBinding, - CredentialHolderDidBinding, - CredentialRequestToCredentialMapper, + OpenId4VcCredentialHolderBinding, + OpenId4VcCredentialHolderDidBinding, + OpenId4VciCredentialRequestToCredentialMapper, OpenId4VciCredentialSupportedWithId, OpenId4VcIssuerRecord, } from '@aries-framework/openid4vc' import { AskarModule } from '@aries-framework/askar' import { + ClaimFormat, parseDid, AriesFrameworkError, W3cCredential, @@ -51,7 +52,7 @@ function getCredentialRequestToCredentialMapper({ issuerDidKey, }: { issuerDidKey: DidKey -}): CredentialRequestToCredentialMapper { +}): OpenId4VciCredentialRequestToCredentialMapper { return async ({ holderBinding, credentialsSupported }) => { const credentialSupported = credentialsSupported[0] @@ -59,6 +60,7 @@ function getCredentialRequestToCredentialMapper({ assertDidBasedHolderBinding(holderBinding) return { + format: ClaimFormat.JwtVc, credential: new W3cCredential({ type: universityDegreeCredential.types, issuer: new W3cIssuer({ @@ -77,6 +79,7 @@ function getCredentialRequestToCredentialMapper({ assertDidBasedHolderBinding(holderBinding) return { + format: ClaimFormat.JwtVc, credential: new W3cCredential({ type: openBadgeCredential.types, issuer: new W3cIssuer({ @@ -93,6 +96,7 @@ function getCredentialRequestToCredentialMapper({ if (credentialSupported.id === universityDegreeCredentialSdJwt.id) { return { + format: ClaimFormat.SdJwtVc, payload: { vct: universityDegreeCredentialSdJwt.vct, university: 'innsbruck', degree: 'bachelor' }, holder: holderBinding, issuer: { @@ -131,7 +135,7 @@ export class Issuer extends BaseAgent<{ }, }, }), - } as const, + }, }) this.app.use('/oid4vci', openId4VciRouter) @@ -148,14 +152,13 @@ export class Issuer extends BaseAgent<{ } public async createCredentialOffer(offeredCredentials: string[]) { - const { credentialOfferUri } = await this.agent.modules.openId4VcIssuer.createCredentialOffer({ + const { credentialOffer } = await this.agent.modules.openId4VcIssuer.createCredentialOffer({ issuerId: this.issuerRecord.issuerId, offeredCredentials, - scheme: 'openid-credential-offer', preAuthorizedCodeFlowConfig: { userPinRequired: false }, }) - return credentialOfferUri + return credentialOffer } public async exit() { @@ -170,7 +173,9 @@ export class Issuer extends BaseAgent<{ } function assertDidBasedHolderBinding( - holderBinding: CredentialHolderBinding -): asserts holderBinding is CredentialHolderDidBinding { - throw new AriesFrameworkError('Only did based holder bindings supported for this credential type') + holderBinding: OpenId4VcCredentialHolderBinding +): asserts holderBinding is OpenId4VcCredentialHolderDidBinding { + if (holderBinding.method !== 'did') { + throw new AriesFrameworkError('Only did based holder bindings supported for this credential type') + } } diff --git a/demo-openid/src/Verifier.ts b/demo-openid/src/Verifier.ts index 85b758ddcf..8ab4002d74 100644 --- a/demo-openid/src/Verifier.ts +++ b/demo-openid/src/Verifier.ts @@ -1,13 +1,8 @@ -import type { - ProofResponseHandler, - CreateProofRequestOptions, - VerifierEndpointConfig, - PresentationDefinitionV2, -} from '@aries-framework/openid4vc' -import type e from 'express' +import type { DifPresentationExchangeDefinitionV2 } from '@aries-framework/core/src' +import type { OpenId4VcVerifierRecord } from '@aries-framework/openid4vc' import { AskarModule } from '@aries-framework/askar' -import { SigningAlgo, OpenId4VcVerifierModule, staticOpOpenIdConfig } from '@aries-framework/openid4vc' +import { OpenId4VcVerifierModule } from '@aries-framework/openid4vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { Router } from 'express' @@ -19,12 +14,18 @@ const universityDegreePresentationDefinition = { purpose: 'Present your UniversityDegreeCredential to verify your education level.', input_descriptors: [ { - id: 'UniversityDegreeCredential', - // changed jwt_vc_json to jwt_vc - format: { jwt_vc: { alg: ['EdDSA'] } }, - // changed $.type to $.vc.type + id: 'UniversityDegreeCredentialDescriptor', constraints: { - fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'UniversityDegree' } }], + fields: [ + { + // Works for JSON-LD, SD-JWT and JWT + path: ['$.vc.type.*', '$.vct', '$.type'], + filter: { + type: 'string', + pattern: 'UniversityDegree', + }, + }, + ], }, }, ], @@ -35,12 +36,18 @@ const openBadgeCredentialPresentationDefinition = { purpose: 'Provide proof of employment to confirm your employment status.', input_descriptors: [ { - id: 'OpenBadgeCredential', - // changed jwt_vc_json to jwt_vc - format: { jwt_vc: { alg: ['EdDSA'] } }, - // changed $.type to $.vc.type + id: 'OpenBadgeCredentialDescriptor', constraints: { - fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'OpenBadgeCredential' } }], + fields: [ + { + // Works for JSON-LD, SD-JWT and JWT + path: ['$.vc.type.*', '$.vct', '$.type'], + filter: { + type: 'string', + pattern: 'OpenBadgeCredential', + }, + }, + ], }, }, ], @@ -51,64 +58,48 @@ export const presentationDefinitions = [ openBadgeCredentialPresentationDefinition, ] -function getOpenIdVerifierModules() { - return { - askar: new AskarModule({ ariesAskar }), - openId4VcVerifier: new OpenId4VcVerifierModule({ - verifierMetadata: { - verifierBaseUrl: 'http://localhost:4000', - verificationEndpointPath: '/verify', - }, - }), - } as const -} +export class Verifier extends BaseAgent<{ askar: AskarModule; openId4VcVerifier: OpenId4VcVerifierModule }> { + public verifierRecord!: OpenId4VcVerifierRecord -export class Verifier extends BaseAgent> { public constructor(port: number, name: string) { - super({ port, name, modules: getOpenIdVerifierModules() }) + const openId4VcSiopRouter = Router() + + super({ + port, + name, + modules: { + askar: new AskarModule({ ariesAskar }), + openId4VcVerifier: new OpenId4VcVerifierModule({ + baseUrl: 'http://localhost:4000/siop', + }), + }, + }) + + this.app.use('/siop', openId4VcSiopRouter) } public static async build(): Promise { const verifier = new Verifier(4000, 'OpenId4VcVerifier ' + Math.random().toString()) await verifier.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598g') + verifier.verifierRecord = await verifier.agent.modules.openId4VcVerifier.createVerifier() return verifier } - public async configureVerifierRouter(): Promise { - const endpointConfig: VerifierEndpointConfig = { - basePath: '/', - verificationEndpointConfig: { - enabled: true, - proofResponseHandler: Verifier.proofResponseHandler, + // TODO: add method to show the received presentation submission + public async createProofRequest(presentationDefinition: DifPresentationExchangeDefinitionV2) { + const { authorizationRequestUri } = await this.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'did', + didUrl: this.verificationMethod.id, }, - } - - const router = await this.agent.modules.openId4VcVerifier.configureRouter(Router(), endpointConfig) - this.app.use('/', router) - return router - } - - public async createProofRequest(presentationDefinition: PresentationDefinitionV2) { - const createProofRequestOptions: CreateProofRequestOptions = { - verificationMethod: this.verificationMethod, - presentationDefinition, - holderMetadata: { - ...staticOpOpenIdConfig, - idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], - requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], - vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, + verifierId: this.verifierRecord.verifierId, + presentationExchange: { + definition: presentationDefinition, }, - } - - const { proofRequest } = await this.agent.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) - - return proofRequest - } + }) - private static proofResponseHandler: ProofResponseHandler = async (payload) => { - console.log('Received a valid proof response', payload) - return { status: 200 } + return authorizationRequestUri } public async exit() { diff --git a/demo-openid/src/VerifierInquirer.ts b/demo-openid/src/VerifierInquirer.ts index 46de1521cd..8877242fb6 100644 --- a/demo-openid/src/VerifierInquirer.ts +++ b/demo-openid/src/VerifierInquirer.ts @@ -31,7 +31,6 @@ export class VerifierInquirer extends BaseInquirer { public static async build(): Promise { const verifier = await Verifier.build() - await verifier.configureVerifierRouter() return new VerifierInquirer(verifier) } diff --git a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts index 740c639472..c0a281a7ba 100644 --- a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts +++ b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts @@ -1,5 +1,6 @@ import type { LinkedDataProofOptions } from './LinkedDataProof' import type { W3cCredentialOptions } from '../../models/credential/W3cCredential' +import type { W3cJsonCredential } from '../../models/credential/W3cJsonCredential' import { ValidateNested } from 'class-validator' @@ -59,4 +60,8 @@ export class W3cJsonLdVerifiableCredential extends W3cCredential { public get encoded() { return this.toJson() } + + public get jsonCredential(): W3cJsonCredential { + return this.toJson() as W3cJsonCredential + } } diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiableCredential.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiableCredential.ts index c9d3852a35..869f00121e 100644 --- a/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiableCredential.ts +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiableCredential.ts @@ -1,6 +1,8 @@ import type { W3cCredential } from '../models/credential/W3cCredential' +import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' import { Jwt } from '../../../crypto/jose/jwt/Jwt' +import { JsonTransformer } from '../../../utils' import { ClaimFormat } from '../models/ClaimFormat' import { getCredentialFromJwtPayload } from './credentialTransformer' @@ -117,4 +119,8 @@ export class W3cJwtVerifiableCredential { public get encoded() { return this.serializedJwt } + + public get jsonCredential(): W3cJsonCredential { + return JsonTransformer.toJSON(this.credential) as W3cJsonCredential + } } diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index 23d5104e57..fb8ebd25b1 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -90,7 +90,7 @@ export class OpenId4VcSiopHolderService { let presentationExchangeOptions: PresentationExchangeResponseOpts | undefined = undefined // Handle presentation exchange part - if (authorizationRequest.presentationDefinitions) { + if (authorizationRequest.presentationDefinitions && authorizationRequest.presentationDefinitions.length > 0) { if (!presentationExchange) { throw new AriesFrameworkError( 'Authorization request included presentation definition. `presentationExchange` MUST be supplied to accept authorization requests.' @@ -127,7 +127,7 @@ export class OpenId4VcSiopHolderService { } } else if (options.presentationExchange) { throw new AriesFrameworkError( - '`presentationExchange` was supplied, but no presentation definition was found in the presentaiton request.' + '`presentationExchange` was supplied, but no presentation definition was found in the presentation request.' ) } @@ -159,9 +159,21 @@ export class OpenId4VcSiopHolderService { ) const response = await openidProvider.submitAuthorizationResponse(authorizationResponseWithCorrelationId) + let responseDetails: string | Record | undefined = undefined + try { + responseDetails = await response.text() + if (responseDetails.includes('{')) { + responseDetails = JSON.parse(responseDetails) + } + } catch (error) { + // no-op + } + return { - ok: response.status === 200, - status: response.status, + serverResponse: { + status: response.status, + body: responseDetails, + }, submittedResponse: authorizationResponseWithCorrelationId.response.payload, } } diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/OpenId4VcHolderModule.test.ts similarity index 100% rename from packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts rename to packages/openid4vc/src/openid4vc-holder/__tests__/OpenId4VcHolderModule.test.ts diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts index a98fa20082..83c7a2b0cc 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts @@ -101,7 +101,7 @@ describe('OpenId4VcHolder', () => { // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json'), + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json').map((m) => m.id), credentialBindingResolver: () => ({ method: 'did', didUrl: holderVerificationMethod }), }) @@ -144,7 +144,7 @@ describe('OpenId4VcHolder', () => { // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json'), + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json').map((m) => m.id), credentialBindingResolver: () => ({ method: 'did', didUrl: holderVerificationMethod }), }) ) @@ -181,7 +181,7 @@ describe('OpenId4VcHolder', () => { // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'vc+sd-jwt'), + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'vc+sd-jwt').map((m) => m.id), credentialBindingResolver: () => ({ method: 'jwk', jwk: getJwkFromKey(holderKey) }), }) diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts index 6c7b4d4166..8b511cc99a 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts @@ -1,27 +1,14 @@ import type { AgentType } from '../../../tests/utils' -import type { OpenId4VcSiopCreateAuthorizationRequestOptions } from '../../openid4vc-verifier' +import type { OpenId4VcVerifierRecord } from '../../openid4vc-verifier/repository' import type { Express } from 'express' import type { Server } from 'http' -import { AskarModule } from '@aries-framework/askar' -import { W3cJwtVerifiableCredential } from '@aries-framework/core' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import express, { Router } from 'express' -import nock from 'nock' +import express from 'express' import { OpenId4VcHolderModule } from '..' +import { AskarModule } from '../../../../askar/src' +import { askarModuleConfig } from '../../../../askar/tests/helpers' import { createAgentFromModules } from '../../../tests/utils' -import { - openBadgeCredentialPresentationDefinitionLdpVc, - combinePresentationDefinitions, - getOpenBadgeCredentialLdpVc, - openBadgePresentationDefinition, - staticOpOpenIdConfigEdDSA, - universityDegreePresentationDefinition, - waitForMockFunction, - waltPortalOpenBadgeJwt, - waltUniversityDegreeJwt, -} from '../../../tests/utilsVp' import { OpenId4VcVerifierModule } from '../../openid4vc-verifier' const port = 3121 @@ -30,45 +17,37 @@ const verifierBaseUrl = `http://localhost:${port}` const holderModules = { openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule({ ariesAskar }), + askar: new AskarModule(askarModuleConfig), } const verifierModules = { openId4VcVerifier: new OpenId4VcVerifierModule({ - verifierMetadata: { - verifierBaseUrl: verifierBaseUrl, - verificationEndpointPath, + baseUrl: verifierBaseUrl, + endpoints: { + authorization: { + endpointPath: verificationEndpointPath, + }, }, }), - - askar: new AskarModule({ ariesAskar }), + askar: new AskarModule(askarModuleConfig), } describe('OpenId4VcHolder | OpenID4VP', () => { + let openIdVerifier: OpenId4VcVerifierRecord let verifier: AgentType let holder: AgentType let verifierApp: Express + // eslint-disable-next-line @typescript-eslint/no-explicit-any let verifierServer: Server - const mockFunction = jest.fn() - mockFunction.mockReturnValue({ status: 200 }) - beforeEach(async () => { verifier = await createAgentFromModules('verifier', verifierModules, '96213c3d7fc8d4d6754c7a0fd969598f') + openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() holder = await createAgentFromModules('holder', holderModules, '96213c3d7fc8d4d6754c7a0fd969598e') verifierApp = express() - const router = await verifier.agent.modules.openId4VcVerifier.configureRouter(Router(), { - basePath: '/', - verificationEndpointConfig: { - enabled: true, - proofResponseHandler: mockFunction, - }, - }) - - verifierApp.use('/', router) - + verifierApp.use('/', verifier.agent.modules.openId4VcVerifier.config.router) verifierServer = verifierApp.listen(port) }) @@ -80,553 +59,52 @@ describe('OpenId4VcHolder | OpenID4VP', () => { await verifier.agent.wallet.delete() }) - it('siop request with static metadata', async () => { - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - } - - //////////////////////////// RP (create request) //////////////////////////// - const { authorizationRequestUri, metadata } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - - //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - - if (result.proofType == 'presentation') throw new Error('Expected an authenticationRequest') - - //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest( - result.authenticationRequest, - holder.verificationMethod - ) - - expect(status).toBe(200) - - expect(result.authenticationRequest.authorizationRequestPayload.redirect_uri).toBe( - verifierBaseUrl + verificationEndpointPath - ) - expect(result.authenticationRequest.issuer).toBe(verifier.verificationMethod.controller) - - //////////////////////////// RP (verify the response) //////////////////////////// - - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( - submittedResponse - ) - - const { state, nonce } = metadata - expect(submission).toBe(undefined) - expect(idTokenPayload).toBeDefined() - expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(nonce) - - await waitForMockFunction(mockFunction) - expect(mockFunction).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload), - submission: undefined, - }) - }) - - // TODO: not working yet - xit('siop request with issuer', async () => { - nock('https://helloworld.com') - .get('/.well-known/openid-configuration') - .reply(200, staticOpOpenIdConfigEdDSA) - .get('/.well-known/openid-configuration') - .reply(200, staticOpOpenIdConfigEdDSA) - .get('/.well-known/openid-configuration') - .reply(200, staticOpOpenIdConfigEdDSA) - .get('/.well-known/openid-configuration') - .reply(200, staticOpOpenIdConfigEdDSA) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - // TODO: if provided this way client metadata is not resolved for the verification method - openIdProvider: 'https://helloworld.com', - } - - //////////////////////////// RP (create request) //////////////////////////// - const { authorizationRequestUri, metadata } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - - //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - - if (result.proofType == 'presentation') throw new Error('Expected a proofType') - - //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest( - result.authenticationRequest, - holder.verificationMethod - ) - - expect(status).toBe(200) - - //////////////////////////// RP (verify the response) //////////////////////////// - - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( - submittedResponse - ) - - const { state, nonce } = metadata - expect(idTokenPayload).toBeDefined() - expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(nonce) - - await waitForMockFunction(mockFunction) - expect(mockFunction).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload), - submission: expect.objectContaining(submission), - }) - }) - - it('resolving vp request with no credentials', async () => { - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions - ) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - expect(result.credentialsForRequest.areRequirementsSatisfied).toBeFalsy() - expect(result.credentialsForRequest.requirements.length).toBe(1) - }) - - it('resolving vp request with wrong credentials errors', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + it('siop authorization request without presentation exchange', async () => { + const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'did', + didUrl: verifier.kid, + }, + verifierId: openIdVerifier.verifierId, }) - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions + const resolvedAuthorizationRequest = await holder.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri ) - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - expect(result.credentialsForRequest.areRequirementsSatisfied).toBeFalsy() - expect(result.credentialsForRequest.requirements.length).toBe(1) - }) - - it('expect submitting a wrong submission to fail', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), - }) - - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), - }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - } - - const { authorizationRequestUri: openBadge } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) - const { authorizationRequestUri: university } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ - ...createProofRequestOptions, - presentationDefinition: universityDegreePresentationDefinition, + const { submittedResponse, serverResponse } = + await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, + // When no VP is created, we need to provide the did we want to use for authentication + openIdTokenIssuer: { + method: 'did', + didUrl: holder.kid, + }, }) - //////////////////////////// OP (validate and parse the request) //////////////////////////// - - const resolvedOpenBadge = await holder.agent.modules.openId4VcHolder.resolveProofRequest(openBadge) - const resolvedUniversityDegree = await holder.agent.modules.openId4VcHolder.resolveProofRequest(university) - if (resolvedOpenBadge.proofType !== 'presentation') throw new Error('expected prooftype presentation') - if (resolvedUniversityDegree.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - await expect( - holder.agent.modules.openId4VcHolder.acceptPresentationRequest(resolvedOpenBadge.presentationRequest, { - submission: resolvedUniversityDegree.credentialsForRequest, - submissionEntryIndexes: [0], - }) - ).rejects.toThrow() - }) - - it('resolving vp request with multiple credentials in wallet only allows selecting the correct ones', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), - }) - - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), - }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions - ) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - const { presentationRequest, credentialsForRequest } = result - expect(credentialsForRequest.areRequirementsSatisfied).toBeTruthy() - expect(credentialsForRequest.requirements.length).toBe(1) - expect(credentialsForRequest.requirements[0].needsCount).toBe(1) - expect(credentialsForRequest.requirements[0].submissionEntry.length).toBe(1) - expect(credentialsForRequest.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') - - expect(presentationRequest.presentationDefinitions[0].definition).toMatchObject(openBadgePresentationDefinition) - }) - - it('resolving vp request with multiple credentials in wallet select the correct credentials from the wallet', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), - }) - - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), - }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: combinePresentationDefinitions([ - openBadgePresentationDefinition, - universityDegreePresentationDefinition, - ]), - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions - ) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - const { credentialsForRequest } = result - expect(credentialsForRequest.areRequirementsSatisfied).toBeTruthy() - expect(credentialsForRequest.requirements.length).toBe(2) - expect(credentialsForRequest.requirements[0].needsCount).toBe(1) - expect(credentialsForRequest.requirements[0].submissionEntry.length).toBe(1) - expect(credentialsForRequest.requirements[1].needsCount).toBe(1) - expect(credentialsForRequest.requirements[1].submissionEntry.length).toBe(1) - expect(credentialsForRequest.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') - expect(credentialsForRequest.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') - - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( - result.presentationRequest, - { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0, 0], - } - ) - - expect(status).toBe(200) - - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( - submittedResponse - ) - - expect(idTokenPayload).toBeDefined() - expect(submission).toBeDefined() - expect(submission?.presentationDefinitions).toHaveLength(1) - expect(submission?.submissionData.definition_id).toBe('Combined') - expect(submission?.presentations).toHaveLength(1) - expect(submission?.presentations[0].vcs).toHaveLength(2) - - if (submission?.presentations[0].vcs[0].credential.type.includes('OpenBadgeCredential')) { - expect(submission?.presentations[0].vcs[0].credential.type).toEqual([ - 'VerifiableCredential', - 'OpenBadgeCredential', - ]) - expect(submission?.presentations[0].vcs[1].credential.type).toEqual([ - 'VerifiableCredential', - 'UniversityDegreeCredential', - ]) - } else { - expect(submission?.presentations[0].vcs[1].credential.type).toEqual([ - 'VerifiableCredential', - 'OpenBadgeCredential', - ]) - expect(submission?.presentations[0].vcs[0].credential.type).toEqual([ - 'VerifiableCredential', - 'UniversityDegreeCredential', - ]) - } - - await waitForMockFunction(mockFunction) - expect(mockFunction).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload), - submission: expect.objectContaining(submission), - }) - }) - - it('expect accepting a proof request with only a partial set of requirements to error', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + expect(serverResponse).toEqual({ + status: 200, + body: '', }) - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), + expect(submittedResponse).toMatchObject({ + expires_in: 6000, + id_token: expect.any(String), + state: expect.any(String), }) - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: combinePresentationDefinitions([ - openBadgePresentationDefinition, - universityDegreePresentationDefinition, - ]), - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions - ) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - await expect( - holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0], + const { idToken, presentationExchange } = + await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse({ + authorizationResponse: submittedResponse, + verifierId: openIdVerifier.verifierId, }) - ).rejects.toThrow() - }) - - it('expect vp request with single requested credential to succeed', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), - }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - } - - const { authorizationRequestUri, metadata } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') - - //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - // Select the appropriate credentials - - result.credentialsForRequest.requirements[0] - - if (!result.credentialsForRequest.areRequirementsSatisfied) { - throw new Error('Requirements are not satisfied.') - } - - //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( - result.presentationRequest, - { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0], - } - ) - - expect(status).toBe(200) - - // The RP MUST validate that the aud (audience) Claim contains the value of the client_id - // that the RP sent in the Authorization Request as an audience. - // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( - submittedResponse - ) - - const { state, nonce } = metadata - expect(idTokenPayload).toBeDefined() - expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(nonce) - expect(submission).toBeDefined() - expect(submission?.presentationDefinitions).toHaveLength(1) - expect(submission?.submissionData.definition_id).toBe('OpenBadgeCredential') - expect(submission?.presentations).toHaveLength(1) - expect(submission?.presentations[0].vcs[0].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) - - await waitForMockFunction(mockFunction) - expect(mockFunction).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload), - submission: expect.objectContaining(submission), - }) - }) - - it('expect vp request with single requested ldp_vc credential to succeed', async () => { - const credential = await getOpenBadgeCredentialLdpVc( - verifier.agent.context, - verifier.verificationMethod, - holder.verificationMethod - ) - await holder.agent.w3cCredentials.storeCredential({ - credential, - }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgeCredentialPresentationDefinitionLdpVc, - } - - const { authorizationRequestUri, metadata } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') - - //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - // Select the appropriate credentials - - if (!result.credentialsForRequest.areRequirementsSatisfied) { - throw new Error('Requirements are not satisfied.') - } - - //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( - result.presentationRequest, - { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0], - } - ) - - expect(status).toBe(200) - - // The RP MUST validate that the aud (audience) Claim contains the value of the client_id - // that the RP sent in the Authorization Request as an audience. - // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( - submittedResponse - ) - - const { state, nonce } = metadata - expect(idTokenPayload).toBeDefined() - expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(nonce) - - expect(submission).toBeDefined() - expect(submission?.presentationDefinitions).toHaveLength(1) - expect(submission?.submissionData.definition_id).toBe('OpenBadgeCredential') - expect(submission?.presentations).toHaveLength(1) - expect(submission?.presentations[0].vcs[0].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) - - await waitForMockFunction(mockFunction) - expect(mockFunction).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload), - submission: expect.objectContaining(submission), - }) - }) - - it('expects the submission to fail if there are too few submission entry indexes, and also to fail when requesting two different presentation formats', async () => { - const credential = await getOpenBadgeCredentialLdpVc( - verifier.agent.context, - verifier.verificationMethod, - holder.verificationMethod - ) - - await holder.agent.w3cCredentials.storeCredential({ credential }) - - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + expect(presentationExchange).toBeUndefined() + expect(idToken).toMatchObject({ + payload: { + state: expect.any(String), + nonce: expect.any(String), + }, }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: combinePresentationDefinitions([ - universityDegreePresentationDefinition, - openBadgeCredentialPresentationDefinitionLdpVc, - ]), - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions - ) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') - - //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - // Select the appropriate credentials - - if (!result.credentialsForRequest.areRequirementsSatisfied) { - throw new Error('Requirements are not satisfied.') - } - - //////////////////////////// OP (accept the verified request) //////////////////////////// - await expect( - holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0, 0], - }) - ).rejects.toThrow() - - await expect( - holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0], - }) - ).rejects.toThrow() }) - - // it('edited walt vp request', async () => { - // const credential = W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt) - // await holder.w3cCredentials.storeCredential({ credential }) - - // const authorizationRequestUri = - // 'openid4vp://authorize?response_type=vp_token&client_id=https%3A%2F%2Fverifier.portal.walt.id%2Fopenid4vc%2Fverify&response_mode=direct_post&state=97509d5c-2dd2-490b-8617-577f45e3b6d0&presentation_definition=%7B%22id%22%3A%22test%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22OpenBadgeCredential%22%2C%22format%22%3A%7B%22jwt_vc%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%5D%7D%7D%2C%22constraints%22%3A%7B%22fields%22%3A%5B%7B%22path%22%3A%5B%22%24.vc.type.%2A%22%5D%2C%22filter%22%3A%7B%22type%22%3A%22string%22%2C%22pattern%22%3A%22OpenBadgeCredential%22%7D%7D%5D%7D%7D%5D%7D&client_id_scheme=redirect_uri&response_uri=https%3A%2F%2Fverifier.portal.walt.id%2Fopenid4vc%2Fverify%2F97509d5c-2dd2-490b-8617-577f45e3b6d0' - - // //////////////////////////// OP (validate and parse the request) //////////////////////////// - // const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - // if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') - - // //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - // // Select the appropriate credentials - - // const { presentationRequest, selectResults } = result - // result.selectResults.requirements[0] - - // if (!result.selectResults.areRequirementsSatisfied) { - // throw new Error('Requirements are not satisfied.') - // } - - // //////////////////////////// OP (accept the verified request) //////////////////////////// - // const responseStatus = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest(presentationRequest, { - // submission: selectResults, - // submissionEntryIndexes: [0], - // }) - - // expect(responseStatus.ok).toBeTruthy() - // }) }) diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/OpenId4VcIsserModule.test.ts similarity index 100% rename from packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts rename to packages/openid4vc/src/openid4vc-issuer/__tests__/OpenId4VcIsserModule.test.ts diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index 4d04802de9..4d454231fe 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -135,7 +135,7 @@ export class OpenId4VcSiopVerifierService { } const relyingParty = await this.getRelyingParty(agentContext, options.verifier, { - presentationDefinition: presentationDefinitionsWithLocation?.[0].definition, + presentationDefinition: presentationDefinitionsWithLocation?.[0]?.definition, clientId: requestClientId, }) @@ -156,10 +156,10 @@ export class OpenId4VcSiopVerifierService { }, }) - const presentationExchange = response.oid4vpSubmission + const presentationExchange = response.oid4vpSubmission?.submissionData ? { submission: response.oid4vpSubmission.submissionData, - definition: response.oid4vpSubmission.presentationDefinitions[0].definition, + definition: response.oid4vpSubmission.presentationDefinitions[0]?.definition, presentations: response.oid4vpSubmission?.presentations.map(getVerifiablePresentationFromSphereonWrapped), } : undefined diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/OpenId4VcVerifierModule.test.ts similarity index 56% rename from packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts rename to packages/openid4vc/src/openid4vc-verifier/__tests__/OpenId4VcVerifierModule.test.ts index cf9becd453..4251c0586e 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/OpenId4VcVerifierModule.test.ts @@ -1,9 +1,13 @@ -/* eslint-disable @typescript-eslint/unbound-method */ +import type { OpenId4VcVerifierModuleConfigOptions } from '../OpenId4VcVerifierModuleConfig' import type { DependencyManager } from '@aries-framework/core' +import { Router } from 'express' + +import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' import { OpenId4VcVerifierApi } from '../OpenId4VcVerifierApi' import { OpenId4VcVerifierModule } from '../OpenId4VcVerifierModule' -import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' +import { OpenId4VcVerifierModuleConfig } from '../OpenId4VcVerifierModuleConfig' +import { OpenId4VcVerifierRepository } from '../repository' const dependencyManager = { registerInstance: jest.fn(), @@ -14,20 +18,29 @@ const dependencyManager = { describe('OpenId4VcVerifierModule', () => { test('registers dependencies on the dependency manager', () => { - const verifierMetadata = { - verifierBaseUrl: 'http://redirect-uri', - verificationEndpointPath: '', - } - const openId4VcClientModule = new OpenId4VcVerifierModule({ verifierMetadata }) - + const options = { + baseUrl: 'http://localhost:3000', + endpoints: { + authorization: { + endpointPath: '/hello', + }, + }, + router: Router(), + } satisfies OpenId4VcVerifierModuleConfigOptions + const openId4VcClientModule = new OpenId4VcVerifierModule(options) openId4VcClientModule.register(dependencyManager) expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith( + OpenId4VcVerifierModuleConfig, + new OpenId4VcVerifierModuleConfig(options) + ) expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcVerifierApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcSiopVerifierService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcVerifierRepository) }) }) diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts index b80d752396..106cce5849 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts @@ -1,34 +1,25 @@ -import { AskarModule } from '@aries-framework/askar' import { Jwt } from '@aries-framework/core' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { SigningAlgo } from '@sphereon/did-auth-siop' import { cleanAll, enableNetConnect } from 'nock' -import { OpenId4VcVerifierModule } from '..' +import { AskarModule } from '../../../../askar/src' +import { askarModuleConfig } from '../../../../askar/tests/helpers' import { createAgentFromModules, type AgentType } from '../../../tests/utils' -import { - staticOpOpenIdConfigEdDSA, - staticSiopConfigEDDSA, - universityDegreePresentationDefinition, -} from '../../../tests/utilsVp' +import { universityDegreePresentationDefinition } from '../../../tests/utilsVp' +import { OpenId4VcVerifierModule } from '../OpenId4VcVerifierModule' const modules = { openId4VcVerifier: new OpenId4VcVerifierModule({ - verifierMetadata: { - verifierBaseUrl: 'http://redirect-uri', - verificationEndpointPath: '', - }, - }), - askar: new AskarModule({ - ariesAskar, + baseUrl: 'http://redirect-uri', }), + askar: new AskarModule(askarModuleConfig), } describe('OpenId4VcVerifier', () => { let verifier: AgentType beforeEach(async () => { - verifier = await createAgentFromModules('verifier', { ...modules }, '96213c3d7fc8d4d6754c7a0fd969598f') + verifier = await createAgentFromModules('verifier', modules, '96213c3d7fc8d4d6754c7a0fd969598f') }) afterEach(async () => { @@ -42,38 +33,33 @@ describe('OpenId4VcVerifier', () => { enableNetConnect() }) - it(`cannot sign authorization request with alg that isn't supported by the OpenId Provider`, async () => { - await expect( - verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ - verificationEndpointUrl: 'http://redirect-uri', - verificationMethod: verifier.verificationMethod, - }) - ).rejects.toThrow() - }) - - it(`check openid proof request format`, async () => { + it('check openid proof request format', async () => { + const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ - verificationEndpointUrl: 'http://redirect-uri', - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: universityDegreePresentationDefinition, + requestSigner: { + method: 'did', + didUrl: verifier.kid, + }, + verifierId: openIdVerifier.verifierId, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, }) - const base = - 'openid://?redirect_uri=http%3A%2F%2Fredirect-uri&presentation_definition=%7B%22id%22%3A%22UniversityDegreeCredential%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22UniversityDegree%22%2C%22format%22%3A%7B%22jwt_vc%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%5D%7D%7D%2C%22constraints%22%3A%7B%22fields%22%3A%5B%7B%22path%22%3A%5B%22%24.vc.type.*%22%5D%2C%22filter%22%3A%7B%22type%22%3A%22string%22%2C%22pattern%22%3A%22UniversityDegree%22%7D%7D%5D%7D%7D%5D%7D&request=' + const base = `openid://?redirect_uri=http%3A%2F%2Fredirect-uri%2F${openIdVerifier.verifierId}%2Fauthorize&request=` expect(authorizationRequestUri.startsWith(base)).toBe(true) const _jwt = authorizationRequestUri.substring(base.length) const jwt = Jwt.fromSerializedJwt(_jwt) - expect(authorizationRequestUri.startsWith(base)).toBe(true) - expect(jwt.header.kid).toEqual(verifier.kid) expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) expect(jwt.header.typ).toEqual('JWT') expect(jwt.payload.additionalClaims.scope).toEqual('openid') - expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.kid) - expect(jwt.payload.additionalClaims.redirect_uri).toEqual('http://redirect-uri') + expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did) + expect(jwt.payload.additionalClaims.redirect_uri).toEqual( + `http://redirect-uri/${openIdVerifier.verifierId}/authorize` + ) expect(jwt.payload.additionalClaims.response_mode).toEqual('post') expect(jwt.payload.additionalClaims.nonce).toBeDefined() expect(jwt.payload.additionalClaims.state).toBeDefined() @@ -81,32 +67,5 @@ describe('OpenId4VcVerifier', () => { expect(jwt.payload.iss).toEqual(verifier.did) expect(jwt.payload.sub).toEqual(verifier.did) }) - - it(`check siop proof request format`, async () => { - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ - verificationEndpointUrl: 'http://redirect-uri', - verificationMethod: verifier.verificationMethod, - openIdProvider: staticSiopConfigEDDSA, - }) - - // TODO: this should be siopv2 - const base = 'openid://?redirect_uri=http%3A%2F%2Fredirect-uri&request=' - expect(authorizationRequestUri.startsWith(base)).toBe(true) - - const _jwt = authorizationRequestUri.substring(base.length) - const jwt = Jwt.fromSerializedJwt(_jwt) - - expect(jwt.header.kid).toEqual(verifier.kid) - expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) - expect(jwt.payload.additionalClaims.scope).toEqual('openid') - expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.kid) - expect(jwt.payload.additionalClaims.redirect_uri).toEqual('http://redirect-uri') - expect(jwt.payload.additionalClaims.response_mode).toEqual('post') - expect(jwt.payload.additionalClaims.response_type).toEqual('id_token') - expect(jwt.payload.additionalClaims.nonce).toBeDefined() - expect(jwt.payload.additionalClaims.state).toBeDefined() - expect(jwt.payload.iss).toEqual(verifier.did) - expect(jwt.payload.sub).toEqual(verifier.did) - }) }) }) diff --git a/packages/openid4vc/src/openid4vc-verifier/index.ts b/packages/openid4vc/src/openid4vc-verifier/index.ts index 7c8fd8a4b1..25a6548336 100644 --- a/packages/openid4vc/src/openid4vc-verifier/index.ts +++ b/packages/openid4vc/src/openid4vc-verifier/index.ts @@ -3,3 +3,4 @@ export * from './OpenId4VcVerifierModule' export * from './OpenId4VcSiopVerifierService' export * from './OpenId4VcSiopVerifierServiceOptions' export * from './OpenId4VcVerifierModuleConfig' +export * from './repository' diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 7039161bb5..038c854c76 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -384,7 +384,7 @@ describe('OpenId4Vc', () => { resolvedProofRequest1.presentationExchange.credentialsForRequest ) - const { status: status1, submittedResponse: submittedResponse1 } = + const { submittedResponse: submittedResponse1, serverResponse: serverResponse1 } = await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ authorizationRequest: resolvedProofRequest1.authorizationRequest, presentationExchange: { @@ -414,7 +414,9 @@ describe('OpenId4Vc', () => { state: expect.any(String), vp_token: expect.any(String), }) - expect(status1).toBe(200) + expect(serverResponse1).toMatchObject({ + status: 200, + }) // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. @@ -452,14 +454,16 @@ describe('OpenId4Vc', () => { resolvedProofRequest2.presentationExchange.credentialsForRequest ) - const { status: status2, submittedResponse: submittedResponse2 } = + const { serverResponse: serverResponse2, submittedResponse: submittedResponse2 } = await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ authorizationRequest: resolvedProofRequest2.authorizationRequest, presentationExchange: { credentials: selectedCredentials2, }, }) - expect(status2).toBe(200) + expect(serverResponse2).toMatchObject({ + status: 200, + }) // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. @@ -601,12 +605,13 @@ describe('OpenId4Vc', () => { resolvedAuthorizationRequest.presentationExchange.credentialsForRequest ) - const { status, submittedResponse } = await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ - authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, - presentationExchange: { - credentials: selectedCredentials, - }, - }) + const { serverResponse, submittedResponse } = + await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) // path_nested should not be used for sd-jwt expect(submittedResponse.presentation_submission?.descriptor_map[0].path_nested).toBeUndefined() @@ -627,7 +632,9 @@ describe('OpenId4Vc', () => { state: expect.any(String), vp_token: expect.any(String), }) - expect(status).toBe(200) + expect(serverResponse).toMatchObject({ + status: 200, + }) // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. diff --git a/tsconfig.test.json b/tsconfig.test.json index 4fa873e5a5..b39f15bce9 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -4,6 +4,9 @@ "require": ["tsconfig-paths/register"] }, "compilerOptions": { + // Needed because of type-issued in sphereon siop-oid4vp lib + // https://github.com/Sphereon-Opensource/SIOP-OID4VP/pull/71#issuecomment-1913552869 + "skipLibCheck": true, "baseUrl": ".", "paths": { "@aries-framework/*": ["packages/*/src"] diff --git a/yarn.lock b/yarn.lock index 9262f00d5d..0460bc2a93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3109,7 +3109,7 @@ dependencies: "@types/express" "*" -"@types/node@*", "@types/node@>=13.7.0", "@types/node@^18.18.8": +"@types/node@*", "@types/node@18.18.8", "@types/node@>=13.7.0", "@types/node@^18.18.8": version "18.18.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.8.tgz#2b285361f2357c8c8578ec86b5d097c7f464cfd6" integrity sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ==