Identity and trust
Migrating to Microsoft Entra Verified ID
A neutral playbook for issuing and verifying user-owned credentials with open standards, and wiring the verification step into an existing app.
Problem
Many apps verify identity attributes with a bespoke token: a signed JWT that only the issuing system can produce and only your app can parse. That locks the user into one issuer's flow and forces every verifier to build a custom integration. Microsoft Entra Verified ID uses open standards (decentralized identifiers, W3C verifiable credentials, OpenID for Verifiable Credentials) so a person can hold a credential in a wallet, such as Microsoft Authenticator, and present it to any verifier that trusts the issuer.
Options and trade-offs
- Issuer setup: bind a decentralized identifier to your domain (did:web), keep signing keys in Key Vault, grant the Request Service the permissions it needs, and define a credential contract (display and rules). The result is portable credentials, at the cost of setup steps and a dependency on a wallet.
- Verifier setup: a separate app, ideally in a separate tenant from the issuer, that creates presentation requests, receives callbacks and validates what comes back. It gets privacy-respecting, user-approved presentations instead of passwords or shared secrets.
- Hybrid designs: where a server must query an issuer continuously, user presentation is a poor fit. Combine it with server-side checks instead of replacing them.
My recommendation
Migrate one low-risk attribute first, such as proof of employment or membership, and keep the existing check running in parallel. Verify the credential's issuer, signature and status, and accept only the claims you need. Expand once the flow is reliable.
Minimal working example
The verifier flow has three parts: get a token for the Request Service, create a presentation request, and handle the callback.
Step 1: get an access token with the client credentials flow, using the Verifiable Credentials Service Request scope (confirm the exact scope value in the current docs):
curl -s -X POST "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
-d "client_id=<app-id>" \
-d "client_secret=<secret-or-use-a-certificate>" \
-d "grant_type=client_credentials" \
-d "scope=3db474b9-6a0c-4840-96ac-1fceb342124f/.default"
Step 2: create a presentation request:
POST https://verifiedid.did.msidentity.com/v1.0/verifiableCredentials/createPresentationRequest
Content-Type: application/json
Authorization: Bearer <token>
{
"authority": "did:web:verifier.example.com",
"includeQRCode": true,
"registration": { "clientName": "Example Verifier" },
"callback": {
"url": "https://verifier.example.com/api/presentation-callback",
"state": "<unique-request-id>",
"headers": { "api-key": "<shared-callback-secret>" }
},
"requestedCredentials": [
{
"type": "ProofOfEmployment",
"purpose": "Confirm your employer before granting access",
"acceptedIssuers": [ "did:web:issuer.example.com" ]
}
]
}
The response includes a QR code and a deep link for the wallet. Step 3: handle the callback and authorize only on a verified presentation:
// Callback handler (framework-agnostic sketch)
void onCallback(String apiKeyHeader, JsonNode body) {
if (!MessageDigest.isEqual(apiKeyHeader.getBytes(UTF_8), EXPECTED_KEY.getBytes(UTF_8))) {
throw new SecurityException("bad callback key");
}
String status = body.path("requestStatus").asText();
switch (status) {
case "request_retrieved" -> markScanned(body.path("state").asText());
case "presentation_verified" -> grantAccess(body.path("state").asText(),
body.path("verifiedCredentialsData"));
case "presentation_error" -> markFailed(body.path("state").asText());
default -> { /* ignore unknown statuses */ }
}
}
Test it: run the flow with a test credential and confirm access is granted only after presentation_verified. Then present a credential from an issuer that is not in acceptedIssuers and confirm it is rejected.
Checklist
- Use did:web with domain binding, serve it over HTTPS, and avoid redirects on the well-known path.
- Keep signing keys in Key Vault and plan rotation before you need it.
- Check the issuer DID against an allowlist, and confirm the credential status, not just the signature.
- Use a unique state value per request, and authenticate callbacks with a secret you compare in constant time.
- Request only the claims you will use, and explain the purpose to the user.
- Align on the supported standards (VC Data Model, OpenID4VC, Presentation Exchange) so you can work with other wallets and verifiers.
Where this doesn't apply
Scenarios that need continuous server-side queries to an issuer rather than a user presenting a credential. Use a hybrid design there.
Sources
- Verified ID introduction and standards — https://learn.microsoft.com/en-us/entra/verified-id/decentralized-identifier-overview
- Request Service REST API presentation specification — https://learn.microsoft.com/en-us/entra/verified-id/presentation-request-api
- Configure your tenant for Verified ID — https://learn.microsoft.com/en-us/entra/verified-id/verifiable-credentials-configure-tenant
---