diff --git a/packages/consumption/src/consumption/ConsumptionController.ts b/packages/consumption/src/consumption/ConsumptionController.ts index d116e4dd9..9a26c0fc9 100644 --- a/packages/consumption/src/consumption/ConsumptionController.ts +++ b/packages/consumption/src/consumption/ConsumptionController.ts @@ -11,6 +11,7 @@ import { ProposeAttributeRequestItem, ReadAttributeRequestItem, ShareAttributeRequestItem, + ShareAuthorizationRequestRequestItem, ShareCredentialOfferRequestItem, TransferFileOwnershipRequestItem } from "@nmshd/content"; @@ -42,6 +43,7 @@ import { RequestItemProcessorRegistry, SettingsController, ShareAttributeRequestItemProcessor, + ShareAuthorizationRequestRequestItemProcessor, ShareCredentialOfferRequestItemProcessor, TransferFileOwnershipRequestItemProcessor } from "../modules"; @@ -163,7 +165,8 @@ export class ConsumptionController { [AuthenticationRequestItem, GenericRequestItemProcessor], [FormFieldRequestItem, FormFieldRequestItemProcessor], [TransferFileOwnershipRequestItem, TransferFileOwnershipRequestItemProcessor], - [ShareCredentialOfferRequestItem, ShareCredentialOfferRequestItemProcessor] + [ShareCredentialOfferRequestItem, ShareCredentialOfferRequestItemProcessor], + [ShareAuthorizationRequestRequestItem, ShareAuthorizationRequestRequestItemProcessor] ]); } diff --git a/packages/consumption/src/modules/requests/index.ts b/packages/consumption/src/modules/requests/index.ts index 985d56e91..c553a2ded 100644 --- a/packages/consumption/src/modules/requests/index.ts +++ b/packages/consumption/src/modules/requests/index.ts @@ -30,6 +30,7 @@ export * from "./itemProcessors/RequestItemConstructor"; export * from "./itemProcessors/RequestItemProcessorConstructor"; export * from "./itemProcessors/RequestItemProcessorRegistry"; export * from "./itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor"; +export * from "./itemProcessors/shareAuthorizationRequest/ShareAuthorizationRequestRequestItemProcessor"; export * from "./itemProcessors/shareCredentialOffer/ShareCredentialOfferRequestItemProcessor"; export * from "./itemProcessors/transferFileOwnership/TransferFileOwnershipRequestItemProcessor"; export * from "./local/LocalRequest"; diff --git a/packages/consumption/src/modules/requests/itemProcessors/shareAuthorizationRequest/ShareAuthorizationRequestRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/shareAuthorizationRequest/ShareAuthorizationRequestRequestItemProcessor.ts new file mode 100644 index 000000000..5754bcbe8 --- /dev/null +++ b/packages/consumption/src/modules/requests/itemProcessors/shareAuthorizationRequest/ShareAuthorizationRequestRequestItemProcessor.ts @@ -0,0 +1,43 @@ +import { AcceptResponseItem, ResponseItemResult, ShareAuthorizationRequestRequestItem } from "@nmshd/content"; +import { ConsumptionCoreErrors } from "../../../../consumption/ConsumptionCoreErrors"; +import { ValidationResult } from "../../../common/ValidationResult"; +import { AcceptRequestItemParametersJSON } from "../../incoming/decide/AcceptRequestItemParameters"; +import { GenericRequestItemProcessor } from "../GenericRequestItemProcessor"; +import { LocalRequestInfo } from "../IRequestItemProcessor"; + +export class ShareAuthorizationRequestRequestItemProcessor extends GenericRequestItemProcessor { + public override async canAccept( + requestItem: ShareAuthorizationRequestRequestItem, + _params: AcceptRequestItemParametersJSON, + _requestInfo: LocalRequestInfo + ): Promise { + try { + const resolvedAuthorizationRequest = await this.consumptionController.openId4Vc.resolveAuthorizationRequest(requestItem.authorizationRequestUrl); + if (resolvedAuthorizationRequest.matchingCredentials.length === 0) { + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidRequestItem( + `The authorization request at URL '${requestItem.authorizationRequestUrl}' can't be fulfilled with the credentials currently in the wallet.` + ) + ); + } + return ValidationResult.success(); + } catch (error) { + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidRequestItem( + `The authorization request at URL '${requestItem.authorizationRequestUrl}' could not be processed. Cause: ${error}` + ) + ); + } + } + + public override async accept( + requestItem: ShareAuthorizationRequestRequestItem, + _params: AcceptRequestItemParametersJSON, + _requestInfo: LocalRequestInfo + ): Promise { + const resolvedAuthorizationRequest = await this.consumptionController.openId4Vc.resolveAuthorizationRequest(requestItem.authorizationRequestUrl); + await this.consumptionController.openId4Vc.acceptAuthorizationRequest(resolvedAuthorizationRequest.authorizationRequest); + + return AcceptResponseItem.from({ result: ResponseItemResult.Accepted }); + } +} diff --git a/packages/content/src/requests/RequestItem.ts b/packages/content/src/requests/RequestItem.ts index 27bb7fbab..7db0cc5a3 100644 --- a/packages/content/src/requests/RequestItem.ts +++ b/packages/content/src/requests/RequestItem.ts @@ -19,6 +19,7 @@ import { IProposeAttributeRequestItem, IReadAttributeRequestItem, IShareAttributeRequestItem, + IShareAuthorizationRequestRequestItem, IShareCredentialOfferRequestItem, ITransferFileOwnershipRequestItem, ProposeAttributeRequestItem, @@ -27,6 +28,8 @@ import { ReadAttributeRequestItemJSON, ShareAttributeRequestItem, ShareAttributeRequestItemJSON, + ShareAuthorizationRequestRequestItem, + ShareAuthorizationRequestRequestItemJSON, ShareCredentialOfferRequestItem, ShareCredentialOfferRequestItemJSON, TransferFileOwnershipRequestItem, @@ -65,7 +68,8 @@ export type RequestItemJSONDerivations = | AuthenticationRequestItemJSON | FormFieldRequestItemJSON | TransferFileOwnershipRequestItemJSON - | ShareCredentialOfferRequestItemJSON; + | ShareCredentialOfferRequestItemJSON + | ShareAuthorizationRequestRequestItemJSON; export interface IRequestItem extends ISerializable { /** @@ -99,7 +103,8 @@ export type IRequestItemDerivations = | IAuthenticationRequestItem | IFormFieldRequestItem | ITransferFileOwnershipRequestItem - | IShareCredentialOfferRequestItem; + | IShareCredentialOfferRequestItem + | IShareAuthorizationRequestRequestItem; export abstract class RequestItem extends Serializable { @serialize() @@ -130,7 +135,8 @@ export type RequestItemDerivations = | AuthenticationRequestItem | FormFieldRequestItem | TransferFileOwnershipRequestItem - | ShareCredentialOfferRequestItem; + | ShareCredentialOfferRequestItem + | ShareAuthorizationRequestRequestItem; export function isRequestItemDerivation(input: any): input is RequestItemDerivations { return ( @@ -144,6 +150,7 @@ export function isRequestItemDerivation(input: any): input is RequestItemDerivat input["@type"] === "AuthenticationRequestItem" || input["@type"] === "FormFieldRequestItem" || input["@type"] === "TransferFileOwnershipRequestItem" || - input["@type"] === "ShareCredentialOfferRequestItem" + input["@type"] === "ShareCredentialOfferRequestItem" || + input["@type"] === "ShareAuthorizationRequestRequestItem" ); } diff --git a/packages/content/src/requests/items/index.ts b/packages/content/src/requests/items/index.ts index 9428c0c81..cb814d952 100644 --- a/packages/content/src/requests/items/index.ts +++ b/packages/content/src/requests/items/index.ts @@ -14,6 +14,7 @@ export * from "./proposeAttribute/ProposeAttributeRequestItem"; export * from "./readAttribute/ReadAttributeAcceptResponseItem"; export * from "./readAttribute/ReadAttributeRequestItem"; export * from "./shareAttribute/ShareAttributeRequestItem"; +export * from "./shareAuthorizationRequest/ShareAuthorizationRequestRequestItem"; export * from "./shareCredentialOffer/ShareCredentialOfferRequestItem"; export * from "./transferFileOwnership/TransferFileOwnershipAcceptResponseItem"; export * from "./transferFileOwnership/TransferFileOwnershipRequestItem"; diff --git a/packages/content/src/requests/items/shareAuthorizationRequest/ShareAuthorizationRequestRequestItem.ts b/packages/content/src/requests/items/shareAuthorizationRequest/ShareAuthorizationRequestRequestItem.ts new file mode 100644 index 000000000..17028b6ae --- /dev/null +++ b/packages/content/src/requests/items/shareAuthorizationRequest/ShareAuthorizationRequestRequestItem.ts @@ -0,0 +1,29 @@ +import { serialize, type, validate } from "@js-soft/ts-serval"; +import { RequestItemJSON } from "../.."; +import { IRequestItem, RequestItem } from "../../RequestItem"; + +export interface ShareAuthorizationRequestRequestItemJSON extends RequestItemJSON { + "@type": "ShareAuthorizationRequestRequestItem"; + authorizationRequestUrl: string; +} + +export interface IShareAuthorizationRequestRequestItem extends IRequestItem { + authorizationRequestUrl: string; +} + +@type("ShareAuthorizationRequestRequestItem") +export class ShareAuthorizationRequestRequestItem extends RequestItem implements IShareAuthorizationRequestRequestItem { + @serialize() + @validate() + public authorizationRequestUrl: string; + + public static from( + value: IShareAuthorizationRequestRequestItem | Omit | ShareAuthorizationRequestRequestItemJSON + ): ShareAuthorizationRequestRequestItem { + return this.fromAny(value); + } + + public override toJSON(verbose?: boolean | undefined, serializeAsString?: boolean | undefined): ShareAuthorizationRequestRequestItemJSON { + return super.toJSON(verbose, serializeAsString) as ShareAuthorizationRequestRequestItemJSON; + } +} diff --git a/packages/runtime/src/dataViews/DataViewExpander.ts b/packages/runtime/src/dataViews/DataViewExpander.ts index 0b6c9a671..2a846ece1 100644 --- a/packages/runtime/src/dataViews/DataViewExpander.ts +++ b/packages/runtime/src/dataViews/DataViewExpander.ts @@ -49,6 +49,7 @@ import { ResponseJSON, SexJSON, ShareAttributeRequestItemJSON, + ShareAuthorizationRequestRequestItemJSON, ShareCredentialOfferRequestItemJSON, SurnameJSON, ThirdPartyRelationshipAttributeQueryJSON, @@ -133,6 +134,7 @@ import { ResponseItemDVO, ResponseItemGroupDVO, ShareAttributeRequestItemDVO, + ShareAuthorizationRequestRequestItemDVO, ShareCredentialOfferRequestItemDVO, ThirdPartyRelationshipAttributeQueryDVO, TransferFileOwnershipAcceptResponseItemDVO, @@ -655,6 +657,24 @@ export class DataViewExpander { credentialResponses } as ShareCredentialOfferRequestItemDVO; + case "ShareAuthorizationRequestRequestItem": + const shareAuthorizationRequestRequestItem = requestItem as ShareAuthorizationRequestRequestItemJSON; + + const resolutionResult = await this.consumption.openId4Vc.resolveAuthorizationRequest({ + authorizationRequestUrl: shareAuthorizationRequestRequestItem.authorizationRequestUrl + }); + const matchingCredentials = resolutionResult.isSuccess ? resolutionResult.value.matchingCredentials : []; + + return { + ...shareAuthorizationRequestRequestItem, + type: "ShareAuthorizationRequestRequestItemDVO", + id: "", + name: this.generateRequestItemName(requestItem["@type"], isDecidable), + isDecidable: isDecidable && matchingCredentials.length > 0, + response: responseItemDVO, + matchingCredentials: await this.expandLocalAttributeDTOs(matchingCredentials) + } as ShareAuthorizationRequestRequestItemDVO; + default: return { ...requestItem, diff --git a/packages/runtime/src/dataViews/content/RequestItemDVOs.ts b/packages/runtime/src/dataViews/content/RequestItemDVOs.ts index ff22c5901..75d10aac8 100644 --- a/packages/runtime/src/dataViews/content/RequestItemDVOs.ts +++ b/packages/runtime/src/dataViews/content/RequestItemDVOs.ts @@ -82,3 +82,9 @@ export interface ShareCredentialOfferRequestItemDVO extends RequestItemDVO { credentialOfferUrl: string; credentialResponses?: OpenId4VciCredentialResponseJSON[]; } + +export interface ShareAuthorizationRequestRequestItemDVO extends RequestItemDVO { + type: "ShareAuthorizationRequestRequestItemDVO"; + authorizationRequestUrl: string; + matchingCredentials?: LocalAttributeDVO[]; +} diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 83166975f..c32723f19 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -422,6 +422,66 @@ describe("custom openid4vc service", () => { expect(presentationResult.value.status).toBe(200); }); + test("request presentation using requests", async () => { + const createPresentationResponse = await axiosInstance.post("/presentation/presentationRequests", { + pex: { + id: "anId", + purpose: "To prove you work here", + + // eslint-disable-next-line @typescript-eslint/naming-convention + input_descriptors: [ + { + id: "EmployeeIdCard", + format: { + // eslint-disable-next-line @typescript-eslint/naming-convention + "vc+sd-jwt": { + // eslint-disable-next-line @typescript-eslint/naming-convention + "sd-jwt_alg_values": ["RS256", "PS256", "HS256", "ES256", "ES256K", "RS384", "PS384", "HS384", "ES384", "RS512", "PS512", "HS512", "ES512", "EdDSA"] + } + }, + constraints: { + fields: [ + { + path: ["$.vct"], + filter: { + type: "string", + pattern: "EmployeeIdCard" + } + } + ] + } + } + ] + }, + version: "v1.draft21" + }); + expect(createPresentationResponse.status).toBe(200); + const createPresentationResponseData = await createPresentationResponse.data; + const authorizationRequestUrl = createPresentationResponseData.result.presentationRequest as string; + const authorizationRequestId = authorizationRequestUrl.split("%2F").at(-1)?.slice(0, 36); + + await exchangeAndAcceptRequestByMessage( + runtimeServices1, + runtimeServices2, + { + content: { + items: [ + { + "@type": "ShareAuthorizationRequestRequestItem", + authorizationRequestUrl, + mustBeAccepted: true + } + ] + }, + peer: (await runtimeServices2.transport.account.getIdentityInfo()).value.address + }, + [{ accept: true }] + ); + + const verificationStatus = (await axiosInstance.get(`/presentation/presentationRequests/${authorizationRequestId}/verificationSessionState`)).data.result; + expect(verificationStatus).toBe("ResponseVerified"); + }); + async function startOid4VcComposeStack() { let baseUrl = process.env.NMSHD_TEST_BASEURL!; let addressGenerationHostnameOverride: string | undefined;