For the complete documentation index, see llms.txt. This page is also available as Markdown.

Creating a Protected Dashboard

What We Are Doing:

  • Adding a protected resource (served at /authorization/dashboard) that only verified users can access.

  • Decoding the app session token to display the claims that were disclosed during verification.

Why: To show the end result of verification, we'll display a dashboard page that uses the credential data the holder presented. After a successful verification, our backend issues a short-lived session token (a symmetric JWT we sign ourselves) — the /verifier/kyc-sessions/:sessionId/token route from Creating a Verification Endpoint, which calls the onKycVerified helper from Setting Up the Verification Flow. The dashboard reads that token and renders whatever the verification flow chose to embed in it — no further calls to the Verifier are needed.

The verification session returns only session metadata (id, verifierId, state, authorizationRequestId, authorizationRequestUri, authorizationResponseRedirectUri, expiresAt, errorMessage). To show the holder DID and claims, your flow must obtain them — e.g. via POST /credentials/verify, which returns the credential payload (holder binding in payload.cnf) and prettyClaims — and sign them into the token as sub and claims, as onKycVerified does when you pass them. Otherwise those dashboard fields stay empty.

Steps: Create public/dashboard.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Dashboard</title>
</head>
<body>
<h2>Welcome to the Dashboard!</h2>
<p>DID: <span id="did"></span></p>
<p>First Name: <span id="first-name"></span></p>
<p>Last Name: <span id="last-name"></span></p>
<p>Age: <span id="age"></span></p>

<script>
    (async () => {
        const token = localStorage.getItem("access_token");
        if (!token) {
            window.location.href = "/";
            return;
        }

        const res = await fetch('/authorization/decode-access-token', {
            headers: { Authorization: `Bearer ${token}` }
        });
        if (!res.ok) {
            // Token missing, expired, or invalid — send the user back to verify again.
            localStorage.removeItem("access_token");
            window.location.href = "/";
            return;
        }

        // The DID/claim fields are filled only if the verification flow
        // signed them into the token (see the hint above).
        const data = await res.json();
        const claims = data.claims ?? {};
        document.getElementById("did").innerText = data.did ?? "";
        document.getElementById("first-name").innerText = claims.first_name ?? "";
        document.getElementById("last-name").innerText = claims.last_name ?? "";
        document.getElementById("age").innerText = claims.age ?? "";
    })();
</script>
</body>
</html>

Add a route to decode the session token and extract the claims in src/authorization-routes.js. It verifies the token with the JWT_SECRET you set in your .env during Project Setup:

Finally, mount this router in src/index.js under the /authorization prefix, next to the issuer and verifier routers:

Now, after verification, the user is redirected to /authorization/dashboard, and the dashboard displays what the verification flow signed into the session token.

A few things to keep in mind:

  • Match the claim keys to your schema. This example reads first_name, last_name, and age — the claims of the [email protected]:sd-jwt credential that the "kyc" flow in Setting Up the Verification Flow requires (age ≥ 18). Use the exact property names from the credential schema you are verifying; if you verify a different credential, render its own claim keys instead.

  • The DID shown is the holder's DID — the did:key or did:web of the wallet that presented the credential, read from the session token's sub claim.

  • The disclosed claims come from the verified presentation. Pass through only the claims the holder chose to disclose (SD-JWT selective disclosure), so the dashboard never sees more than what was presented.

Best practice: Give the app session token a short lifetime. The onKycVerified helper signs it with expiresIn: "1h" — tighten that (e.g. "15m") for sensitive resources. jwt.verify() then rejects expired tokens for you, so a leaked token can't grant access to the protected dashboard indefinitely. This session token is internal to your app and is unrelated to the OIDC access token you use to call the Issuer and Verifier APIs.

Last updated