Skip to content
This repository has been archived by the owner on Apr 11, 2023. It is now read-only.

feat: implemented OIDC presentation submission API #453

Merged
merged 1 commit into from
Nov 8, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions cmd/wallet-js-sdk/src/oidc/presentation/openid4vp.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import { decode } from "js-base64";
import { CredentialManager, DIDManager } from "@";
import * as jose from "jose";

// TODO: replace mock function with an actual JWT signer once implemented
async function signJWT({ header, payload }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("mock.signed.jwt");
}, 300);
});
}

/**
* OpenID4VP module is the oidc client that provides APIs for OIDC4VP flows.
*
Expand Down Expand Up @@ -120,6 +129,184 @@ export class OpenID4VP {
}
});

this.client_id = payload.client_id;
this.nonce = payload.nonce;
this.redirect_uri = payload.redirect_uri;

return response.results;
}

/**
* submitOIDCPresentation performs an OIDC presentation submission
* @param {string} kid - consumer's verification method's kid.
* @param {Object} presentationQuery - presentation query object retrieved from user's wallet.
* @param {string} issuer - the issuer's key id.
* @param {number} expiry - time in seconds representing the expiry of the presentation.
*
*
* @returns {Promise<Object>} - empty promise or error if operation fails.
*/
async submitOIDCPresentation(kid, presentationQuery, issuer, expiry) {
if (!kid) {
throw new TypeError(
"Error submitting OpenID4VP presentation: kid cannot be empty"
);
} else if (!presentationQuery) {
throw new TypeError(
"Error submitting OpenID4VP presentation: presentationQuery cannot be empty"
);
} else if (!presentationQuery.presentation_submission) {
throw new TypeError(
"Error submitting OpenID4VP presentation: presentation_submission is missing"
);
} else if (!presentationQuery.type) {
throw new TypeError(
"Error submitting OpenID4VP presentation: type is missing"
);
} else if (!presentationQuery.verifiableCredential) {
throw new TypeError(
"Error submitting OpenID4VP presentation: verifiableCredential is missing"
);
} else if (!issuer) {
throw new TypeError(
"Error submitting OpenID4VP presentation: issuer is missing"
);
} else if (!expiry) {
sudeshrshetty marked this conversation as resolved.
Show resolved Hide resolved
throw new TypeError(
"Error submitting OpenID4VP presentation: expiry is missing"
);
}

const header = new Object();
Object.defineProperty(header, "alg", {
value: alg,
});
Object.defineProperty(header, "kid", {
value: kid,
});
Object.defineProperty(header, "typ", {
value: "JWT",
});

const idToken = await generateIdToken(
kid,
presentationQuery.presentation_submission,
header,
expiry
);

const vp = new Object();
Object.defineProperty(vp, "@context", {
value: presentationQuery["@context"],
});
Object.defineProperty(vp, "type", {
value: presentationQuery.type,
});
Object.defineProperty(vp, "verifiableCredential", {
// TODO: encode and sign with JWT
value: presentationQuery.verifiableCredential,
sudeshrshetty marked this conversation as resolved.
Show resolved Hide resolved
});

const vpToken = await generateVpToken(kid, header, vp, expiry);

const authRequest = new URLSearchParams();
authRequest.append("id_token", idToken);
authRequest.append("vp_token", vpToken);

return await axios.post(this.redirect_uri, authRequest).catch((e) => {
throw new Error("Error submitting OIDC presentation:", e);
});
}
}

/**
* generateIdToken generates an ID Token for the presentation submission request
* @param {string} kid - consumer's verification method's kid.
* @param {Object} presentationSubmission - presentation submission.
* @param {Object} header - header for the ID Token.
* @param {number} expiry - time in seconds representing the expiry of the token.
*
*
* @returns {Promise<string>} - a promise resolving with a string containing ID Token or an error.
*/
async function generateIdToken(kid, presentationSubmission, header, expiry) {
if (!kid) {
throw new TypeError("Error generating ID Token: kid cannot be empty");
} else if (!presentationSubmission) {
throw new TypeError(
"Error generating ID Token: presentationSubmission cannot be empty"
);
} else if (!header) {
throw new TypeError("Error generating ID Token: header cannot be empty");
} else if (!expiry) {
throw new TypeError("Error generating ID Token: expiry cannot be empty");
}

const vpToken = new Object();
Object.defineProperty(vpToken, "presentation_submission", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious about this syntax,

Why can't

``
var vpToken = {}
vpToken. presentation_submission = presentationSubmission

``

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way, the properties inside the vpToken object are not modifiable

Copy link
Member

@sudeshrshetty sudeshrshetty Nov 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they are local fields, simply making them var type doesn't hurt.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot use this syntax to create property @context on the object, which is required for https://github.com/trustbloc/agent-sdk/pull/453/files#diff-73c781567add49e4fe09e28cea2e4f4530d4999259ce327f63a89dc043d3d68dR199

I figured it'd be better to be consistent with the approach I take

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vpToken["@context"] = ctx

value: presentationSubmission,
});

const payload = new Object();
Object.defineProperty(payload, "sub", {
value: kid.split("#")[0],
});
Object.defineProperty(payload, "nonce", {
value: this.nonce,
});
Object.defineProperty(payload, "_vp_token", {
value: vpToken,
});
Object.defineProperty(payload, "aud", {
value: this.client_id,
});
Object.defineProperty(payload, "iss", {
value: "https://self-issued.me/v2/openid-vc",
});
Object.defineProperty(payload, "exp", {
value: expiry,
});

return await signJWT({ header, payload });
}

/**
* generateVpToken generates a VP Token for the presentation submission request
* @param {string} kid - consumer's verification method's kid.
* @param {Object} vp - object containing details for the presentation.
* @param {Object} header - header for the VP Token.
* @param {number} expiry - time in seconds representing the expiry of the token.
*
*
* @returns {Promise<string>} - a promise resolving with a string containing ID Token or an error.
*/
async function generateVpToken(kid, vp, header, expiry) {
if (!kid) {
throw new TypeError("Error generating VP Token: kid cannot be empty");
} else if (!vp) {
throw new TypeError("Error generating VP Token: vp cannot be empty");
} else if (!header) {
throw new TypeError("Error generating VP Token: header cannot be empty");
} else if (!expiry) {
throw new TypeError("Error generating VP Token: expiry cannot be empty");
}

const payload = new Object();
Object.defineProperty(payload, "nonce", {
value: this.nonce,
});
Object.defineProperty(payload, "vp", {
value: vp,
});
Object.defineProperty(payload, "aud", {
value: this.client_id,
});
Object.defineProperty(payload, "iss", {
value: kid.split("#")[0],
});
Object.defineProperty(payload, "exp", {
value: expiry,
});

return await signJWT({ header, payload });
}