Skip to content

Commit

Permalink
Split issuance process. Added data type validation.
Browse files Browse the repository at this point in the history
  • Loading branch information
moratori committed Jun 16, 2024
1 parent 84e6e48 commit 30a33a5
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 77 deletions.
208 changes: 134 additions & 74 deletions src/oid4vci/credentialEndpoint/CredentialIssuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@ import {
IssueResult,
ProofOfPossession,
} from "./types";
import { HttpRequest } from "../types/types.js";
import {
CredentialRequest,
CredentialRequestJwtVcJson,
CredentialRequestVcSdJwt,
HttpRequest,
} from "../types/types.js";
import { authenticate } from "./authenticate.js";
import { validateProof } from "./validateProof.js";
import { Result } from "../../types.js";
import { toError } from "../utils.js";
import authStore from "../../store/authStore.js";
import {
credentialRequestJwtVcJsonValidator,
credentialRequestValidator,
credentialRequestVcSdJwtValidator,
} from "../types/validator.js";

const UNEXPECTED_ERROR = "unexpected_error";
const INVALID_REQUEST = "invalid_request";
Expand All @@ -18,43 +28,51 @@ const UNSUPPORTED_CREDENTIAL_FORMAT = "unsupported_credential_format";
export class CredentialIssuer<T> {
// eslint-disable-next-line no-unused-vars
constructor(private config: CredentialIssuerConfig<T>) {}
async issue(credentialRequest: HttpRequest): Promise<IssueResult> {
async issue(httpRequest: HttpRequest): Promise<IssueResult> {
/*
https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-error-response
invalid_request:
- Credential Request was malformed. One or more of the parameters (i.e. format, proof) are missing or malformed.
*/
const authResult = await authenticate(
credentialRequest.getHeader("Authorization"),
httpRequest.getHeader("Authorization"),
this.config.accessTokenStateProvider,
);
if (!authResult.ok) {
const { ok, error } = authResult;
return { ok, error: { status: 401, payload: error } };
}

const body = credentialRequest.getBody();
if (!body) {
const credentialRequest = (() => {
try {
return credentialRequestValidator(httpRequest.getBody);
} catch (e) {
return undefined;
}
})();

if (!credentialRequest) {
const error = toError(INVALID_REQUEST, "Invalid data received!");
return { ok: false, error: { status: 400, payload: error } };
}
if (!body.format) {

if (!credentialRequest.format) {
const error = toError(INVALID_REQUEST, "Missing or malformed format");
return { ok: false, error: { status: 400, payload: error } };
}

const { authorizedCode } = authResult.payload;
let proofOfPossession = undefined;
if (authorizedCode.proofElements) {
if (!body.proof) {
if (!credentialRequest.proof) {
const error = toError(INVALID_REQUEST, "Missing or malformed format");
return { ok: false, error: { status: 400, payload: error } };
}
// proof: OPTIONAL. JSON object containing proof of possession of the key material the issued Credential shall be bound to.
const checkFlow = await authStore.getAuthCode(authorizedCode.code);
const validateProofResult = await validateProof(
body.proof,
credentialRequest.proof,
this.config.credentialIssuer,
authorizedCode.proofElements,
{
Expand All @@ -70,7 +88,7 @@ export class CredentialIssuer<T> {
proofOfPossession = validateProofResult.payload;
}
const issueResult = await this._issue(
body,
credentialRequest,
authorizedCode.code,
proofOfPossession,
);
Expand All @@ -88,21 +106,92 @@ export class CredentialIssuer<T> {
return {
ok: true,
payload: {
format: body.format,
format: credentialRequest.format,
credential: issueResult.payload,
nonce: { nonce, expiresIn },
},
};
} else {
return {
ok: true,
payload: { format: body.format, credential: issueResult.payload },
payload: {
format: credentialRequest.format,
credential: issueResult.payload,
},
};
}
}

async _issueJwtVcJson(
credentialRequest: CredentialRequestJwtVcJson,
preAuthorizedCode: string,
proofOfPossession?: ProofOfPossession,
): Promise<Result<string, ErrorPayloadWithStatusCode>> {
if (!credentialRequest.credential_definition) {
const error = toError(
INVALID_REQUEST,
"credential_definition is REQUIRED when the format parameter is present in the Credential Request",
);
return { ok: false, error: { status: 400, payload: error } };
}
const { type, credentialSubject } = credentialRequest.credential_definition;
if (!type || !credentialSubject) {
const error = toError(
INVALID_REQUEST,
"The payload needs types and credentialSubject",
);
return { ok: false, error: { status: 400, payload: error } };
}
if (!this.config.issuingExecutor.jwtVcJson) {
const error = toError(
UNEXPECTED_ERROR,
"No issuing function is provided",
);
return { ok: false, error: { status: 500, payload: error } };
}
const result = await this.config.issuingExecutor.jwtVcJson(
preAuthorizedCode,
credentialRequest,
proofOfPossession,
);
if (result.ok) {
return result;
} else {
return { ok: false, error: { status: 500, payload: result.error } };
}
}

async _issueVcSdJwt(
credentialRequest: CredentialRequestVcSdJwt,
preAuthorizedCode: string,
proofOfPossession?: ProofOfPossession,
): Promise<Result<string, ErrorPayloadWithStatusCode>> {
const vct = credentialRequest.vct;
if (!vct) {
const error = toError(INVALID_REQUEST, "The payload needs vct");
return { ok: false, error: { status: 400, payload: error } };
}
if (!this.config.issuingExecutor.sdJwtVc) {
const error = toError(
UNEXPECTED_ERROR,
"No issuing function is provided",
);
return { ok: false, error: { status: 500, payload: error } };
}
const result = await this.config.issuingExecutor.sdJwtVc(
preAuthorizedCode,
credentialRequest,
proofOfPossession,
);
if (result.ok) {
return result;
} else {
return { ok: false, error: { status: 500, payload: result.error } };
}
}

async _issue(
body: any,
credentialRequest: CredentialRequest,
preAuthorizedCode: string,
proofOfPossession?: ProofOfPossession,
): Promise<Result<string, ErrorPayloadWithStatusCode>> {
Expand All @@ -118,72 +207,43 @@ export class CredentialIssuer<T> {
unsupported_credential_format:
- requested credential format is not supported
*/
if (body.format === "jwt_vc_json") {
// OpenID for Verifiable Credential Issuance
// E.1.1. VC Signed as a JWT, Not Using JSON-LD
// E.1.1.5. Credential Request
//
// - credential_definition: REQUIRED.
// It consists at least of the following sub claims:
//
// - type: REQUIRED.
// - credentialSubject: OPTIONAL.
//
const { type, credentialSubject } = body.credential_definition;
if (!type || !credentialSubject) {
const error = toError(
INVALID_REQUEST,
"The payload needs types and credentialSubject",
);
return { ok: false, error: { status: 400, payload: error } };
}
if (!this.config.issuingExecutor.jwtVcJson) {
const error = toError(
UNEXPECTED_ERROR,
"No issuing function is provided",
);
return { ok: false, error: { status: 500, payload: error } };
}
const result = await this.config.issuingExecutor.jwtVcJson(
preAuthorizedCode,
body,
proofOfPossession,
);
if (result.ok) {
return result;
} else {
return { ok: false, error: { status: 500, payload: result.error } };
}
} else if (body.format === "vc+sd-jwt") {
const { vct } = body.credential_definition;
if (!vct) {
const error = toError(INVALID_REQUEST, "The payload needs vct");
return { ok: false, error: { status: 400, payload: error } };
}

if (!this.config.issuingExecutor.sdJwtVc) {
const error = toError(
UNEXPECTED_ERROR,
"No issuing function is provided",
const unsupportedFormatError: Result<string, ErrorPayloadWithStatusCode> = {
ok: false,
error: {
status: 400,
payload: toError(
UNSUPPORTED_CREDENTIAL_FORMAT,
"Unsupported Credential Format",
),
},
};

switch (credentialRequest.format) {
case "jwt_vc_json": {
const jwtVcJsonRequest =
credentialRequestJwtVcJsonValidator(credentialRequest);
return this._issueJwtVcJson(
jwtVcJsonRequest,
preAuthorizedCode,
proofOfPossession,
);
return { ok: false, error: { status: 500, payload: error } };
}
const result = await this.config.issuingExecutor.sdJwtVc(
preAuthorizedCode,
body,
proofOfPossession,
);
if (result.ok) {
return result;
} else {
return { ok: false, error: { status: 500, payload: result.error } };
case "vc+sd-jwt": {
const vcSdJwtRequest =
credentialRequestVcSdJwtValidator(credentialRequest);
return this._issueVcSdJwt(
vcSdJwtRequest,
preAuthorizedCode,
proofOfPossession,
);
}
} else {
const error = toError(
UNSUPPORTED_CREDENTIAL_FORMAT,
"Unsupported Credential Format",
);
return { ok: false, error: { status: 400, payload: error } };
case "ldp_vc":
return unsupportedFormatError;
case "jwt_vc_json-ld":
return unsupportedFormatError;
default:
return unsupportedFormatError;
}
}
}
11 changes: 8 additions & 3 deletions src/oid4vci/credentialEndpoint/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ErrorPayload, Result } from "../../types.js";
import { Exists, NotExists } from "../types/types.js";
import {
CredentialRequestJwtVcJson,
CredentialRequestVcSdJwt,
Exists,
NotExists,
} from "../types/types.js";
import * as jose from "jose";

export interface ValidAccessTokenState<T> {
Expand Down Expand Up @@ -119,7 +124,7 @@ export type AccessTokenStateProvider<T> = (
*/
export type IssueJwtVcJsonCredential = (
preAuthorizedCode: string,
payload: PayloadJwtVc,
payload: CredentialRequestJwtVcJson,
proofOfPossession?: ProofOfPossession,
) => Promise<Result<string, ErrorPayload>>;

Expand All @@ -139,7 +144,7 @@ export type IssueJwtVcJsonCredential = (
*/
export type IssueSdJwtVcCredential = (
preAuthorizedCode: string,
payload: PayloadSdJwtVc,
payload: CredentialRequestVcSdJwt,
proofOfPossession?: ProofOfPossession,
) => Promise<Result<string, ErrorPayload>>;

Expand Down

0 comments on commit 30a33a5

Please sign in to comment.