Skip to Content
GuidesOID4VCI🎓 Interaction hook tutorial

Learn how to integrate an interaction hook into an OID4VCI workflow

Introduction

In an OpenID4VCI issuance workflow you can enhance the credential issuance process with custom interactions by configuring an Interaction hook. This could include gathering additional information, introducing extra authentication steps, or communicating the terms of service. This allows you to create a more tailored and engaging credential issuance experience.

Interaction hooks are only available for the OID4VCI Authorization Code flow

This tutorial focuses on guiding you through the process of setting up an OID4VCI workflow that redirects the user to a custom interaction where they can provide additional details that will be used in the issued credential.

Prerequisites

We recommend using the MATTR VII Postman collection in this tutorial. While this isn’t an explicit prerequisite it can really speed things up.

Tutorial overview

The current tutorial builds upon the get started with OID4VCI tutorial by adding the following steps:

  1. Set up a sample web application: Deploy a sample Next.js web app that acts as the custom interaction component in the issuance flow. In this example, the app lets users update their preferred name and pronoun, which will be included in the issued credential. You can extend this app to collect any additional information or add custom steps such as extra authentication, biometrics checks or liveness checks, as needed for your workflow.
  2. Integrate the Interaction hook with MATTR VII: Configure MATTR VII to redirect the user to the Interaction hook as part of the issuance flow, and then handle any information returned by the Interaction hook as the user is redirected back to the issuance flow.

Tutorial steps

Start a ngrok tunnel on your local machine

  1. Start an ngrok tunnel in a new terminal window:
BASH
ngrok http http://localhost:3000

Your terminal window should show the tunnel details:

ngrok tunnel

  1. Make note of the Forwarding value, we will use it in the next step.

Configure an Interaction hook on your MATTR VII tenant

Request

Make a request of the following structure to configure a new Interaction hook:

HTTP request
PUT /v1/openid/configuration
Request body
{ "interactionHook": { "url": "https://7658-3-24-140-84.ngrok-free.app", "disabled": false } }
  • url : Replace with the Forwarding URL displayed in the terminal after starting the ngrok tunnel. This URL will be used to redirect the user to your custom interaction component during the OID4VCI workflow.
  • disabled : Set to false to enable the Interaction hook. If set to true (or if disabled is not provided), the Interaction hook will not be used during the OID4VCI workflow.

Response

Response
{ "interactionHook": { "url": "https://7658-3-24-140-84.ngrok-free.app", "disabled": false, "secret": "uwIkhw************************************" } }
  • secret : The secret is a 32-byte array, provided as a Base64-encoded string. You will use this secret later in the tutorial.

Once the Interaction hook is configured and enabled (disabled is set to false), any OID4VCI issuance workflow will redirect the user to the Interaction hook component after they have authenticated.

Currently any tenant can only have a single Interaction hook configured. You can combine several custom interactions as part of the issuance workflow by building them into a single Interaction hook component.

Currently there is nothing running on the Interaction hook URL, so the next step is to set up a local web application that the user can interact with.

Setup a local web application as an Interaction hook component

Next, you will set up a local Interaction hook component that allows the user to update their preferred name. MATTR VII will use this updated name in the issued credential. This component is a Next.js application that simulates a custom interaction, which the user is redirected to during the issuance flow.

We are using Next.js because it allows us to show both the server side and client side responsibilities of the web app.

Interaction hook components can be either a web or native application. We recommend using web applications as they are more compatible with most scenarios.

Perform the following steps to setup the Interaction hook application:

  1. Clone the MATTR Sample Apps repo to your machine and navigate to the interaction-hook-app folder.

  2. Rename the .env-example file to .env.

  3. Open the .env file in a text editor and update the following variables:

    • INTERACTION_HOOK_SECRET: Replace this with the secret value returned in the response when you configured the Interaction hook in the previous step.
    • APP_URL: Replace this with the Forwarding URL displayed in the terminal after starting the ngrok tunnel. This URL will be used to redirect the user to your custom interaction component.
    • ISSUER_TENANT_URL: Replace this with the URL of your MATTR VII tenant, for example: https://learn.vii.au01.mattr.global.
  4. Install and start the app by running the following command in the terminal:

    BASH
    npm install npm run dev

Your Interaction hook component is now set up and ready to be used in the OID4VCI workflow. Let’s test that the interaction works as expected.

Test the workflow

To test this workflow you will use the credential offer created as part of the OID4VCI Authorization Code tutorial.

  1. Open the GO hold example app.
  2. Select Scan.
  3. Scan the QR code generated when you created the credential offer.
  4. Review the credential offer and select Accept.
  5. Complete authentication.
  6. Following authentication you will be redirected to the Interaction hook component.
  7. Update your preferred name and select Submit (make sure you use a different name than the one you setup for your Auth0 user to see the difference in the issued credential). This will redirect you back to the OID4VCI workflow.
  8. You will be redirected back to the OID4VCI workflow, where you can review the credential and select Accept.
  9. The new credential will be issued using the name you provided in the Interaction hook component, replacing the original value retrieved from the authentication provider.

Now that the workflow is working, let’s break down our Interaction hook application and understand how it is integrated into the OID4VCI workflow.

Breaking down the Interaction hook component

The sample interaction hook application provided in this tutorial is intentionally simple. While it contains some basic features that are not covered here, we will focus on the essential functionalities required for any Interaction hook component integrated with an OID4VCI Authorization Code flow:

  1. Use the Interaction hook JWT decoded payload: The application can extract different claims from the decoded JWT payload and use them in the interaction.
  2. Sign the Interaction hook response JWT and redirect back to MATTR VII: Once the user completes the interaction, the application must generate and sign a new JWT token and include it as a query parameter when redirecting the user back to MATTR VII.

Use the Interaction hook JWT decoded payload

MATTR VII redirects users to your Interaction hook component with a JWT object passed as a session_token query parameter, as shown in the following example:

https://7658-3-24-140-84.ngrok-free.app?session_token=ey...

Your application can decode this JWT object, extract different claims from the decoded payload and use them in the interaction. The following code snippet shows the structure of the decoded JWT payload retrieved from the Interaction hook configured above:

Decoded example JWT payload
{ "state": "hJvfiSp3eEGybd-KmL8ja", "scopes": ["ldp_vc:CourseCredential"], "claims": { "name": "John Doe", "email": "example@example.com" }, "authenticationProvider": { "url": "https://myidentityprovider.auth0.com", "subjectId": "user|123456789" }, "redirectUrl": "https://learn.vii.au01.mattr.global/v1/oauth/interaction/hJvfiSp3eEGybd-KmL8ja/interactionhook/callback", "sub": "a44a7f92-c61e-48a0-88b6-863eeeb58394", "aud": "<INTERACTION_HOOK_URL>", "iss": "https://learn.vii.au01.mattr.global", "iat": 1673910963, "exp": 1673911263 }
  • state : A unique value associated with each Interaction hook session. You will use it in the next step when signing the response JWT so that MATTR VII can match the response to the correct credential issuance flow.
  • scopes : Scopes retrieved from user authentication workflow.
  • claims : User claims defined when you configured your interaction hook.
  • authenticationProvider : A provider that the user has authenticated with.
    • url : URL of the Authentication provider.
    • subjectId : Subject Identifier of the end user with this Authentication provider.
  • redirectUrl : URL to redirect to when users complete the Interaction hook journey.
  • sub : User identifier in MATTR VII.
  • aud : Interaction hook URL.
  • iss : Your MATTR VII tenant.
  • iat : Issued at (Epoch Unix timestamp)
  • exp : Expires at (Epoch Unix timestamp).

Now let’s take a look at how the sample application uses the first_name and last_name claims from the JWT payload to allow the user to update their preferred name:

src/app/page.tsx
import { decodeJwt } from 'jose' import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useMemo } from 'react' // Optional validation for claims // const passedClaims = z.object... export default function Home() { const router = useRouter() // Get the session token from the URL const searchParams = useSearchParams() const sessionToken = searchParams.get('session_token') // Decode the JWT to use the claims const decodedToken = useMemo(() => { if (!sessionToken) return null try { return decodeJwt(sessionToken) } catch (err) { return null } }, [sessionToken]) // Optional: Use ZOD to parse the claims from the decoded JWT // const claims = useMemo(() => { // if (!decodedToken) return null; // try { // const parsed = passedClaims.parse(decodedToken.claims); // return parsed; // } catch (err) { // return null; // } // }, [decodedToken]); const handleSubmit = useCallback(async () => { // Perform any last minute validation, etc. try { // Form Submission Handler - Step 4.5: Send data to backend API const response = await fetch('/api/interaction-hook', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: sessionToken, data: { // Pass your updated claims here } }) }) // Handle any errors... const result = await response.json() // The redirect URL contains the signed JWT response router.push(result.redirect) } catch (err) { console.error(err) } }, [sessionToken]) return <form>// Render the UI form - use the claims to pre-fill the form</form> }
  1. The decoded JWT payload is passed into the rendered React page and the name claim is used to pre-fill a form.
  2. The user can update their name and submit the form, which triggers the next step in which the application signs a new JWT and redirects the user back to MATTR VII.

Sign the Interaction hook response JWT and redirect back to MATTR VII

Once the user completes the interaction (in our example this is when they submit the form), your application must sign a new JWT using the secret provided by MATTR VII and redirect the user back to MATTR VII. This allows the issuance flow to continue with the updated information.

Your application backend must first ensure that the original request is legitimate and originates from MATTR VII by verifying the JWT object using the interaction hook secret provided in the response when you configured the Interaction hook.

Here’s how the sample application generates and signs the response JWT:

src/app/api/interaction-hook/route.ts
import { jwtVerify, SignJWT, type JWTVerifyResult } from 'jose' // Optional request schema // const requestSchema = z.object... export async function POST(request: Request) { try { // Parse request from client const body = await request.json() // Optional: use ZOD to parse & validate the request // const { token, data } = requestSchema.parse(body); const { token, data } = body // Load environment variables const secret = process.env.INTERACTION_HOOK_SECRET const issuerUrl = process.env.ISSUER_TENANT_URL const appUrl = process.env.APP_URL // Error handling... // MATTR VII provides the secret as base64-encoded when you create the interaction hook const secretBuffer = Buffer.from(secret, 'base64') // Verify the JWT from MATTR VII let verifiedJwt: JWTVerifyResult try { verifiedJwt = await jwtVerify(token, secretBuffer, { issuer: issuerUrl, audience: appUrl }) } catch (verifyError: unknown) { return Response.json({ error: 'Invalid session token' }, { status: 401 }) } // Sign new JWT with update claims const responseJwt = await new SignJWT({ iss: appUrl, // Issuer: Your app aud: issuerUrl, // Audience: MATTR VII tenant state: verifiedJwt.payload.state, // Session state from original request // Custom claims to be added to the credential claims: { name: data.name, pronouns: data.pronouns }, // Claims to persist in MATTR VII (empty in this example) // You could store data here for future interactions claimsToPersist: [] }) .setProtectedHeader({ alg: 'HS256', typ: 'JWT' }) .setIssuedAt() .setExpirationTime('1m') // JWT expires in 1 minute .sign(secretBuffer) // Construct return URL for the frontend to redirect to const redirectUrl = `${verifiedJwt.payload.redirectUrl}?session_token=${responseJwt}` // return response to the client return Response.json({ redirect: redirectUrl, // Include debug info in development only ...(process.env.NODE_ENV === 'development' && { debug: { claimsProcessed: Object.keys(data), jwtCreated: true } }) }) } catch (error) { // Handle any errors... } }
  1. The handler verifies the original session_token and extracts the state, redirectUrl, and other identifiers.
  2. It signs a new JWT (interaction_token) with the updated preferred_name claim. The interaction_token must include the state value from the original session to ensure that MATTR VII can match the response to the correct credential issuance flow.
  3. The response is a 302 redirect back to the redirectUrl, with the signed JWT included as a query parameter.
  4. MATTR VII will validate this signed JWT and continue the issuance process using the data you’ve included in the response. This means that you can use the new claims.preferred_name value in the issued credential.

What’s next?

Check out more resources on MATTR Learn that will enable you to:

  • Configure a Claims source to retrieve data from compatible data sources and use it in the issued credential.
  • Apply branding to issued credentials as part of creating a Credential configuration.
Last updated on