From 7d68fc1185945ff7696e851a9f165d61b8343788 Mon Sep 17 00:00:00 2001 From: pstamatop Date: Wed, 20 Mar 2024 21:06:24 +0200 Subject: [PATCH 1/4] Fix verifier admin panel --- .../OpenidForPresentationReceivingService.ts | 18 +++- src/services/interfaces.ts | 1 + src/verifier/verifierPanelRouter.ts | 101 ++++++------------ views/verifier/definitions.pug | 7 +- views/verifier/detailed-presentation.pug | 41 +++++-- views/verifier/presentations.pug | 20 ++-- views/verifier/public_definitions.pug | 2 +- 7 files changed, 97 insertions(+), 93 deletions(-) diff --git a/src/services/OpenidForPresentationReceivingService.ts b/src/services/OpenidForPresentationReceivingService.ts index 6a9ae60..66d54b3 100644 --- a/src/services/OpenidForPresentationReceivingService.ts +++ b/src/services/OpenidForPresentationReceivingService.ts @@ -298,14 +298,13 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta // store presentation const newVerifiablePresentation = new VerifiablePresentationEntity() - newVerifiablePresentation.presentation_definition_id = presentation_submission.definition_id; + newVerifiablePresentation.presentation_definition_id = (JSON.parse(presentation_submission) as any).definition_id; newVerifiablePresentation.claims = presentationClaims ?? null; newVerifiablePresentation.status = true; newVerifiablePresentation.raw_presentation = vp_token; newVerifiablePresentation.presentation_submission = presentationSubmissionObject; newVerifiablePresentation.date = new Date(); newVerifiablePresentation.state = verifierStateId as string; - this.verifiablePresentationRepository.save(newVerifiablePresentation); console.error(msg); @@ -479,4 +478,19 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta else return { status: false }; } + + public async getPresentationById(id: string): Promise<{ status: boolean, presentationClaims?: PresentationClaims, rawPresentation?: string }> { + const vp = await this.verifiablePresentationRepository.createQueryBuilder('vp') + .where("id = :id", { id: id }) + .getOne(); + + if (!vp?.raw_presentation || !vp.claims) { + return { status: false }; + } + + if (vp) + return { status: true, presentationClaims: vp.claims, rawPresentation: vp?.raw_presentation }; + else + return { status: false }; + } } \ No newline at end of file diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index f9b3d66..297a14f 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -38,6 +38,7 @@ export interface OpenidForPresentationsReceivingInterface { generateAuthorizationRequestURL(ctx: { req: Request, res: Response }, presentation_definition_id: string, directPostEndpoint?: string): Promise<{ url: URL; stateId: string }>; getPresentationDefinitionHandler(ctx: { req: Request, res: Response }): Promise; getPresentationByState(state: string): Promise<{ status: boolean, presentationClaims?: PresentationClaims, rawPresentation?: string }>; + getPresentationById(id: string): Promise<{ status: boolean, presentationClaims?: PresentationClaims, rawPresentation?: string }>; /** * @throws diff --git a/src/verifier/verifierPanelRouter.ts b/src/verifier/verifierPanelRouter.ts index 3a26a3d..1d7d23b 100644 --- a/src/verifier/verifierPanelRouter.ts +++ b/src/verifier/verifierPanelRouter.ts @@ -5,13 +5,11 @@ import AppDataSource from "../AppDataSource"; import { VerifiablePresentationEntity } from "../entities/VerifiablePresentation.entity"; import { appContainer } from "../services/inversify.config"; import { TYPES } from "../services/types"; -import { VerifierConfigurationInterface } from "../services/interfaces"; +import { OpenidForPresentationsReceivingInterface, VerifierConfigurationInterface } from "../services/interfaces"; import base64url from "base64url"; import locale from "../configuration/locale"; -import { VerifierConfigurationService } from "../configuration/verifier/VerifierConfigurationService"; -import { PresentationSubmission } from "@wwwallet/ssi-sdk"; -import { JSONPath } from "jsonpath-plus"; +const openidForPresentationReceivingService = appContainer.get(TYPES.OpenidForPresentationsReceivingService); const verifierPanelRouter = Router(); @@ -35,96 +33,63 @@ verifierPanelRouter.get('/', async (req, res) => { }) }) +type VerifiablePresentationWithDetails = VerifiablePresentationEntity & { holderInfo?: string, claims?: any }; verifierPanelRouter.get('/filter/by/definition/:definition_id', async (req, res) => { const definition_id = req.params.definition_id; if (!definition_id) { return res.status(500).send({ error: "No definition id was specified" }); } - const verifiablePresentations = await verifiablePresentationRepository.createQueryBuilder('vp') + let verifiablePresentations = await verifiablePresentationRepository.createQueryBuilder('vp') .where("vp.presentation_definition_id = :definition_id", { definition_id: definition_id }) .getMany(); + + const presentationsWithDetails: VerifiablePresentationWithDetails[] = verifiablePresentations.map(vp => { + try { + const decoded = vp.raw_presentation ? JSON.parse(base64url.decode(vp.raw_presentation.split('.')[1])) : null as any; + const holderInfo = decoded?.vp?.holder || "No Holder Info"; + const claims = vp.claims; + return { ...vp, holderInfo, claims } as VerifiablePresentationWithDetails; + } catch (error) { + console.error("Error decoding VP:", error); + return { ...vp, holderInfo: 'Error decoding holder info' } as VerifiablePresentationWithDetails; + } + }); + return res.render('verifier/presentations.pug', { lang: req.lang, - verifiablePresentations: verifiablePresentations, + verifiablePresentations: presentationsWithDetails, locale: locale[req.lang] }) }) - -type PresentationView = { - descriptor_mapped_value: { - inputDescriptorId: string; - vcView: VerifiableCredentialView; - }[] -} - -type VerifiableCredentialView = { - rows: { - name: string; - value: string; - }[]; -} - - verifierPanelRouter.get('/presentation/:presentation_id', async (req, res) => { const presentation_id = req.params.presentation_id; if (!presentation_id) { return res.status(500).send({ error: "No presentation_id was specified" }); } - const verifiablePresentation = await verifiablePresentationRepository.createQueryBuilder('vp') - .where("vp.id = :presentation_id", { presentation_id: presentation_id }) - .getOne(); - - if (!verifiablePresentation || !verifiablePresentation.raw_presentation) { - return res.status(400).render('error', { - msg: "Verifiable presentation not found", + const { presentationClaims, rawPresentation } = await openidForPresentationReceivingService.getPresentationById(presentation_id as string); + + if (!presentationClaims || !rawPresentation) { + return res.render('error.pug', { + msg: "Failed to get presentation", + code: 0, lang: req.lang, - locale: locale[req.lang] - }); + locale: locale[req.lang], + }) } - const presentationSubmission = verifiablePresentation.presentation_submission as PresentationSubmission; - - const payload = JSON.parse(base64url.decode(verifiablePresentation.raw_presentation?.split('.')[1])) as any; - const vcList = payload.vp.verifiableCredential as string[]; - const credentialList = []; - for (const vc of vcList) { - const vcPayload = JSON.parse(base64url.decode(vc.split('.')[1])) as any; - credentialList.push(vcPayload.vc); - } + const presentationPayload = JSON.parse(base64url.decode(rawPresentation.split('.')[1])) as any; + const credentials = presentationPayload.vp.verifiableCredential.map((vcString: any) => { + return JSON.parse(base64url.decode(vcString.split('.')[1])); + }).map((credential: any) => credential.vc); - const presentationDefinition = appContainer.resolve(VerifierConfigurationService) - .getPresentationDefinitions() - .filter(pd => - pd.id == verifiablePresentation.presentation_definition_id - )[0]; - - let view: PresentationView = { descriptor_mapped_value: [] }; - for (const descriptor of presentationDefinition.input_descriptors) { - const correspondingElementOnSubmission = presentationSubmission.descriptor_map.filter(desc => desc.id == descriptor.id)[0]; - - const vcRows = descriptor.constraints.fields.map((field) => { - const vcPath = correspondingElementOnSubmission.path; - const payload = JSON.parse(base64url.decode(verifiablePresentation.raw_presentation?.split('.')[1] as string)) as any; - const vcJwtString = JSONPath({ json: payload.vp, path: vcPath })[0]; - const vcPayload = JSON.parse(base64url.decode(vcJwtString.split('.')[1])) as any; - const valuePath = field.path[0]; - const value = JSONPath({ json: vcPayload.vc, path: valuePath })[0] as string; - return { name: valuePath, value: value } - }); - view.descriptor_mapped_value.push({ - inputDescriptorId: descriptor.id, - vcView: { rows: vcRows } - }); - } - console.dir(view, { depth: null }) return res.render('verifier/detailed-presentation.pug', { - view: view, - status: verifiablePresentation.status, lang: req.lang, - locale: locale[req.lang] + presentationClaims: presentationClaims, + credentialPayloads: credentials, + locale: locale[req.lang], }) }) diff --git a/views/verifier/definitions.pug b/views/verifier/definitions.pug index ac5fd85..cfd897d 100644 --- a/views/verifier/definitions.pug +++ b/views/verifier/definitions.pug @@ -1,12 +1,11 @@ extends ../layout.pug block layout-content .container - h1 Job positions + h1 Verifiable Presentations .card-container for definition in presentationDefinitions .card - .card-title Software Engineer - Core - p.card-text Send your Bachelor Diploma to apply for "Software Engineer - Core" + .card-title #{definition.title} + p.card-text #{definition.description} .card-buttons button.btn.btn-primary(onclick=`window.location.href = "/verifier-panel/filter/by/definition/${definition.id}"`) See presentations - diff --git a/views/verifier/detailed-presentation.pug b/views/verifier/detailed-presentation.pug index 3b7837f..10577c4 100644 --- a/views/verifier/detailed-presentation.pug +++ b/views/verifier/detailed-presentation.pug @@ -1,16 +1,35 @@ extends ../layout.pug + block layout-content - .container - h1 Detailed Presentation - .card-container - for inputDescriptor in view.descriptor_mapped_value - div.table-container - table - tbody - each row, index in inputDescriptor.vcView.rows - tr(class= index % 2 === 0 ? 'even' : 'odd') - td #{row.name} - td #{row.value} + #success-container + .container2 + .centered-layout + h1 Detailed Presentation + .inline-box-container(style="display: flex; gap:30px; flex-direction: row; flex-wrap: wrap;") + // Iterate over each credential and display an inline box with dynamic background color and text color + each credential, index in credentialPayloads + - const branding = credential.credentialBranding || { backgroundColor: 'red', textColor: 'black' } + - const imageUrl = credential.credentialBranding.image.url // Get the corresponding image URL + .credential-box(style=`display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; position: relative; padding: 10px;`) + img(style="width: 240px; height: 150px; border-radius: 10px;")(src=imageUrl) + + h3 Requested claims extracted from credentials + + // Text area to display claims + textarea#json-textarea.wide(style="border: 1px solid #ccc; padding: 10px; margin-bottom:20px; padding-top:0px; background-color: white; color: black; overflow: auto; resize: vertical; width:500px; min-height: 200px;") + //- Display the 'credentialPayloads' JSON object + | #{JSON.stringify(presentationClaims, null, 2)} + + h3 Credentials + + // Text area to display credentials JSON format + textarea#json-textarea.wide(style="border: 1px solid #ccc; padding: 10px; margin-bottom:20px; padding-top:0px; background-color: white; color: black; overflow: auto; resize: vertical; width:500px; min-height: 200px;") + //- Display the 'credentialPayloads' JSON object + | #{JSON.stringify(credentialPayloads, null, 2)} + block footer + include ../footer.pug + link(rel="stylesheet" href="/styles/index.css") link(rel="stylesheet" href="/styles/detailed-presentation.css") + script(src="/js/index.js") diff --git a/views/verifier/presentations.pug b/views/verifier/presentations.pug index 49f7ea9..fb8661b 100644 --- a/views/verifier/presentations.pug +++ b/views/verifier/presentations.pug @@ -2,10 +2,16 @@ extends ../layout.pug block layout-content .container h1 Received Verifiable Presentations - .card-container - for vp in verifiablePresentations - .card - .card-title #{vp.id} - p.card-text Date received: #{vp.date} - .card-buttons - button.btn.btn-secondary(onclick=`window.location.href = "/verifier-panel/presentation/${vp.id}"`) Inspect details \ No newline at end of file + if verifiablePresentations && verifiablePresentations.length > 0 + .card-container + for vp in verifiablePresentations + .card + .card-title Unique ID: #{vp.id} + p + .card-text Date: #{vp.date} + .card-text Claims: #{JSON.stringify(vp.claims, null, 2)} + .card-text Holder Info: #{vp.holderInfo} + .card-buttons + button.btn.btn-primary(onclick=`window.location.href = "/verifier-panel/presentation/${vp.id}"`) Inspect details + else + h4 No Verifiable Presentations \ No newline at end of file diff --git a/views/verifier/public_definitions.pug b/views/verifier/public_definitions.pug index 6357770..c29ea92 100644 --- a/views/verifier/public_definitions.pug +++ b/views/verifier/public_definitions.pug @@ -1,7 +1,7 @@ extends ../layout.pug block layout-content .container - h1 Job positions + h1 Present your Verifiable Credentials .card-container for definition in presentationDefinitions .card From b7506cc391941a9f6f7e3c76403fe6f7278d58d3 Mon Sep 17 00:00:00 2001 From: pstamatop Date: Fri, 22 Mar 2024 19:19:09 +0200 Subject: [PATCH 2/4] Prevent sd-jwt verification failure based on expiration --- public/js/check-expired.js | 14 ++++++++++++++ .../OpenidForPresentationReceivingService.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 public/js/check-expired.js diff --git a/public/js/check-expired.js b/public/js/check-expired.js new file mode 100644 index 0000000..2b28f58 --- /dev/null +++ b/public/js/check-expired.js @@ -0,0 +1,14 @@ +function updateLayoutForExpiredCredentials(credentialPayloads) { + const currentDate = new Date(); + credentialPayloads.forEach((credential, index) => { + const expirationDate = new Date(credential.expirationDate); + if (expirationDate < currentDate) { + const credentialBox = document.querySelectorAll('.credential-box')[index]; + credentialBox.classList.add('expired-credential'); + const expiredLabel = document.createElement('span'); + expiredLabel.textContent = 'Expired'; + expiredLabel.style.color = 'red'; + credentialBox.appendChild(expiredLabel); + } + }); +} \ No newline at end of file diff --git a/src/services/OpenidForPresentationReceivingService.ts b/src/services/OpenidForPresentationReceivingService.ts index 66d54b3..d588fa4 100644 --- a/src/services/OpenidForPresentationReceivingService.ts +++ b/src/services/OpenidForPresentationReceivingService.ts @@ -403,7 +403,7 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta presentationClaims[desc.id].push({ name: claimName, value: value } as ClaimRecord); }); console.log("Verification result = ", verificationResult) - if (!verificationResult.isValid) { + if (!verificationResult.isSignatureValid || !verificationResult.areRequiredClaimsIncluded) { return { error: new Error("SD_JWT_VERIFICATION_FAILURE"), error_description: new Error(`Verification result ${JSON.stringify(verificationResult)}`) }; } } From d0f079eab566b09082c64d5696eeee41347065a6 Mon Sep 17 00:00:00 2001 From: pstamatop Date: Fri, 22 Mar 2024 19:36:41 +0200 Subject: [PATCH 3/4] Add expiration info in received VPs --- views/verifier/detailed-presentation.pug | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/views/verifier/detailed-presentation.pug b/views/verifier/detailed-presentation.pug index 10577c4..19fdf05 100644 --- a/views/verifier/detailed-presentation.pug +++ b/views/verifier/detailed-presentation.pug @@ -10,7 +10,7 @@ block layout-content each credential, index in credentialPayloads - const branding = credential.credentialBranding || { backgroundColor: 'red', textColor: 'black' } - const imageUrl = credential.credentialBranding.image.url // Get the corresponding image URL - .credential-box(style=`display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; position: relative; padding: 10px;`) + .credential-box(style=`display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; position: relative; padding: 10px;`, id=`credential-box-${index}`) img(style="width: 240px; height: 150px; border-radius: 10px;")(src=imageUrl) h3 Requested claims extracted from credentials @@ -27,6 +27,11 @@ block layout-content //- Display the 'credentialPayloads' JSON object | #{JSON.stringify(credentialPayloads, null, 2)} + script(src="/js/check-expired.js") + script. + var credentialPayloads = !{JSON.stringify(credentialPayloads)}; + updateLayoutForExpiredCredentials(credentialPayloads); + block footer include ../footer.pug From 6528994c7831c82805dbbc84eeecaac4fadf795c Mon Sep 17 00:00:00 2001 From: pstamatop Date: Tue, 26 Mar 2024 12:50:09 +0200 Subject: [PATCH 4/4] Fix expired label --- public/js/check-expired.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/check-expired.js b/public/js/check-expired.js index 2b28f58..662ba2f 100644 --- a/public/js/check-expired.js +++ b/public/js/check-expired.js @@ -5,9 +5,9 @@ function updateLayoutForExpiredCredentials(credentialPayloads) { if (expirationDate < currentDate) { const credentialBox = document.querySelectorAll('.credential-box')[index]; credentialBox.classList.add('expired-credential'); - const expiredLabel = document.createElement('span'); + const expiredLabel = document.createElement('div'); + expiredLabel.className = 'expired-label'; expiredLabel.textContent = 'Expired'; - expiredLabel.style.color = 'red'; credentialBox.appendChild(expiredLabel); } });