Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ProposeAttributeRequestItem,
ReadAttributeRequestItem,
ShareAttributeRequestItem,
ShareAuthorizationRequestRequestItem,
ShareCredentialOfferRequestItem,
TransferFileOwnershipRequestItem
} from "@nmshd/content";
Expand Down Expand Up @@ -42,6 +43,7 @@ import {
RequestItemProcessorRegistry,
SettingsController,
ShareAttributeRequestItemProcessor,
ShareAuthorizationRequestRequestItemProcessor,
ShareCredentialOfferRequestItemProcessor,
TransferFileOwnershipRequestItemProcessor
} from "../modules";
Expand Down Expand Up @@ -163,7 +165,8 @@ export class ConsumptionController {
[AuthenticationRequestItem, GenericRequestItemProcessor],
[FormFieldRequestItem, FormFieldRequestItemProcessor],
[TransferFileOwnershipRequestItem, TransferFileOwnershipRequestItemProcessor],
[ShareCredentialOfferRequestItem, ShareCredentialOfferRequestItemProcessor]
[ShareCredentialOfferRequestItem, ShareCredentialOfferRequestItemProcessor],
[ShareAuthorizationRequestRequestItem, ShareAuthorizationRequestRequestItemProcessor]
]);
}

Expand Down
1 change: 1 addition & 0 deletions packages/consumption/src/modules/requests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ShareAuthorizationRequestRequestItem> {
public override async canAccept(
requestItem: ShareAuthorizationRequestRequestItem,
_params: AcceptRequestItemParametersJSON,
_requestInfo: LocalRequestInfo
): Promise<ValidationResult> {
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<AcceptResponseItem> {
const resolvedAuthorizationRequest = await this.consumptionController.openId4Vc.resolveAuthorizationRequest(requestItem.authorizationRequestUrl);
await this.consumptionController.openId4Vc.acceptAuthorizationRequest(resolvedAuthorizationRequest.authorizationRequest);

return AcceptResponseItem.from({ result: ResponseItemResult.Accepted });
}
}
15 changes: 11 additions & 4 deletions packages/content/src/requests/RequestItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
IProposeAttributeRequestItem,
IReadAttributeRequestItem,
IShareAttributeRequestItem,
IShareAuthorizationRequestRequestItem,
IShareCredentialOfferRequestItem,
ITransferFileOwnershipRequestItem,
ProposeAttributeRequestItem,
Expand All @@ -27,6 +28,8 @@ import {
ReadAttributeRequestItemJSON,
ShareAttributeRequestItem,
ShareAttributeRequestItemJSON,
ShareAuthorizationRequestRequestItem,
ShareAuthorizationRequestRequestItemJSON,
ShareCredentialOfferRequestItem,
ShareCredentialOfferRequestItemJSON,
TransferFileOwnershipRequestItem,
Expand Down Expand Up @@ -65,7 +68,8 @@ export type RequestItemJSONDerivations =
| AuthenticationRequestItemJSON
| FormFieldRequestItemJSON
| TransferFileOwnershipRequestItemJSON
| ShareCredentialOfferRequestItemJSON;
| ShareCredentialOfferRequestItemJSON
| ShareAuthorizationRequestRequestItemJSON;

export interface IRequestItem extends ISerializable {
/**
Expand Down Expand Up @@ -99,7 +103,8 @@ export type IRequestItemDerivations =
| IAuthenticationRequestItem
| IFormFieldRequestItem
| ITransferFileOwnershipRequestItem
| IShareCredentialOfferRequestItem;
| IShareCredentialOfferRequestItem
| IShareAuthorizationRequestRequestItem;

export abstract class RequestItem extends Serializable {
@serialize()
Expand Down Expand Up @@ -130,7 +135,8 @@ export type RequestItemDerivations =
| AuthenticationRequestItem
| FormFieldRequestItem
| TransferFileOwnershipRequestItem
| ShareCredentialOfferRequestItem;
| ShareCredentialOfferRequestItem
| ShareAuthorizationRequestRequestItem;

export function isRequestItemDerivation(input: any): input is RequestItemDerivations {
return (
Expand All @@ -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"
);
}
1 change: 1 addition & 0 deletions packages/content/src/requests/items/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
@@ -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, "@type"> | ShareAuthorizationRequestRequestItemJSON
): ShareAuthorizationRequestRequestItem {
return this.fromAny(value);
}

public override toJSON(verbose?: boolean | undefined, serializeAsString?: boolean | undefined): ShareAuthorizationRequestRequestItemJSON {
return super.toJSON(verbose, serializeAsString) as ShareAuthorizationRequestRequestItemJSON;
}
}
20 changes: 20 additions & 0 deletions packages/runtime/src/dataViews/DataViewExpander.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
ResponseJSON,
SexJSON,
ShareAttributeRequestItemJSON,
ShareAuthorizationRequestRequestItemJSON,
ShareCredentialOfferRequestItemJSON,
SurnameJSON,
ThirdPartyRelationshipAttributeQueryJSON,
Expand Down Expand Up @@ -133,6 +134,7 @@ import {
ResponseItemDVO,
ResponseItemGroupDVO,
ShareAttributeRequestItemDVO,
ShareAuthorizationRequestRequestItemDVO,
ShareCredentialOfferRequestItemDVO,
ThirdPartyRelationshipAttributeQueryDVO,
TransferFileOwnershipAcceptResponseItemDVO,
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions packages/runtime/src/dataViews/content/RequestItemDVOs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,9 @@ export interface ShareCredentialOfferRequestItemDVO extends RequestItemDVO {
credentialOfferUrl: string;
credentialResponses?: OpenId4VciCredentialResponseJSON[];
}

export interface ShareAuthorizationRequestRequestItemDVO extends RequestItemDVO {
type: "ShareAuthorizationRequestRequestItemDVO";
authorizationRequestUrl: string;
matchingCredentials?: LocalAttributeDVO[];
}
60 changes: 60 additions & 0 deletions packages/runtime/test/consumption/openid4vc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,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;
Expand Down
Loading