Skip to content

Commit

Permalink
Merge branch 'prevent-duplicate-credential' into stable-userid
Browse files Browse the repository at this point in the history
  • Loading branch information
emlun committed Sep 6, 2023
2 parents 7e0353d + ae339b1 commit b090fa0
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 87 deletions.
12 changes: 9 additions & 3 deletions src/routers/issuance.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import express, { Router } from 'express';
import { AuthMiddleware } from '../middlewares/auth.middleware';
import _ from 'lodash';
import { appContainer } from '../services/inversify.config';
import { OpenidCredentialReceiving } from '../services/interfaces';
import { IssuanceErr, OpenidCredentialReceiving } from '../services/interfaces';
import { TYPES } from '../services/types';


Expand Down Expand Up @@ -57,8 +57,14 @@ issuanceRouter.post('/handle/authorization/response', async (req, res) => {
if (!(new URL(authorization_response_url).searchParams.get("code"))) {
return res.status(500).send({});
}
await openidForCredentialIssuanceService.handleAuthorizationResponse(req.user.did, authorization_response_url);
res.send({});
const result = await openidForCredentialIssuanceService.handleAuthorizationResponse(req.user.did, authorization_response_url);
if (result.ok) {
res.send({});
} else if (result.val === IssuanceErr.STATE_NOT_FOUND) {
res.status(404).send({});
} else {
res.status(500).send({});
}
}
catch(err) {
res.status(500).send({ error: "Failed to handle authorization response" });
Expand Down
14 changes: 12 additions & 2 deletions src/routers/presentation.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ presentationRouter.post('/handle/authorization/request', async (req, res) => {
} = req.body;

try{
const outboundRequest = await openidForPresentationService.handleRequest(req.user.did, authorization_request)
const outboundRequestResult = await openidForPresentationService.handleRequest(req.user.did, authorization_request);
if (!outboundRequestResult.ok) {
return res.status(500).send({});
}

const outboundRequest = outboundRequestResult.val;
if (outboundRequest.conformantCredentialsMap && outboundRequest.verifierDomainName) {
const { conformantCredentialsMap, verifierDomainName } = outboundRequest;
// convert from map to JSON
Expand Down Expand Up @@ -54,7 +59,12 @@ presentationRouter.post('/generate/authorization/response', async (req, res) =>

const selection = new Map(Object.entries(verifiable_credentials_map)) as Map<string, string>;
try {
const { redirect_to, error } = await openidForPresentationService.sendResponse(req.user.did, selection);
const result = await openidForPresentationService.sendResponse(req.user.did, selection);
if (!result.ok) {
return res.status(500).send({});
}

const { redirect_to, error } = result.val;
if (error) {
const errText = `Error generating authorization response: ${error}`;
console.error(errText);
Expand Down
48 changes: 29 additions & 19 deletions src/services/DatabaseKeystoreService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { SignJWT, importJWK } from "jose";
import { AdditionalKeystoreParameters, WalletKeystore } from "./interfaces";
import { getUserByDID } from "../entities/user.entity";
import { SignVerifiablePresentationJWT, WalletKey } from "@gunet/ssi-sdk";
import { randomUUID } from "crypto";
import { verifiablePresentationSchemaURL } from "../util/util";
import { injectable } from "inversify";
import "reflect-metadata";
import { Err, Ok, Result } from "ts-results";

import { SignVerifiablePresentationJWT, WalletKey } from "@gunet/ssi-sdk";
import { AdditionalKeystoreParameters, WalletKeystore, WalletKeystoreErr } from "./interfaces";
import { verifiablePresentationSchemaURL } from "../util/util";
import { getUserByDID } from "../entities/user.entity";


@injectable()
Expand All @@ -16,13 +18,15 @@ export class DatabaseKeystoreService implements WalletKeystore {

constructor() { }

async createIdToken(userDid: string, nonce: string, audience: string, additionalParameters: AdditionalKeystoreParameters): Promise<{ id_token: string; }> {

async createIdToken(userDid: string, nonce: string, audience: string, additionalParameters: AdditionalKeystoreParameters): Promise<Result<{ id_token: string; }, WalletKeystoreErr>> {
const user = (await getUserByDID(userDid)).unwrap();

const keys = JSON.parse(user.keys.toString()) as WalletKey;
const privateKey = await importJWK(keys.privateKey, keys.alg);

if (!keys.privateKey) {
return Err(WalletKeystoreErr.KEYS_UNAVAILABLE);
}

const privateKey = await importJWK(keys.privateKey, keys.alg);
const jws = await new SignJWT({ nonce: nonce })
.setProtectedHeader({
alg: keys.alg,
Expand All @@ -36,14 +40,18 @@ export class DatabaseKeystoreService implements WalletKeystore {
.setIssuedAt()
.sign(privateKey);

return { id_token: jws };
return Ok({ id_token: jws });
}

async signJwtPresentation(userDid: string, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters: AdditionalKeystoreParameters): Promise<{ vpjwt: string }> {
async signJwtPresentation(userDid: string, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters: AdditionalKeystoreParameters): Promise<Result<{ vpjwt: string }, WalletKeystoreErr>> {
const user = (await getUserByDID(userDid)).unwrap();
const keys = JSON.parse(user.keys.toString()) as WalletKey;
const privateKey = await importJWK(keys.privateKey, keys.alg);

if (!keys.privateKey) {
return Err(WalletKeystoreErr.KEYS_UNAVAILABLE);
}

const privateKey = await importJWK(keys.privateKey, keys.alg);
const jws = await new SignVerifiablePresentationJWT()
.setProtectedHeader({
alg: keys.alg,
Expand All @@ -55,7 +63,7 @@ export class DatabaseKeystoreService implements WalletKeystore {
.setType(["VerifiablePresentation"])
.setAudience(audience)
.setCredentialSchema(
verifiablePresentationSchemaURL,
verifiablePresentationSchemaURL,
"FullJsonSchemaValidator2021")
.setIssuer(user.did)
.setSubject(user.did)
Expand All @@ -65,30 +73,32 @@ export class DatabaseKeystoreService implements WalletKeystore {
.setIssuedAt()
.setExpirationTime('1m')
.sign(privateKey);
return { vpjwt: jws };
return Ok({ vpjwt: jws });
}

async generateOpenid4vciProof(userDid: string, audience: string, nonce: string, additionalParameters: AdditionalKeystoreParameters): Promise<{ proof_jwt: string }> {

async generateOpenid4vciProof(userDid: string, audience: string, nonce: string, additionalParameters: AdditionalKeystoreParameters): Promise<Result<{ proof_jwt: string }, WalletKeystoreErr>> {
const user = (await getUserByDID(userDid)).unwrap();

const keys = JSON.parse(user.keys.toString()) as WalletKey;

if (!keys.privateKey) {
return Err(WalletKeystoreErr.KEYS_UNAVAILABLE);
}

const privateKey = await importJWK(keys.privateKey, keys.alg);
const header = {
alg: keys.alg,
typ: "openid4vci-proof+jwt",
kid: keys.did + "#" + keys.did.split(":")[2]
};

const jws = await new SignJWT({ nonce: nonce })
.setProtectedHeader(header)
.setIssuedAt()
.setIssuer(user.did)
.setAudience(audience)
.setExpirationTime('1m')
.sign(privateKey);
return { proof_jwt: jws };

return Ok({ proof_jwt: jws });
}

}
71 changes: 42 additions & 29 deletions src/services/OpenidForCredentialIssuanceService.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import axios from "axios";
import * as _ from 'lodash';
import base64url from "base64url";
import qs from "qs";
import { injectable, inject } from "inversify";
import "reflect-metadata";
import { Err, Ok, Result } from "ts-results";

import { LegalPersonEntity, getLegalPersonByDID, getLegalPersonByUrl } from "../entities/LegalPerson.entity";
import { CredentialIssuerMetadata, CredentialResponseSchemaType, CredentialSupportedJwtVcJson, GrantType, OpenidConfiguration, TokenResponseSchemaType, VerifiableCredentialFormat } from "../types/oid4vci";
import config from "../../config";
import { getUserByDID } from "../entities/user.entity";
import { sendPushNotification } from "../lib/firebase";
import * as _ from 'lodash';
import { generateCodeChallengeFromVerifier, generateCodeVerifier } from "../util/util";
import base64url from "base64url";
import { createVerifiableCredential } from "../entities/VerifiableCredential.entity";
import { getLeafNodesWithPath } from "../lib/leafnodepaths";
import qs from "qs";
import { TYPES } from "./types";
import { OpenidCredentialReceiving, WalletKeystore } from "./interfaces";
import { injectable, inject } from "inversify";
import "reflect-metadata";
import { IssuanceErr, OpenidCredentialReceiving, WalletKeystore, WalletKeystoreErr } from "./interfaces";


type IssuanceState = {
Expand Down Expand Up @@ -223,23 +225,28 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei
* @param authorizationResponseURL
* @throws
*/
public async handleAuthorizationResponse(userDid: string, authorizationResponseURL: string): Promise<void> {
public async handleAuthorizationResponse(userDid: string, authorizationResponseURL: string): Promise<Result<void, IssuanceErr | void>> {
const currentState = this.states.get(userDid);
if (!currentState) {
return Err(IssuanceErr.STATE_NOT_FOUND);
}

const url = new URL(authorizationResponseURL);
const code = url.searchParams.get('code');
if (!code) {
throw new Error("Code not received");
}
const currentState = this.states.get(userDid);
let newState = { ...currentState, code };
this.states.set(userDid, newState);

this.tokenRequest(newState).then(tokenResponse => {
newState = { ...newState, tokenResponse }
this.states.set(userDid, newState);
this.credentialRequests(userDid, newState).catch(e => {
console.error("Credential requests failed with error : ", e)
});
})
const tokenResponse = await this.tokenRequest(newState);
newState = { ...newState, tokenResponse }
this.states.set(userDid, newState);
try {
return await this.credentialRequests(userDid, newState);
} catch (e) {
console.error("Credential requests failed with error : ", e)
}
}


Expand Down Expand Up @@ -312,17 +319,22 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei
/**
* @throws
*/
private async credentialRequests(userDid: string, state: IssuanceState) {

private async credentialRequests(userDid: string, state: IssuanceState): Promise<Result<void, void>> {
console.log("State = ", state)
const httpHeader = {

const httpHeader = {
"authorization": `Bearer ${state.tokenResponse.access_token}`,
};

const c_nonce = state.tokenResponse.c_nonce;
const res = await this.walletKeyStore.generateOpenid4vciProof(userDid, state.credentialIssuerMetadata.credential_issuer, c_nonce);
if (!res.ok) {
if (res.val === WalletKeystoreErr.KEYS_UNAVAILABLE) {
return Err.EMPTY;
}
}

const { proof_jwt } = await this.walletKeyStore.generateOpenid4vciProof(userDid, state.credentialIssuerMetadata.credential_issuer, c_nonce);

const { proof_jwt } = res.val;
const credentialEndpoint = state.credentialIssuerMetadata.credential_endpoint;

let httpResponsePromises = state.authorization_details.map((authzDetail) => {
Expand All @@ -335,29 +347,30 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei
}
return axios.post(credentialEndpoint, httpBody, { headers: httpHeader });
})


const responses = await Promise.allSettled(httpResponsePromises);
let credentialResponses = responses
.filter(res => res.status == 'fulfilled')
.map((res) =>
.map((res) =>
res.status == "fulfilled" ? res.value.data as CredentialResponseSchemaType : null
);

// Prevent duplicate credential acceptance
this.states.delete(userDid);

for (const cr of credentialResponses) {
this.checkConstantlyForPendingCredential(state, cr.acceptance_token);
}

// remove the ones that are for deferred endpoint
credentialResponses = credentialResponses.filter((cres) => !cres.acceptance_token);

for (const response of credentialResponses) {
console.log("Response = ", response)
this.handleCredentialStorage(userDid, response);
this.handleCredentialStorage(state, response);
}
console.log("=====FINISHED OID4VCI")
return;
return Ok.EMPTY;
}

// Deferred Credential only
Expand All @@ -370,7 +383,7 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei
{},
{ headers: defferedCredentialReqHeader } )
.then((res) => {
this.handleCredentialStorage(state.userDid, res.data);
this.handleCredentialStorage(state, res.data);
})
.catch(err => {
setTimeout(() => {
Expand All @@ -381,14 +394,14 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei

}

private async handleCredentialStorage(userDid: string, credentialResponse: CredentialResponseSchemaType) {
const userRes = await getUserByDID(userDid);
private async handleCredentialStorage(state: IssuanceState, credentialResponse: CredentialResponseSchemaType) {
const userRes = await getUserByDID(state.userDid);
if (userRes.err) {
return;
}
const user = userRes.unwrap();

const { legalPerson } = this.states.get(userDid);
const { legalPerson } = state;
console.log("Legal person = ", legalPerson)
const credentialPayload = JSON.parse(base64url.decode(credentialResponse.credential.split('.')[1]))
const type = credentialPayload.vc.type as string[];
Expand Down
Loading

0 comments on commit b090fa0

Please sign in to comment.