Learn how to build a digital credential verifier from scratch using Next.js, OpenID4VP, and ISO mDoc. This step-by-step developer guide shows you how to create a verifier that can request, receive, and validate mobile driver's licenses and other digital c
Amine
Created: July 31, 2025
Updated: August 1, 2025
Proving identities online is a constant challenge, leading to a reliance on passwords and the sharing of sensitive documents over insecure channels. This has made identity verification for businesses a slow, expensive, and fraud-prone process. Digital Credentials offer a new approach, putting users back in control of their data. They are the digital equivalent of a physical wallet, containing everything from a driver's license to a university degree, but with the added benefits of being cryptographically secure, privacy-preserving, and instantly verifiable.
This guide provides developers with a practical, step-by-step tutorial for building a verifier for Digital Credentials. While the standards exist, there is little guidance on implementing them. This tutorial fills that gap, showing you how to build a verifier using the browser's native Digital Credential API, OpenID4VP for the presentation protocol, and ISO mDoc (e.g., mobile driver's license) as the credential format.
The end result will be a simple yet functional Next.js application that can request, receive, and verify a Digital Credential from a compatible mobile wallet.
Here is a quick look at the final application in action. The process involves four main steps:
Step 1: Initial Page The user lands on the initial page and clicks "Verify with Digital Identity" to start the process.
Step 2: Trust Prompt The browser prompts the user for trust. The user clicks "Continue" to proceed.
Step 3: QR Code Scan A QR code is displayed, which the user scans with their compatible wallet application.
Step 4: Decoded Credential After successful verification, the application displays the decoded credential data.
The magic behind digital credentials lies in a simple but powerful "trust triangle" model involving three key players:
When a user wants to access a service, they present the credential from their wallet. The verifier can then instantly check its authenticity without needing to contact the original issuer directly.
For this decentralized identity ecosystem to flourish, the role of the verifier is absolutely critical. They are the gatekeepers of this new trust infrastructure, the ones who consume the credentials and make them useful in the real world. As the diagram below illustrates, a verifier completes the trust triangle by requesting, receiving, and validating a credential from the holder.
If you're a developer, building a service to perform this verification is a foundational skill for the next generation of secure and user-centric applications. This guide is designed to walk you through that exact process. We'll cover everything you need to know to build your own verifiable credential verifier, from the core concepts and standards to the step-by-step implementation details of validating signatures and checking credential status.
Want to skip ahead? You can find the complete, finished project for this tutorial on GitHub. Feel free to clone it and try it out yourself: https://github.com/corbado/digital-credentials-example
Let's get started.
Before you start, make sure you have:
We'll now go through each of these prerequisites in detail, starting with the standards and protocols that underpin this mdoc-based verifier.
Our verifier is built for the following:
Standard / Protocol | Description |
---|---|
W3C VC | The W3C Verifiable Credentials Data Model. It defines the standard structure for digital credentials, including claims, metadata, and proofs. |
SD-JWT | Selective Disclosure for JWTs. A format for VCs based on JSON Web Tokens that allows holders to selectively disclose only specific claims from a credential, enhancing privacy. |
ISO mDoc | ISO/IEC 18013-5. The international standard for mobile Driver's Licenses (mDLs) and other mobile IDs, defining data structures and communication protocols for offline and online use. |
OpenID4VP | OpenID for Verifiable Presentations. An interoperable presentation protocol built on OAuth 2.0. It defines how a verifier requests credentials and a holder's wallet presents them. |
For this tutorial, we specifically use:
Note on Scope: While we briefly introduce W3C VC and SD-JWT to provide broader context, this tutorial exclusively implements ISO mDoc credentials via OpenID4VP. W3C-based VCs are out of scope for this example.
The ISO/IEC 18013-5 mDoc standard defines the structure and encoding for mobile documents such as mobile driver's licenses (mDLs). mDoc credentials are CBOR-encoded, cryptographically signed, and can be presented digitally for verification. Our verifier will focus on decoding and validating these mdoc credentials.
OpenID4VP is an interoperable protocol for requesting and presenting digital credentials, built on top of OAuth 2.0 and OpenID Connect. In this implementation, OpenID4VP is used to:
Now that we have a clear understanding of the standards and protocols, we need to choose the right tech stack to build our verifier. Our choices are designed for robustness, developer experience, and compatibility with the modern web ecosystem.
We'll use TypeScript for both our frontend and backend code. As a superset of JavaScript, it adds static typing, which helps catch errors early, improves code quality, and makes complex applications easier to manage. In a security-sensitive context like credential verification, type safety is a massive advantage.
Next.js is our framework of choice because it provides a seamless, integrated experience for building full-stack applications.
redirect_uri
to securely receive and verify the final response from the CMWallet.Our implementation relies on a specific set of libraries for the frontend and backend:
Note on openid-client
: More advanced, production-grade verifiers might use the
openid-client
library to handle the OpenID4VP protocol directly on the backend,
enabling features like a dynamic redirect_uri
. In a server-driven OpenID4VP flow with
a redirect_uri
, openid-client
would be used to parse and validate vp_token
responses directly. For this tutorial, we are using a simpler, browser-mediated flow
that does not require it, making the process easier to understand.
This tech stack ensures a robust, type-safe, and scalable verifier implementation focused on the browser's Digital Credential API and ISO mDoc credential format.
To test your verifier, you need a mobile wallet that can interact with the browser's Digital Credential API.
We'll use the CMWallet, a robust OpenID4VP-compliant testing wallet for Android.
How to Install CMWallet (Android):
Note: Only install APK files from sources you trust. The link provided is from the official project repository.
Before we dive into the implementation, it's essential to understand the cryptographic concepts that underpin verifiable credentials. This is what makes them "verifiable" and trustworthy.
At its heart, a Verifiable Credential is a set of claims (like name, date of birth, etc.) that has been digitally signed by an issuer. A digital signature provides two critical guarantees:
Digital signatures are created using public/private key cryptography (also called asymmetric cryptography). Here’s how it works in our context:
Note on DIDs: In this tutorial, we don’t resolve issuer keys via DIDs. In production, issuers would typically expose public keys via DIDs or other authoritative endpoints, which the verifier would use for cryptographic validation.
Verifiable Credentials are often formatted as JSON Web Tokens (JWTs). A JWT is a
compact, URL-safe way to represent claims to be transferred between two parties. A signed
JWT (also known as a JWS) has three parts separated by dots (.
):
alg
).vc
claim), including the
issuer
, credentialSubject
, etc.// Example of a JWT structure [Header].[Payload].[Signature]
Note: JWT-based Verifiable Credentials are out of scope for this blog post. This implementation focuses on ISO mDoc credentials and OpenID4VP, not W3C Verifiable Credentials or JWT-based credentials.
It’s not enough for a verifier to know that a credential is valid; it also needs to know that the person presenting the credential is the legitimate holder. This prevents someone from using a stolen credential.
This is solved using a Verifiable Presentation (VP). A VP is a wrapper around one or more VCs that is signed by the holder themselves.
The flow is as follows:
Our verifier must then perform two separate signature checks:
This two-level check ensures both the authenticity of the credential and the identity of the person presenting it, creating a robust and secure trust model.
Note: The concept of Verifiable Presentations as defined in the W3C VC ecosystem is
out of scope for this blog post. The term
Verifiable Presentation here refers to the
OpenID4VP vp_token
response, which behaves similarly to a W3C VP but is based on ISO
mDoc semantics rather than W3C's JSON-LD signature model. This
guide focuses on ISO mDoc credentials and OpenID4VP, not on W3C Verifiable Presentations
or their signature validation.
Our verifier architecture uses the browser's built-in Digital Credential API as a secure intermediary to connect our web application with the user's mobile CMWallet. This approach simplifies the flow by letting the browser handle the native QR code display and wallet communication.
navigator.credentials.get()
API, receive the result, and forward it to our backend for verification.openid4vp
protocol, and natively generates a QR code. It
then waits for the wallet to return a response.Here is a sequence diagram illustrating the complete and accurate flow:
The Flow Explained:
/api/verify/start
), which
generates a request object containing the query and a nonce, then returns it.navigator.credentials.get()
with the request
object.openid4vp
protocol request and natively
displays a QR code. The .get()
promise is now pending.Note: This QR code flow occurs on desktop browsers. On mobile browsers (Android
Chrome with experimental flag enabled), the browser can directly communicate with
compatible wallets on the same device, eliminating the
need for QR code scanning. To enable this feature on Android Chrome, navigate to
chrome://flags#web-identity-digital-credentials
and set the flag to "Enabled".
.get()
promise on the frontend finally resolves, delivering the presentation payload./api/verify/finish
endpoint. The backend validates the nonce and
credential.Now that we have a solid understanding of the standards, protocols, and architectural flow, we can start building our verifier.
Follow Along or Use the Final Code
We will now go through the setup and code implementation step by step. If you prefer to jump straight to the finished product, you can clone the complete project from our GitHub repository and run it locally.
git clone https://github.com/corbado/digital-credentials-example.git
First, we'll initialize a new Next.js project, install the necessary dependencies, and start our database.
Open your terminal, navigate to the directory where you want to create your project, and run the following command. We're using the App Router, TypeScript, and Tailwind CSS for this project.
npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm
This command scaffolds a new Next.js application in your current directory.
Next, we need to install the libraries that will handle CBOR decoding, database connections, and UUID generation.
npm install cbor-web mysql2 uuid @types/uuid
This command installs:
cbor-web
: For decoding the mdoc credential payload.mysql2
: The MySQL client for our database.uuid
: To generate unique challenge strings.@types/uuid
: TypeScript types for the uuid
library.Our backend requires a MySQL database to store OIDC session data, ensuring that each
verification flow is secure and stateful. We've included a docker-compose.yml
file to
make this easy.
If you have cloned the repository, you can simply run docker-compose up -d
. If you are
building from scratch, create a file
named docker-compose.yml
with the following content:
services: mysql: image: mysql:8.0 restart: always environment: MYSQL_ROOT_PASSWORD: rootpassword MYSQL_DATABASE: digital_credentials MYSQL_USER: app_user MYSQL_PASSWORD: app_password ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] timeout: 20s retries: 10 volumes: mysql_data:
This Docker Compose setup also requires a SQL initialization script. Create a directory
named sql
and inside it, a file named init.sql
with the following content to set up
the necessary tables:
-- Create database if not exists CREATE DATABASE IF NOT EXISTS digital_credentials; USE digital_credentials; -- Table for storing challenges CREATE TABLE IF NOT EXISTS challenges ( id VARCHAR(36) PRIMARY KEY, challenge VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, used BOOLEAN DEFAULT FALSE, INDEX idx_challenge (challenge), INDEX idx_expires_at (expires_at) ); -- Table for storing verification sessions CREATE TABLE IF NOT EXISTS verification_sessions ( id VARCHAR(36) PRIMARY KEY, challenge_id VARCHAR(36), status ENUM('pending', 'verified', 'failed', 'expired') DEFAULT 'pending', presentation_data JSON, verified_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE, INDEX idx_challenge_id (challenge_id), INDEX idx_status (status) ); -- Table for storing verified credentials data (optional) CREATE TABLE IF NOT EXISTS verified_credentials ( id VARCHAR(36) PRIMARY KEY, session_id VARCHAR(36), credential_type VARCHAR(255), issuer VARCHAR(255), subject VARCHAR(255), claims JSON, verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (session_id) REFERENCES verification_sessions(id) ON DELETE CASCADE, INDEX idx_session_id (session_id), INDEX idx_credential_type (credential_type) );
Once both files are in place, open your terminal in the project root and run:
docker-compose up -d
This command will start a MySQL container in the background.
Our Next.js application is structured to separate concerns between the frontend and backend, even though they are part of the same project.
src/app/page.tsx
): A single React page that
initiates the verification flow and displays the result. It interacts with the browser's
Digital Credential API.src/app/api/verify/...
):
start/route.ts
: Generates the OpenID4VP request and a security nonce.finish/route.ts
: Receives the presentation from the wallet (via the browser),
validates the nonce, and decodes the credential.src/lib/
):
database.ts
: Manages all database interactions (creating challenges, verifying
sessions).crypto.ts
: Handles the decoding of the CBOR-based mDoc credential.Here is a diagram illustrating the internal architecture:
Our frontend is intentionally lightweight. Its primary responsibility is to act as the user-facing trigger for the verification flow and to communicate with both our backend and the browser's native credential handling capabilities. It doesn't contain any complex protocol logic itself; that's all delegated.
Specifically, the frontend will handle the following:
/api/verify/start
and receives a structured
JSON payload (protocol
, request
, state
) describing exactly what the wallet should
present.navigator.credentials.get()
,
which renders a native QR code and waits for the wallet response./api/verify/finish
endpoint in a POST request
for the final server-side validation.The core logic is in the startVerification
function:
// src/app/page.tsx const startVerification = async () => { setLoading(true); setVerificationResult(null); try { // 1. Check if the browser supports the API if (!navigator.credentials?.get) { throw new Error("Browser does not support the Credential API."); } // 2. Ask our backend for a request object const res = await fetch("/api/verify/start"); const { protocol, request } = await res.json(); // 3. Hand that object to the browser – this triggers the native QR code const credential = await (navigator.credentials as any).get({ mediation: "required", digital: { requests: [ { protocol, // "openid4vp" data: request, // contains dcql_query, nonce, etc. }, ], }, }); // 4. Forward the wallet response (from the browser) to our finish endpoint for server-side checks const verifyRes = await fetch("/api/verify/finish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(credential), }); const result = await verifyRes.json(); if (verifyRes.ok && result.verified) { setVerificationResult(`Success: ${result.message}`); } else { throw new Error(result.message || "Verification failed."); } } catch (err) { setVerificationResult(`Error: ${(err as Error).message}`); } finally { setLoading(false); } };
This function shows the four key steps of the frontend logic: checking for API support, fetching the request from the backend, calling the browser API, and sending the result back for verification. The rest of the file is standard React boilerplate for state and UI rendering, which you can view in the GitHub repository.
digital
and mediation: 'required'
?#You might notice our call to navigator.credentials.get()
looks different from simpler
examples. This is because we are adhering strictly to the official
W3C Digital Credentials API specification.
digital
Member: The specification requires that all digital credential requests be
nested inside a digital
object. This provides a clear, standardized namespace for this
API, distinguishing it from other credential types (like password
or federated
) and
allowing for future extensions without conflicts.
mediation: 'required'
: This option is a crucial security and user-experience
feature. It enforces that the user must actively interact with a prompt (e.g., a
biometric scan, PIN entry, or a consent screen) to approve the credential request.
Without it, a website could potentially attempt to silently access credentials in the
background, which poses a significant privacy risk. By requiring mediation, we ensure
that the user is always in control and gives explicit consent for every transaction.
With the React UI in place we now need two API routes that do the heavy-lifting on the server:
/api/verify/start
– builds an OpenID4VP request, persists a one-time challenge in
MySQL and hands everything back to the browser./api/verify/finish
– receives the wallet response, validates the challenge,
verifies & decodes the credential, and finally returns a concise JSON result to the UI./api/verify/start
: Generate the OpenID4VP Request#// src/app/api/verify/start/route.ts import { NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { createChallenge, cleanupExpiredChallenges } from "@/lib/database"; export async function GET() { // 1️⃣ Create a short-lived, random nonce (challenge) const challenge = uuidv4(); const challengeId = uuidv4(); const expiresAt = new Date(Date.now() + 5 * 60 * 1000); await createChallenge(challengeId, challenge, expiresAt); cleanupExpiredChallenges().catch(console.error); // 2️⃣ Build a DCQL query that describes *what* we want const dcqlQuery = { credentials: [ { id: "cred1", format: "mso_mdoc", meta: { doctype_value: "eu.europa.ec.eudi.pid.1" }, claims: [ { path: ["eu.europa.ec.eudi.pid.1", "family_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "given_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "birth_date"] }, ], }, ], }; // 3️⃣ Return an object the browser can pass to navigator.credentials.get() return NextResponse.json({ protocol: "openid4vp", // tells the browser which wallet protocol to use request: { dcql_query: dcqlQuery, // WHAT to present nonce: challenge, // anti-replay response_type: "vp_token", response_mode: "dc_api", // the wallet will POST directly to /finish }, state: { credential_type: "mso_mdoc", // kept for later checks nonce: challenge, challenge_id: challengeId, }, }); }
Key parameters
• nonce
– cryptographic challenge that binds
request & response (prevents replay).
• dcql_query
– An object describing the exact claims we need. For this guide, we
use a dcql_query
structure inspired by recent drafts of the Digital Credential Query
Language, even though this is not yet a finalized standard.
• state
– arbitrary JSON echoed back by the wallet so we can look up the DB
record.
The file src/lib/database.ts
wraps the basic MySQL operations for challenges &
verification sessions (insert, read, mark-used). Keeping this logic in a single module
makes it easy to swap the datastore later.
/api/verify/finish
: Validate & Decode the Presentation#// src/app/api/verify/finish/route.ts import { NextResponse, NextRequest } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { getChallenge, markChallengeAsUsed, createVerificationSession, updateVerificationSession, } from "@/lib/database"; import { decodeDigitalCredential, decodeAllNamespaces } from "@/lib/crypto"; export async function POST(request: NextRequest) { const body = await request.json(); // 1️⃣ Extract the verifiable presentation pieces const vpTokenMap = body.vp_token ?? body.data?.vp_token; const state = body.state; const mdocToken = vpTokenMap?.cred1; // we asked for this ID in dcqlQuery if (!vpTokenMap || !state || !mdocToken) { return NextResponse.json( { verified: false, message: "Malformed response" }, { status: 400 }, ); } // 2️⃣ One-time-use challenge validation const stored = await getChallenge(state.nonce); if (!stored) { return NextResponse.json( { verified: false, message: "Invalid or expired challenge" }, { status: 400 }, ); } const sessionId = uuidv4(); await createVerificationSession(sessionId, stored.id); // 3️⃣ (Pseudo) cryptographic checks – replace with real mDL validation in prod // In a real application, you would use a dedicated library to perform full // cryptographic validation of the mdoc signature against the issuer's public key. const isValid = mdocToken.length > 0; if (!isValid) { await updateVerificationSession(sessionId, "failed", { reason: "mdoc validation failed", }); return NextResponse.json( { verified: false, message: "Credential validation failed" }, { status: 400 }, ); } // 4️⃣ Decode the mobile-DL (mdoc) payload into human readable JSON const decoded = await decodeDigitalCredential(mdocToken); const readable = decodeAllNamespaces(decoded)["eu.europa.ec.eudi.pid.1"]; await markChallengeAsUsed(state.nonce); await updateVerificationSession(sessionId, "verified", { readable }); return NextResponse.json({ verified: true, message: "mdoc credential verified successfully!", credentialData: readable, sessionId, }); }
Important fields in the wallet response
• vp_token
– map that holds each credential the wallet returns. For our demo we
pull vp_token.cred1
.
• state
– echo of the blob we provided in /start
; contains the nonce
so we can
look up the DB record.
• mdocToken
– a Base64URL-encoded CBOR structure that represents the ISO mDoc.
When the verifier receives an mdoc credential from the browser, it is a Base64URL string
containing CBOR-encoded binary data. To extract the actual claims, the finish
endpoint
performs a multi-step decoding process using helper functions from src/lib/crypto.ts
.
The decodeDigitalCredential
function handles the conversion from the encoded string to a
usable object:
// src/lib/crypto.ts export async function decodeDigitalCredential(encodedCredential: string) { // 1. Convert Base64URL to standard Base64 const base64UrlToBase64 = (input: string) => { let base64 = input.replace(/-/g, "+").replace(/_/g, "/"); const pad = base64.length % 4; if (pad) base64 += "=".repeat(4 - pad); return base64; }; const base64 = base64UrlToBase64(encodedCredential); // 2. Decode Base64 to binary const binaryString = atob(base64); const byteArray = Uint8Array.from(binaryString, (char) => char.charCodeAt(0)); // 3. Decode CBOR const decoded = await cbor.decodeFirst(byteArray); return decoded; }
cbor-web
library to decode the binary data into a
structured JavaScript object.The decodeAllNamespaces
function further processes the decoded CBOR object to extract
the actual claims from the relevant namespaces:
// src/lib/crypto.ts export function decodeAllNamespaces(jsonObj) { const decoded = {}; try { jsonObj.documents.forEach((doc, idx) => { // 1) issuerSigned.nameSpaces: const issuerNS = doc.issuerSigned?.nameSpaces || {}; Object.entries(issuerNS).forEach(([nsName, entries]) => { if (!decoded[nsName]) decoded[nsName] = {}; (entries as any[]).forEach((entry) => { const bytes = Uint8Array.from(entry.value); const decodedEntry = cbor.decodeFirstSync(bytes); Object.assign(decoded[nsName], decodedEntry); }); }); // 2) deviceSigned.nameSpaces (if present): const deviceNS = doc.deviceSigned?.nameSpaces; if (deviceNS?.value?.data) { const bytes = Uint8Array.from(deviceNS.value); decoded[`deviceSigned_ns_${idx}`] = cbor.decodeFirstSync(bytes); } }); } catch (e) { console.error(e); } return decoded; }
eu.europa.ec.eudi.pid.1
) to extract the actual claim
values (such as name, date of birth, etc.).After running through these steps, the finish endpoint obtains a human-readable object containing the claims from the mdoc, for example:
{ "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" }
This process ensures that the verifier can securely and reliably extract the necessary information from the mdoc credential for display and further processing.
The finish endpoint returns a minimal JSON object to the frontend:
{ "verified": true, "message": "mdoc credential verified successfully!", "credentialData": { "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" } }
The frontend receives this response in startVerification()
and simply persists it in
React state so we can render a nice confirmation card or display individual claims – e.g.
“Welcome, John Doe (born 1990-01-01)!”.
You now have a complete, working verifier that uses the browser's native credential handling capabilities. Here’s how to run it locally and what you can do to take it from a proof-of-concept to a production-ready application.
Clone the Repository:
git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
Install Dependencies:
npm install
Start the Database: Make sure Docker is running on your machine, then start the MySQL container:
docker-compose up -d
Run the Application:
npm run dev
Open your browser to http://localhost:3000
, and you should see the verifier's UI.
You can now use your CMWallet to scan the QR code and complete the verification flow.
This tutorial provides the foundational building blocks for a verifier. To make it production-ready, you would need to implement several additional features:
Full Cryptographic Validation: The current implementation uses a placeholder check
(mdocToken.length > 0
). In a real-world scenario, you must perform full cryptographic
validation of the mdoc signature against the issuer's public key
(e.g., by resolving their DID or fetching their public key certificate). For DID
resolution standards, refer to the
W3C DID Resolution specification.
Issuer Revocation Checking: Credentials can be revoked by the issuer before their expiry date. A production verifier must check the credential's status by querying a revocation list or status endpoint provided by the issuer. The W3C Verifiable Credentials Status List provides the standard for credential revocation lists.
Robust Error Handling & Security: Add comprehensive error handling, input validation, rate-limiting on API endpoints, and ensure all communication is over HTTPS (TLS) to protect data in transit. The OWASP API Security Guidelines provide comprehensive API security best practices.
Support for Multiple Credential Types: Extend the logic to handle different
doctype
values and credential formats if you expect to receive more than just the
European Digital Identity (EUDI) PID credential. The
W3C Verifiable Credentials Data Model provides
comprehensive VC format specifications.
This example is intentionally focused on the core browser-mediated flow to make it easy to understand. The following topics are considered out of scope:
redirect_uri
or dynamic client
registration.By building on this foundation and incorporating these next steps, you can develop a robust and secure verifier capable of trusting and validating digital credentials in your own applications.
That’s it! With fewer than 250 lines of TypeScript, we now have an end-to-end verifier that:
In production, you would replace the placeholder validation with full ISO 18013-5 checks, add issuer revocation look-ups, rate-limiting, audit logging, and of course, end-to-end TLS—but the core building blocks stay exactly the same.
Here are some of the key resources, specifications, and tools used or referenced in this tutorial:
Project Repository:
Key Specifications:
Tools:
Libraries:
Enjoyed this read?
🤝 Join our Passkeys Community
Share passkeys implementation tips and get support to free the world from passwords.
🚀 Subscribe to Substack
Get the latest news, strategies, and insights about passkeys sent straight to your inbox.
Related Articles
Table of Contents