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:
Create presentation request
To create a presentation request definition and explore the presentation API, use the presentation builder as a starting point.
The presentation request input specifies:
- 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.
- The name of the requesting client application (for display in the wallet prompt)
- The identity who must present the credential. When specified, a constraint for identityId is added to each requested credential.
- Other optional input:
- For each requested credential, whether revoked credentials can be presented. It is recommended that the requesting application handle presentation of revoked credentials gracefully.
- Callback info, if presentation event data should also be sent to a server-side endpoint
mutation CreatePresentationRequest($request: PresentationRequestInput!) {
createPresentationRequest(request: $request) {
... on PresentationResponse {
requestId
url
qrCode
expiry
}
... on RequestErrorResponse {
error {
code
message
innererror {
code
message
target
}
}
}
}
}
{
"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"
}
}
}
{
"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
}
}
}
Display QR code or open link
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.
For further info on how to handle presentation links and QR codes, see the wallet link handling guide.
Display QR code
<img src={response.qrCode} alt="Scan this QR code to present credential(s)" />
Open link on 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.
subscription PresentationEvent($requestId: ID!) {
presentationEvent(where: { requestId: $requestId }) {
event {
requestStatus
}
presentation {
id
presentedAt
# select presentation data here
}
}
}
}
{
"data": {
"presentationEvent": {
"event": {
"requestStatus": "request_retrieved"
}
}
}
}
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:
- Subscription to the presentation event
- Polling for presentation data
- Server-side callback
{
"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/"]
}
]
}
}
}
}
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.
query VerifyPresentation($receipt: PresentationReceiptInput!, $presentedAt: String!) {
verifyPresentation(receipt: $receipt, presentedAt: $presentedAt) {
faceCheckValid
idTokenValid
}
}
{
"request": {
"receipt": {
"faceCheck": "eyJhbG…gZmFqg.eyJzdW…Y2lvdHc.sgnNMi…HltdfA",
"id_token": "eyJ0eX…y5cCI6.eyJ1c2…Z2VybmFt.ZXlKaG…jdUxk"
},
"presentedAt": "2023-11-18T14:27:53.123Z"
}
}
{
"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)
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.
- Decode the Header: First, use
decodeProtectedHeader(token)
fromjose
to get thekid
and signing algorithm (alg
). - 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. - Import the Key: Once you have the JWK, use
importJWK(jwk, alg)
to prepare it for thejwtVerify
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.
Option | For id_token | For 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 ). |
audience | Your Verifier's DID (e.g., did:web:your-domain.com ) | undefined (do not include) |
currentDate | The presentedAt Date object | The presentedAt Date object |
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 }
}
}