Learn how to build and configure a web application that can verify mDocs
Introduction
In this tutorial, you will use the Self Service Portal and the Verifier Web SDK to build and configure a web application that can verify an mDoc presented via an online presentation workflow as per ISO/IEC 18013-7:2024 and OID4VP .
This web application will support both same-device and cross-device workflows to accommodate flexible user journeys.
Prerequisites
Before you get started, let’s make sure you have everything you need.
Prior knowledge
- We recommend you make yourself familiar with the following concepts to support your understanding
of the concepts included in this tutorial:
- What is Credential verification?
- What are mDocs?
- What steps does an online verification workflow comprise?
Assets
- Complete the sign up form to get trial access to MATTR VII and the Self Service Portal.
- Use the Self Service Portal to create a new tenant.
- Install the MATTR GO Hold example app by following the getting started guide and use it to claim an mDoc.
Development environment
- Download and install npm .
- Install ngrok and sign up for an account . You will use ngrok to test the presentation workflow.
- You will need a code editor such as VS Code .
Got everything? Let’s get going!
Overview
The following diagram depicts the workflow you will build in this tutorial:
- The user triggers the workflow by interacting with the web application.
- The web application uses the embedded Verifier Web SDK capabilities to start a presentation-based verification session with the configured MATTR VII tenant.
- The MATTR VII tenant responds with a link to invoke a matching mobile application.
- The web application uses the link to invoke a matching mobile application, by either using a redirect (same-device) or rendering a QR code (cross-device).
- The mobile application makes a request to the MATTR VII tenant to retrieve a request object, defining what information is requested for verification.
- The MATTR VII tenant returns the request object to the mobile application.
- The mobile application (upon user consent) returns an authorization response to the MATTR VII tenant, which includes the information required for verification.
- The MATTR VII tenant returns the verification results to the web application.
- The web application surfaces the verification results to the user and the interaction continues.
You will build this workflow in three parts:
-
Part 1: Use the Self Service Portal to configure the MATTR VII verifier tenant.
-
Part 2: Build a web application with mDocs verification capabilities.
-
Part 3: Integrate a backend (optional).
Part 1: Configure the MATTR VII verifier tenant
The MATTR VII verifier tenant will be used to interact with your web application (generating a verification request) and the wallet application (presenting an mDoc for verification) as per OID4VP and ISO/IEC 18013-7. To enable this, you must:
- Create a verifier application configuration: Define what applications can create verification sessions with the MATTR VII tenant, and how to handle these requests.
- Create a supported wallet configuration: Define how to invoke specific wallet applications as part of an online verification workflow.
- Configure a trusted issuer: The MATTR VII verifier tenant will only accept mDocs issued by these trusted issuers.
Create a verifier application configuration
Each MATTR VII tenant can interact with multiple verifier applications, and can handle requests differently for each application. This means you must create a verifier application configuration that defines how to handle verification requests from your web application.
- Expand the Credential Verification section in the left-hand navigation panel.
- Select Applications.
- Select the Create new button.
- Use the Name text box to insert a meaningful and friendly name for your application.
- Use the Type radio button to select Web.
- Use the Allowed domains text box to insert a placeholder domain name (e.g.
place-holder.com
). This indicates domain names that the MATTR VII tenant can verify incoming requests are from known and trusted applications. You will update this placeholder value later. - Use the Redirect URIs text box to insert a placeholder redirect URI (e.g.
https://place-holder.com
). This is the URI the user would be redirected to after presenting a credential on a same-device flow. You will update this placeholder value later. - Select the Create button to create the new application and display the Application detail screen.
- Copy and record the
ID
value. You will use it later in the tutorial.
Create a supported wallet configuration
Verifier web applications can define specific wallet applications to accept mDocs from as part of their verification workflows. The MATTR VII verifier tenant needs to be configured with a specific URI scheme that will be used to invoke these wallets.
- Expand the Credential Verification section in the left-hand navigation panel.
- Select Supported wallets.
- Select the Create new button.
- Use the Name text box to insert a meaningful and friendly name for your wallet application (for example “MATTR GO Hold”).
- Use the Authorize endpoint text box to insert
mdoc-openid4vp://
. This is the URI scheme that will be used to invoke the wallet application. You can learn more about URI scheme selection logic here. - Select the Create button to create the new wallet configuration.
The authorization endpoint configured in the example above (mdoc-openid4vp://
) is the default
OID4VP scheme. While this is technically redundant, we chose to include this step to explain how
to adjust this configuration for wallet applications using different schemes.
Configure a trusted issuer
You must configure trusted issuers on your MATTR VII verifier tenant, as presented mDocs will only be verified if they had been issued by a trusted issuer.
This is achieved by providing the PEM certificate of the IACA used by these issuers to sign mDocs. In production environments the issuer can provide it out of band or you can obtain it via their issuer’s metadata.
- Expand the Credential Verification section in the left-hand navigation panel.
- Select Trusted issuers.
- Select the Create new button.
- Use the Certificate PEM file to upload the following file. This is the IACA identifying the MATTR Labs demo issuer which issues the credential referenced in the prerequisites section.
- Select the Add button to add the new trusted issuer.
This completes the configuration of the MATTR VII verifier tenant. In the next step, you will build a simple application that you can run locally to test the verification workflow and ensure everything is functioning as expected.
Part 2: Build a web application with mDocs verification capabilities
Now that the MATTR VII verifier tenant is properly configured, you can proceed with the steps required to embed verification capabilities into your web application:
- Setup your environment: Setup the required infrastructure for your web application.
- Update the domain and redirect URI: Once your tunneling service is up and running you can update the placeholder values in the MATTR VII verifier application configuration.
- Initialize the SDK: So that the SDK functions are available in your web application.
- Create a credential query: Define what information is required for verification and how to handle the interaction with the user.
- Handle verification results: Create the logic that enables your web application to retrieve verification results in different workflows.
Setup your environment
- Open your terminal and create a new NextJS default project:
npx create-next-app@latest --src-dir --yes
- Approve installing any required packages.
- Insert a name for your project (e.g.
my-verifier-web-application
). - Open the project app’s folder:
cd my-verifier-web-application
- Install the SDK:
npm install @mattrglobal/verifier-sdk-web
- Open the
src/app/page.tsx
file in your preferred code editor and replace all existing code with the following:
'use client'
// Step 3.2 - Add dependencies
export default function Home() {
// Step 5.1 - Store results state
// Step 6.1 - Create createRequest function
// Step 4.1 - Create requestCredentials function
// Step 5.3 - Handle same-device redirect
// Step 6.3 - Create retrieveResults function
// Step 3.3 - Initialize the SDK
// Step 5.4 - Add effect to handle redirect
return (
<div className="max-w-2xl mt-12 mx-auto px-8">
<h1 className="text-3xl font-bold my-8">mDocs Online Verification Tutorial</h1>
{/* Step 4.3 - Add request button */}
{/* Step 5.5 - Render results */}
</div>
)
}
This will serve as the basic structure for your application. You will copy and paste
different code snippets into specific locations to achieve the different functionalities.
These locations are indicated by comments that reference the corresponding tutorial step (e.g. // Step 3.2 - Add dependencies
).
We recommend copying and pasting the comment text to easily locate it in the code.
- Run the project:
npm run dev
This will run the app and make it available at http://localhost:3000 (or the next available port if 3000 is already used).
- Start an ngrok tunnel in a new terminal window:
ngrok http http://localhost:3000
If your web application is running on a port different than 3000, use that value in the ngrok command above.
Your terminal window should show the tunnel details:
- Make note of the
Forwarding
value, you will use it in the next step.
Update the domain and redirect URI
With the tunneling service now active, update the MATTR VII verifier application configuration to include the domain and redirect URI details.
-
Expand the Credential Verification section in the left-hand navigation panel.
-
Select Applications.
-
Select the verifier application configuration you created in the previous step.
-
Update the Allowed domains text box to include the
Forwarding
URL from the ngrok tunnel, removing thehttps://
prefix. -
Update the Redirect URIs text box to include the
Forwarding
URL from the ngrok tunnel. -
Select the Update button to update the application configuration.
The
Forwarding
URL is a temporary URL that will be used to test the web application. It will change every time you restart the ngrok tunnel, so you will need to repeat this step each time you restart the tunnel.
Initialize SDK
-
Create a new file named
.env.local
in the projects root folder and add the following code to add your tenant’s URL to the project’s variables:NEXT_PUBLIC_TENANT_URL=<YOUR_TENANT_URL>
NEXT_PUBLIC_TENANT_URL
: Use yourtenant_url
.
-
Copy and paste the following code under the
Step 3.2 - Add dependencies
comment to add required dependencies to your web application:tsximport * as MATTRVerifierSDK from '@mattrglobal/verifier-sdk-web' import { useCallback, useEffect, useState } from 'react'
-
Copy and paste the following code under the
Step 3.3 - Initialize the SDK
comment to create a new function that will be used by your web application to initialize the SDK:tsxuseEffect(() => { MATTRVerifierSDK.initialise({ apiBaseUrl: process.env.NEXT_PUBLIC_TENANT_URL as string, applicationId: '0eaa8074-8cc4-41ec-9e42-072d36e2acb0' }) }, [])
The function calls the SDK’s initialise function that makes the SDK methods available in your web application while defining the following parameters:
apiBaseUrl
: This indicates to the Verifier Web SDK what MATTR VII tenant to interact with in order to verify presented mDocs. It is retrieved from theNEXT_PUBLIC_TENANT_URL
project variable defined in step 1 above.applicationId
: This will be used by the MATTR VII tenant to identify verification requests from your web application. Replace with theid
displayed on the Application detail screen of your created application configuration.
Create credential request
Now that you have a function and a UI in place, you need to define what the function does by creating a credential request. This request will define what information is requested from the user, and how to interact with the MATTR VII verifier tenant.
-
Copy and paste the following code under the
Step 4.1 - Create requestCredential function
comment to create a newrequestCredentials
function:tsxconst requestCredentials = useCallback(async () => { const credentialQuery = { profile: MATTRVerifierSDK.OpenidPresentationCredentialProfileSupported.MOBILE, docType: 'org.iso.18013.5.1.mDL', nameSpaces: { 'org.iso.18013.5.1': { age_over_18: {}, given_name: {}, family_name: {}, portrait: {} } } } as MATTRVerifierSDK.CredentialQuery // Step 4.2 - Add credential request }, [])
This function currently includes a
credentialQuery
variable that defines what information is requested from the user for verification:This example credential query will request the
age_over_18
,given_name
,family_name
andportrait
claims from a credential of profilemobile
and docTypeorg.iso.18013.5.1.mDL
, under theorg.iso.18013.5.1
namespace. Presenting a credential with different names, docType or namespace will fail verification. -
Copy and paste the following code under the
Step 4.2 - Add credential request
comment so that the newrequestCredentials
function calls the SDK’srequestCredentials
function, passing thecredentialQuery
variable as a parameter:tsxconst options: MATTRVerifierSDK.RequestCredentialsOptions = { credentialQuery: [credentialQuery], // Step 6.2 - Use challenge from backend challenge: MATTRVerifierSDK.utils.generateChallenge(), redirectUri: window.location.origin, crossDeviceCallback: { onComplete: (callbackResponse) => { // Step 5.2 - Retrieve cross-device verification results }, onFailure: (error) => { if (error.type === 'Abort') { alert('The request was aborted') return } alert(`There was an error processing the request - ${error.message}`) } } } await MATTRVerifierSDK.requestCredentials(options)
options
: This object defines the required parameters for calling the SDK’srequestCredentials
function. It is passed as a parameter to the function when calling it.credentialQuery
: Defines what information is requested from the user for verification. This example uses the credential query created in the previous step.challenge
: Unique challenge generated by the SDK to identify this specific presentation session. You will use it in part 3 of the tutorial.redirectUri
: Indicates that the user will be redirected to the same web application screen when completing a same-device workflow. It can be any other path on your application domain, as long as it is handled by the application.crossDeviceCallback
: You will update this property later to configure how the web application handles the callback when a response is received as part of a cross-device workflow.
-
Copy and paste the following code under the
Step 4.3 - Add request credentials button
comment to create a new button that the user will use to interact with the web application:tsx<button onClick={requestCredentials} className="px-4 py-2 rounded border-2 border-black font-bold" > Request Credential </button>
Once the user selects this button, the web application will trigger the verification workflow by calling the
requestCredentials
function created above:- This function uses calls the SDK’s
requestCredentials
method which starts a presentation session with the configured MATTR VII session. - Once a session is established, the MATTR VII verifier tenant will interact with the user’s wallet application to request and verify the details provided in the query.
- When the verification process is complete, the MATTR VII verifier tenant will return the verification results to your web application.
- This function uses calls the SDK’s
Handle verification results
-
Copy and paste the following code under the
Step 5.1 - Store results state
comment to create a variable that will store the verification state:tsxconst [results, setResults] = useState<MATTRVerifierSDK.PresentationSessionResult | null>(null)
-
Copy and paste the following code under the
Step 5.2 - Retrieve cross-device verification results
comment to retrieve verification results when they are available in cross-device workflows (onComplete
ofrequestCredentials
):tsxif ('result' in callbackResponse) { // Step 6.4 - Call retrieveResults function on cross-device callback setResults(callbackResponse.result as MATTRVerifierSDK.PresentationSessionResult) }
-
Copy and paste the following code under the
Step 5.3 - Handle same-device redirect
comment to create a newhandleRedirect
function:tsxconst handleRedirect = useCallback(async () => { const result = await MATTRVerifierSDK.handleRedirectCallback() if (result.isErr()) { alert(`Error retrieving results: ${result.error.message}`) return } const presentationResult = result.value.result if (!('sessionId' in presentationResult) || 'error' in presentationResult) { alert('Something went wrong') return } // Step 6.5 - Call retrieveResults function on same-device redirect setResults(presentationResult as MATTRVerifierSDK.PresentationSessionResult) }, [])
This function calls the SDK’s handleRedirectCallback function, which returns the verification results as the user is redirected following a same-device presentation workflow. These results (
result.value.result
) are stored by the web applicationhandleRedirect
function in thepresentationResult
variable. -
Copy and paste the following code under the
Step 5.4 - Add effect to handle redirect
comment to add an effect that calls thehandleRedirect
function when the web application is refreshed with a hash, indicating a redirect after completing a same-device presentation workflow:tsxuseEffect(() => { if (window.location.hash) { handleRedirect() } }, [handleRedirect])
-
Copy and paste the following code under the
Step 5.5 - Render results
comment to retrieve different properties of theresults
object once it is available, and display them to the user as part of the interaction:tsx;<h2 className="text-xl font-bold mt-8 mb-4">Results</h2> { !results && 'No results available' } { results && 'credentials' in results && ( <div className="flex flex-col gap-2"> <div className="flex flex-row"> <div className="w-1/3 font-bold">Verification Result</div> <div className="w-2/3"> {results.credentials?.[0].verificationResult.verified ? 'verified' : 'not verified'} </div> </div> <div className="flex flex-row"> <div className="w-1/3 font-bold">Valid Until</div> <div className="w-2/3">{results.credentials?.[0].validityInfo.validUntil}</div> </div> <div className="flex flex-row"> <div className="w-1/3 font-bold">Issuer</div> <div className="w-2/3">{results.credentials?.[0].issuerInfo.commonName}</div> </div> <div className="flex flex-row"> <div className="w-1/3 font-bold">Given Name</div> <div className="w-2/3"> {(results.credentials?.[0].claims?.['org.iso.18013.5.1'].given_name .value as string) ?? ''} </div> </div> <div className="flex flex-row"> <div className="w-1/3 font-bold">Age over 18</div> <div className="w-2/3"> {results.credentials?.[0].claims?.['org.iso.18013.5.1'].age_over_18.value ? 'Yes' : 'No'} </div> </div> </div> ) } { results && 'error' in results && ( <div className="flex flex-row"> <div className="w-1/3 font-bold">{results.error.type}</div> <div className="w-2/3">{results.error.message}</div> </div> ) }
In this example when verification is successful (
"credentials" in results
) the app retrieves and displays the following properties of the first credential (credentials?.[0]
) of theresults
object:verificationResult.verified
: Indicates whether or not the mDoc was verified.validityInfo.validUntil
: Indicates the mDoc expiry date.issuerInfo.commonName
: Indicates the issuer’s name.claims?.['org.iso.18013.5.1'].given_name
: Indicates the holder’s given name.claims?.['org.iso.18013.5.1'].age_over_18.value
: Indicates whether or not the holder is over 18.
When verification fails (
"error" in results
) the app retrieves and displays error details (error.type
anderror.message
) from theresults
response.
Your web application will now continue the interaction based on the workflow type:
- For cross-device workflows, the web application will display the verification results in the desktop browser where the interaction started. The user can then continue the interaction on this desktop browser.
- For same-device workflows, the MATTR VII verifier tenant will redirect the user back to the web application in their mobile browser, where verification will be displayed. The user can then continue the interaction on this mobile browser.
Congratulations, your web application is now ready to verify mDocs.
Let’s test what you’ve built!
Test the front channel interaction
If your goal was just to see mDocs online verification in action, there’s no need to proceed to the next step of the tutorial. However, production solutions often integrate a backend, and if you’re curious about how that works, feel free to continue to the next step—it’s optional and won’t change the overall workflow, just what’s happening behind the scenes.
Part 3: Integrate a backend into your online verification workflow
Our online verification workflow is now working well. However, to make the solution more secure it is recommend to integrate your web application backend into the workflow. In this adjusted workflow, verification results are only shared with the backend and only surfaced to the front channel after validation. Refer to the detailed workflow page for more information.
In this part of the tutorial you will configure a simple backend that would retrieve the verification results from the MATTR VII tenant, validate and pass them to the front channel web application.
Update verifier application configuration
The first thing you need to do is to make sure verification results are not returned directly to the web application’s front channel. This setting is part of your MATTR VII verifier application configuration.
-
Expand the Credential Verification section in the left-hand navigation panel.
-
Select Applications.
-
Select the verifier application configuration you created in the previous step.
-
Use the Verification results return method radio button to select Must be retrieved and validated by backend.
-
Select the Update button to update the application configuration.
This setting is only available when the application type is set to Web.
Add your MATTR VII access credentials
Your backend will need to make a request to a protected MATTR VII endpoint to retrieve the verification results. To enable this, you must provide it with your MATTR VII tenant access credentials.
-
Add the following code to your
.env.local
file to add your MATTR VII tenant access credentials:MATTR_CLIENT_ID=ZAGvD******************************** MATTR_CLIENT_SECRET=uc8NM****************************************
MATTR_CLIENT_ID
: Replace with the tenant’sclient_id
value.MATTR_CLIENT_SECRET
: Replace with the tenant’sclient_secret
value.
Create a session and associate a challenge
The web application backend needs to generate a unique challenge in the context of an existing user session. In a typical application, the user would likely already interact with the application inside an active session, and often that session is tracked using a cookie. In this tutorial you are mimicking this by setting a random value as the user session ID.
You are also creating a random challenge
and storing it inside a cookie to ensure that the
presentation results you are retrieving belong to the user in the current user session.
- Create a new file at
src/app/api/request/route.ts
with the following content:
import { cookies } from 'next/headers'
export async function POST(_request: Request) {
const sessionId = crypto.randomUUID()
const challenge = crypto.randomUUID()
const cookieStore = await cookies()
cookieStore.set('session_id', sessionId, { httpOnly: true, secure: true, sameSite: true })
cookieStore.set('challenge', challenge, { httpOnly: true, secure: true, sameSite: true })
return Response.json({ challenge }, { status: 201 })
}
While it’s not strictly necessary to keep the challenge
secret, you might want to only use a
single encrypted session cookie to not leak any information about your session management.
If you use a database to store the sessions state, you could use that database to store the
challenge
against the user session.
Retrieve verification results
Next you need to create an API route that will make a request to the MATTR VII verifier tenant and retrieve the results of a specific presentation session based on its ID.
-
Create a new file at
src/app/api/results/[id]/route.ts
with the following content:tsimport { cookies } from 'next/headers' async function getToken() { const response = await fetch('https://auth.au01.mattr.global/oauth/token', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ client_id: process.env.MATTR_CLIENT_ID, client_secret: process.env.MATTR_CLIENT_SECRET, audience: process.env.NEXT_PUBLIC_TENANT_URL, grant_type: 'client_credentials' }) }) const { access_token } = await response.json() return access_token } export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) { const id = (await params).id const cookiesStore = await cookies() if (!cookiesStore.get('session_id')) { return Response.json({ error: 'No valid session for user' }, { status: 401 }) } const response = await fetch( `${process.env.NEXT_PUBLIC_TENANT_URL}/v2/presentations/sessions/${id}/result`, { headers: { authorization: `Bearer ${await getToken()}` } } ) const results = await response.json() if (results.challenge !== cookiesStore.get('challenge')?.value) { return Response.json({ error: 'Challenge does not match' }) } return Response.json({ results }) }
The
getToken
function first uses project variables to retrieve a MATTR VII access token, as the results are retrieved from a protected endpoint.The route must be called with a dynamic
id
route parameter. This is the unique identifier of the presentation session. It is used in the authenticated request to the/v2/presentations/sessions/${id}/result
endpoint to retrieve the corresponding verification results.Your backend must then validate the returned results. The exact implementation would depend on your user session management, but you should always ensure that:
- The request to retrieve the results comes from an active user session (as per its
session_id
). - The
challenge
included in the response from the MATTR VII tenant matches thechallenge
you have stored for that active user session.
- The request to retrieve the results comes from an active user session (as per its
Adjust front channel to retrieve results via backend
Now that the backend is configured to retrieve the verification results, you need to adjust the front channel web application so that it can retrieve these results from the backend.
-
Copy and paste the following code under the
Step 6.1 - Create createRequest function
comment.tsxasync function createRequest() { const response = await fetch('/api/request', { method: 'POST' }) const { challenge } = await response.json() return challenge }
-
Replace the
challenge
assignment under theStep 6.2 - Use challenge from backend
comment with the following code to call thecreateRequest
function to generate a challenge:tsxchallenge: await createRequest(),
-
Add the following code under the
Step 6.3 - Create retrieveResults function
comment to create a new function that retrieves the verification results:tsxconst retrieveResults = useCallback(async (id: string) => { const response = await fetch(`api/results/${id}`) const { results } = await response.json() setResults(results as MATTRVerifierSDK.PresentationSessionResult) }, [])
This function uses the API route created above to retrieve the verification results and assign them to the
presentationResults
variable. -
Replace the
setResults
call under theStep 6.4 - Call retrieveResults function on cross-device callback
with the following code to call the newretrieveResults
function in the callback of a cross-device workflow:tsxretrieveResults(callbackResponse.result.sessionId)
-
Replace the
setResults
call under theStep 6.5 - Call retrieveResults function on same-device redirect
with the following code to call the newretrieveResults
function when the user is redirected to the app following a same-device workflow:tsxretrieveResults(presentationResult.sessionId)
Your web application now has an integrated backend that can validate verification results before passing them to the front channel, making the solution more secure.
Let’s test to ensure everything is working as expected.
Test the backend interaction
- Repeat the steps in the front channel testing step above.
- The interaction should look the same, but behind the scenes it will actually be handled by your backend.
What’s next?
- This project was run and tested locally using tunneling services. To build similar online
verification capabilities into a production web application, all you need to do is replace the
domain
andredirectUri
with the corresponding values of your production web application. - This project was created to showcase happy paths. It is recommended to review our comprehensive SDK reference documentation to learn more about available functions and classes.
- As always, feel free to reach out if you have any questions or encounter any issues we can help with.