From 27e58dc4ad3d9f261e24852e6ee6960f5b9d3962 Mon Sep 17 00:00:00 2001 From: Anton Biriukov Date: Fri, 4 Nov 2022 15:00:20 +0100 Subject: [PATCH] feat: implemented OIDC presentation submission API Signed-off-by: Anton Biriukov --- .../src/oidc/presentation/openid4vp.js | 175 +++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/cmd/wallet-js-sdk/src/oidc/presentation/openid4vp.js b/cmd/wallet-js-sdk/src/oidc/presentation/openid4vp.js index c1676518..64c4714d 100644 --- a/cmd/wallet-js-sdk/src/oidc/presentation/openid4vp.js +++ b/cmd/wallet-js-sdk/src/oidc/presentation/openid4vp.js @@ -6,9 +6,13 @@ SPDX-License-Identifier: Apache-2.0 */ import axios from "axios"; -import { decode } from "js-base64"; +import { decode, encode } from "js-base64"; import { CredentialManager, DIDManager } from "@"; import * as jose from "jose"; +import { generateNonce } from "@/util/helper"; + +const ID_TOKEN_EXPIRY = 10 * 60; // 10 minutes +const VP_TOKEN_EXPIRY = 10 * 60; // 10 minutes /** * OpenID4VP module is the oidc client that provides APIs for OIDC4VP flows. @@ -120,6 +124,175 @@ export class OpenID4VP { } }); + this.client_id = payload.client_id; + this.nonce = payload.nonce; + this.redirect_uri = payload.redirect_uri; + return response.results; } + + async submitOIDCPresentation( + presentationQuery, + sign = async function (iss, aud, iat, nonce) { + return ""; + } + ) { + 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" + ); + } + + const header = new Object(); + Object.defineProperty(header, "alg", { + value: alg, + }); + Object.defineProperty(header, "kid", { + // TODO: clarify where we get this param from + value: kid, + }); + Object.defineProperty(header, "typ", { + value: "JWT", + }); + + const encodedHeader = encode(JSON.stringify(header), true); + + const id_token = await generateIdToken( + presentationQuery.presentation_submission, + encodedHeader, + (sign = async function (iss, aud, iat, nonce) { + return ""; + }) + ); + + const vp = new Object(); + Object.defineProperty(payload, "@context", { + value: presentationQuery["@context"], + }); + Object.defineProperty(payload, "type", { + value: presentationQuery.type, + }); + Object.defineProperty(payload, "verifiableCredential", { + value: presentationQuery.verifiableCredential, + }); + + const vp_token = await generateVpToken( + encodedHeader, + vp, + (sign = async function (iss, aud, iat, nonce) { + return ""; + }) + ); + + const authRequest = new URLSearchParams(); + authRequest.append("id_token", id_token); + authRequest.append("vp_token", vp_token); + + return await axios.post(this.redirect_uri, authRequest).catch((e) => { + throw new Error("Error submitting OIDC presentation:", e); + }); + } +} + +async function generateIdToken( + presentation_submission, + encodedHeader, + sign = async function (iss, aud, iat, nonce) { + return ""; + } +) { + if (!presentation_submission) { + throw new TypeError( + "Error generating ID Token: presentation_submission is be empty" + ); + } + + const vpToken = new Object(); + Object.defineProperty(vpToken, "presentation_submission", { + value: presentation_submission, + }); + + const iat = new Date().getTime() / 1000; + + const payload = new Object(); + // TODO: dynamically pass sub + Object.defineProperty(payload, "sub", { + value: "sub", + }); + 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: iss, + }); + Object.defineProperty(payload, "iat", { + value: iat, + }); + Object.defineProperty(payload, "exp", { + value: iat + ID_TOKEN_EXPIRY, + }); + + const encodedPayload = encode(JSON.stringify(payload), true); + + const signature = await sign(kid, this.client_id, iat, this.nonce); + + return `${encodedHeader}.${encodedPayload}.${signature}`; +} + +async function generateVpToken( + encodedHeader, + vp, + sign = async function (iss, aud, iat, nonce) { + return ""; + } +) { + const iat = new Date().getTime() / 1000; + const payload = new Object(); + Object.defineProperty(payload, "nonce", { + value: this.nonce, + }); + Object.defineProperty(payload, "vp", { + value: vp, + }); + Object.defineProperty(payload, "aud", { + // TODO: clarify where we get this param from + value: "", + }); + Object.defineProperty(payload, "iss", { + // TODO: clarify where we get this param from + value: "", + }); + Object.defineProperty(payload, "iat", { + value: iat, + }); + Object.defineProperty(payload, "nbf", { + value: iat, + }); + Object.defineProperty(payload, "exp", { + value: iat + VP_TOKEN_EXPIRY, + }); + + const encodedPayload = encode(JSON.stringify(payload), true); + + const signature = await sign(kid, this.client_id, iat, this.nonce); + + return `${encodedHeader}.${encodedPayload}.${signature}`; }