Skip to main content

Presentation

Presentation is the process of having a credential holder present credentials from their wallet, to prove claims about themselves to a verifier.

Presentation is a multi-step process:

  1. Create a presentation request
  2. Display QR code or open link
  3. Receive presentation notification
presentation interaction diagrampresentation interaction diagram

Create presentation request

tip

To create a presentation request definition and explore the presentation API, use the presentation builder as a starting point.

The presentation request input specifies:

  1. One or more requested credentials. For partner types, the issuer must be specified, otherwise just the credential type is required.
    • Optionally, include constraints to narrow what credential can be presented
    • "VerifiableCredential" is a special type that permits the presentation of any valid credential.
  2. The name of the requesting client application (for display in the wallet prompt)
  3. The identity who must present the credential. When specified, a constraint for identityId is added to each requested credential.
  4. Other optional input:
    1. For each requested credential, whether revoked credentials can be presented. It is recommended that the requesting application handle presentation of revoked credentials gracefully.
    2. Callback info, if presentation event data should also be sent to a server-side endpoint
CreatePresentationRequest mutation: will return a response with presentation request or error details
mutation CreatePresentationRequest($request: PresentationRequestInput!) {
createPresentationRequest(request: $request) {
... on PresentationResponse {
requestId
url
qrCode
expiry
}
... on RequestErrorResponse {
error {
code
message
innererror {
code
message
target
}
}
}
}
}
Example variables requesting internal and partner credentials, allowing revoked, with optional callback defined
{
"request": {
"requestedCredentials": [
{
"type": "VerifiedContractor",
"configuration": {
"validation": {
"allowRevoked": true
}
}
},
{
"type": "TrueIdentity",
"acceptedIssuers": "did:ion:EiDXOEH-<DID data>",
"configuration": {
"validation": {
"allowRevoked": true
}
}
}
],
"includeQRCode": true,
"registration": {
"clientName": "Test client"
},
"callback": {
"headers": {
"Authorization": "MySecretTokenToCallMyEndpoint"
},
"url": "https://enr3r4d4gvwrb.x.pipedream.net",
"state": "Any extra state you need to correlate the request with the callback"
}
}
}
Example response of the presentation request
{
"data": {
"createPresentationRequest": {
"requestId": "<request id>",
"url": "openid-vc://?request_uri=https://verifiedid.did.msidentity.com/v1.0/tenants/<tenant ID>/verifiableCredentials/presentationRequests/<request id>",
"qrCode": "data:image/png;base64,iVBORw0KG..<base64 encoded image data>",
"expiry": 1695285131
}
}
}

Once the presentation request is created, it must be opened by Microsoft Authenticator (or custom wallet application) to complete the presentation.

The presentation request can be opened in one of two ways:

  • On a desktop or web application, display a QR code that the recipient can scan with their mobile device.
  • On a mobile application, the URL can be opened directly.
tip

For further info on how to handle presentation links and QR codes, see the wallet link handling guide.

Display QR code

Example display QR code
<img src={response.qrCode} alt="Scan this QR code to present credential(s)" />
Example which opens the link automatically if running on a mobile device
if ('error' in response) displayError(response.error)
else if (/Android|iPhone/i.test(navigator.userAgent)) window.location.href = response.url
else displayQrCode(response.qrCode)

Handle presentation received event

Your application can subscribe to presentation events to be notified when the presentation is received by the wallet.

PresentationEvent subscription notifies when presentation is A) received and B) completed successfully
  subscription PresentationEvent($requestId: ID!) {
presentationEvent(where: { requestId: $requestId }) {
event {
requestStatus
}
presentation {
id
presentedAt
# select presentation data here
}
}
}
}
Event data when presentation is received
{
"data": {
"presentationEvent": {
"event": {
"requestStatus": "request_retrieved"
}
}
}
}
tip

Presentation received events can be received via the PresentationEvent subscription and the server-side callback, however you cannot poll for presentation received events because they are not stored.

Receive presentation notification

Upon successful presentation, the requesting application can receive data by any combination of:

  1. Subscription to the presentation event
  2. Polling for presentation data
  3. Server-side callback
Example event data for successfull presentation of both internal and partner type credentials
{
"data": {
"presentationEvent": {
"event": {
"requestStatus": "presentation_verified"
},
"presentation": {
"presentedAt": "2023-10-25T06:03:28.443Z",
"identity": {
"id": "<GUID>",
"identifier": "<GUID>",
"issuer": "<identity issuer>",
"name": "Mary Smith"
},
"presentedCredentials": [
{
"issuer": "did:ion:EiDKU4Ss...<DID data>",
"type": ["VerifiableCredential", "VerifiedContractor"],
"claims": {
"issuanceId": "<GUID>",
"name": "Mary Smith"
},
"credentialState": {
"revocationStatus": "VALID"
}
},
{
"issuer": "did:ion:EiDXOEH-<DID data>",
"type": ["VerifiableCredential", "TrueIdentity"],
"claims": {
"firstName": "Mary",
"lastName": "Smith",
"scanneddoc": "NY State Drivers License",
"selfie": "Verified Selfie",
"verification": "Fully Verified",
"address": "2345 Anywhere Street, Your City, NY 12345",
"ageverified": "Older than 21"
},
"credentialState": {
"revocationStatus": "VALID"
}
}
],
"issuances": [
{
"id": "<GUID>",
"status": "active",
"issuedAt": "2023-10-10T02:48:44.983Z",
"expiresAt": "2024-10-09T02:48:44.970Z",
"revokedAt": null,
"contract": {
"id": "<GUID>",
"name": "Skysprint contractor credential",
"credentialTypes": ["VerifiedContractor"],
"display": {
"card": {
"title": "Verified Skysprint contractor",
"issuedBy": "Skysprint Pty Ltd",
"backgroundColor": "#ffffff",
"textColor": "#000000",
"description": "This credential verifies you are a Skysprint contractor",
"logo": {
"uri": "https://vodevvrfdorchstnst.blob.core.windows.net/logo-images/AA7ED447-232D-400A-9D96-BD0C9F4E108D.png",
"description": "Skysprint company logo"
}
}
}
}
}
],
"partners": [
{
"id": "C4B8B982-C804-4891-AFEE-716BDB1EC636",
"did": "did:ion:EiDXOEH-<DID data>",
"name": "Wood Grove Demo",
"credentialTypes": ["TrueIdentity", "VerifiedEmployee"],
"linkedDomainUrls": ["https://did.woodgrovedemo.com/"]
}
]
}
}
}
}
tip

For complete code samples of different methods of receiving presentation events, view the presentation samples for your development stack.

Verify presentation

Every presentation includes a receipt, a digital proof that a credential was presented. It consists of one or two JSON Web Tokens (JWTs) that bundle together claims and proof of presentation.

The PresentationReceipt has two properties:

  • id_token (Required): Is a record of the presentation, signed by the user's wallet, proving ownership.
  • faceCheck (Optional): Is a signed record of the biometric face check, if performed during the presentation process.

Using verifyPresentation query

VO provides a GraphQL query to verify the presentation receipt. This query checks the validity of the id_token and, if present, the faceCheck token.

Example verifyPresentation query
query VerifyPresentation($receipt: PresentationReceiptInput!, $presentedAt: String!) {
verifyPresentation(receipt: $receipt, presentedAt: $presentedAt) {
faceCheckValid
idTokenValid
}
}
Example variables
{
"request": {
"receipt": {
"faceCheck": "eyJhbG…gZmFqg.eyJzdW…Y2lvdHc.sgnNMi…HltdfA",
"id_token": "eyJ0eX…y5cCI6.eyJ1c2…Z2VybmFt.ZXlKaG…jdUxk"
},
"presentedAt": "2023-11-18T14:27:53.123Z"
}
}
Example response
{
"data": {
"verifyPresentation": {
"faceCheckValid": true,
"idTokenValid": true,
"__typename": "VerifyPresentationResult"
}
}
}

Manual verification

You can verify presentation receipts yourself without relying on VO queries. This guide shows you how to do it with jose, a universal JWT library. jsonwebtoken is another commonly used library for verification.

The core of the process is the jwtVerify function from jose. To use it, you need to provide the token, the correct public key for verification, and a set of options to validate the token's claims.

await jwtVerify(token, publicKey, options)
note

Receipt tokens come with a 5 minute expiry, therefore validation must be run specifying the presentation timestamp i.e. via presentation.createdAt.

Here’s how to get the required parameters for the jwtVerify function.

1. The token

This is the raw, encoded JWT string from the presentation receipt. It will be either the id_token or the optional faceCheck token.

2. The publicKey

The public key must be resolved from the Key ID (kid) found in the JWT's protected header. The kid is a Decentralized Identifier (DID) that points to the key.

  1. Decode the Header: First, use decodeProtectedHeader(token) from jose to get the kid and signing algorithm (alg).
  2. Resolve the Key from the DID: The prefix of the kid (e.g., did:jwk:, did:web:, did:ion:) determines how you retrieve the public key JWK.
  3. Import the Key: Once you have the JWK, use importJWK(jwk, alg) to prepare it for the jwtVerify function.

3. The options Object

The options object is crucial for validating the token's claims, such as the issuer and audience. These values differ depending on whether you are verifying the id_token or the faceCheck token.

OptionFor id_tokenFor faceCheck token
algorithms[alg] (from header)[alg] (from header)
issuer'https://self-issued.me/v2/openid-vc'Your Verifier's DID (e.g., did:web:your-domain.com).
audienceYour Verifier's DID (e.g., did:web:your-domain.com)undefined (do not include)
currentDateThe presentedAt Date objectThe presentedAt Date object
tip

You can query for your Verifier's DID using Authority query

Code Sample

This TypeScript example demonstrates the full verification flow for an id_token using jose.

import { Buffer } from 'buffer'
import { decodeProtectedHeader, importJWK, jwtVerify } from 'jose'

/**
* Resolves a public key from a DID in the 'kid' header.
* This is a simplified example focusing on the 'did:jwk' method.
*/
async function resolvePublicKey(kid: string, alg: string): Promise<CryptoKey> {
if (kid.startsWith('did:jwk:')) {
const jwkEncoded = kid.replace('did:jwk:', '').split('#')[0]
const jwkJson = Buffer.from(jwkEncoded, 'base64url').toString('utf8')
const jwk = JSON.parse(jwkJson)
return (await importJWK(jwk, alg)) as CryptoKey
}
// For other DID methods like 'did:web' or 'did:ion', you would add
// logic here to fetch the DID document and find the corresponding key.
throw new Error(`Unsupported DID method for this example: ${kid}`)
}

/**
* Manually verifies a presentation id_token.
*/
async function verifyPresentationToken(token: string, presentedAt: string, verifierDid: string) {
try {
// 1. Decode header to get key ID (kid) and algorithm (alg)
const { kid, alg } = decodeProtectedHeader(token)
if (!kid || !alg) {
throw new Error('JWT missing "kid" or "alg" in its header.')
}

// 2. Resolve the public key using the kid
const publicKey = await resolvePublicKey(kid, alg)

// 3. Construct the options object for verification
/**
* The https://self-issued.me/v2/openid-vc issuer claim used by Microsoft Authenticator and other SIOP v2-compliant wallets
* when issuing self-issued verifiable presentations.
*
* See:
* - https://openid.net/specs/openid-connect-self-issued-v2-1_0.html
*/
const options = {
algorithms: [alg],
audience: verifierDid,
issuer: 'https://self-issued.me/v2/openid-vc',
currentDate: new Date(presentedAt),
}

// 4. Verify the token's signature and claims
const { payload } = await jwtVerify(token, publicKey, options)

console.log('✅ Verification Successful! Payload:', payload)
return { success: true, payload }
} catch (error) {
console.error('❌ Verification Failed:', error.message)
return { success: false, error: error.message }
}
}