Tutorial: OpenID4VP¶
Implement credential presentation using the OpenID for Verifiable Presentations protocol.
Time: 20 minutes
Level: Intermediate
Sample: samples/SdJwt.Net.Samples/02-Intermediate/04-OpenId4Vp.cs
What you will learn¶
- OpenID4VP authorization request flow
- Presentation definition creation
- Response handling and validation
Simple explanation¶
OID4VP is the protocol for proving something from a wallet to a verifier. The verifier sends a request describing what it needs, the wallet finds matching credentials, and sends back a presentation with only the required claims.
Packages used¶
| Package | Purpose |
|---|---|
SdJwt.Net.Oid4Vp |
OID4VP protocol models and validation |
SdJwt.Net.PresentationExchange |
Credential matching (DIF PE) |
Where this fits¶
flowchart LR
A["Issuer"] -->|"OID4VCI"| B["Wallet"]
B -->|"OID4VP"| C["Verifier"]
style B fill:#2a6478,color:#fff
style C fill:#2a6478,color:#fff
OID4VCI vs OID4VP¶
| Aspect | OID4VCI | OID4VP |
|---|---|---|
| Direction | Issuer to Wallet | Wallet to Verifier |
| Purpose | Receive a credential | Prove claims from a credential |
| Request starts at | Issuer (offer) | Verifier (authorization request) |
| Key binding | Wallet proves key at issuance | Wallet proves key at presentation |
| Query language | N/A | DCQL or Presentation Exchange |
Protocol overview¶
sequenceDiagram
participant Wallet
participant Verifier
Verifier->>Wallet: 1. Authorization Request (with presentation_definition)
Wallet->>Wallet: 2. User consent
Wallet->>Verifier: 3. Authorization Response (with vp_token)
Verifier->>Verifier: 4. Verification
Step 1: Verifier creates request¶
using SdJwt.Net.Oid4Vp.Models;
using SdJwt.Net.PresentationExchange.Models;
var authRequest = new AuthorizationRequest
{
ResponseType = "vp_token",
ClientId = "https://verifier.example.com",
RedirectUri = "https://verifier.example.com/callback",
Nonce = Guid.NewGuid().ToString(),
State = "session-state-123",
PresentationDefinition = new PresentationDefinition
{
Id = "employment-verification",
InputDescriptors = new[]
{
new InputDescriptor
{
Id = "employee-credential",
Name = "Employment Proof",
Purpose = "Verify current employment",
Constraints = new Constraints
{
Fields = new[]
{
new Field
{
Path = new[] { "$.vct" },
Filter = new FieldFilter
{
Type = "string",
Const = "https://hr.example/EmployeeCredential"
}
},
new Field
{
Path = new[] { "$.employer_name" }
}
}
}
}
}
}
};
Step 2: Send request to wallet¶
// Option A: Same-device (deep link)
var requestUri = $"openid4vp://?{BuildQueryString(authRequest)}";
// Option B: Cross-device (QR code)
var qrContent = $"openid4vp://?request_uri={Uri.EscapeDataString(hostedRequestUri)}";
Step 3: Wallet processes request¶
// Parse authorization request
var request = ParseAuthorizationRequest(requestUri);
// Find matching credentials in wallet
var matchingCredentials = wallet.FindCredentials(request.PresentationDefinition);
// User selects which credentials to present
var selectedCredential = matchingCredentials.First();
// Create selective presentation
var holder = new SdJwtHolder(selectedCredential);
var presentation = holder.CreatePresentation(
d => d.ClaimName == "employer_name", // Only required fields
kbJwtPayload: new JwtPayload
{
["aud"] = request.ClientId,
["nonce"] = request.Nonce
},
kbJwtSigningKey: holderKey,
kbJwtSigningAlgorithm: SecurityAlgorithms.EcdsaSha256
);
Step 4: Wallet sends response¶
var authResponse = new AuthorizationResponse
{
VpToken = presentation,
PresentationSubmission = new PresentationSubmission
{
Id = Guid.NewGuid().ToString(),
DefinitionId = request.PresentationDefinition.Id,
DescriptorMap = new[]
{
new DescriptorMapEntry
{
Id = "employee-credential",
Format = "dc+sd-jwt",
Path = "$"
}
}
},
State = request.State
};
// POST to redirect_uri
await httpClient.PostAsync(request.RedirectUri, authResponse);
Step 5: Verifier validates response¶
// Validate state matches
if (response.State != expectedState)
throw new SecurityException("State mismatch");
// Verify the SD-JWT presentation
var verifier = new SdVerifier(ResolveIssuerKey);
var result = await verifier.VerifyAsync(
response.VpToken,
sdJwtParams,
kbJwtParams,
expectedNonce
);
// Check presentation submission matches definition
ValidatePresentationSubmission(
response.PresentationSubmission,
originalRequest.PresentationDefinition
);
// Extract verified claims
var employerName = result.ClaimsPrincipal.FindFirst("employer_name")?.Value;
Console.WriteLine($"Verified employment at: {employerName}");
Response modes¶
Direct post¶
Response sent directly to verifier backend:
var request = new AuthorizationRequest
{
ResponseMode = "direct_post",
ResponseUri = "https://verifier.example.com/response"
};
Fragment¶
Response in URL fragment (same-device):
var request = new AuthorizationRequest
{
ResponseMode = "fragment",
RedirectUri = "https://verifier.example.com/callback"
};
Multiple credentials¶
Request multiple credentials at once:
var definition = new PresentationDefinition
{
Id = "full-verification",
InputDescriptors = new[]
{
new InputDescriptor { Id = "id-credential", ... },
new InputDescriptor { Id = "address-credential", ... },
new InputDescriptor { Id = "employment-credential", ... }
}
};
Presentation Exchange validation¶
When the request uses presentation_definition, keep the shared PEX definition and pass it to the OID4VP validator. Validation then checks the wallet's presentation_submission against the verified disclosed claims.
var options = VpTokenValidationOptions.CreateForOid4Vp("https://verifier.example.com");
options.ExpectedPresentationExchangeDefinition = expectedPresentationDefinition;
var result = await validator.ValidateAsync(response, expectedNonce, options);
Run the sample¶
cd samples/SdJwt.Net.Samples
dotnet run -- 2.4
Next steps¶
- Presentation Exchange - Advanced query syntax
- OpenID Federation - Trust establishment
Key takeaways¶
- OpenID4VP standardizes credential presentation
- Presentation definitions specify required credentials
- Wallet creates selective disclosures based on requirements
- OID4VP validation can enforce Presentation Exchange constraints after token verification
- Nonces prevent replay attacks
Expected output¶
Authorization request created with nonce
Wallet matched 1 credential
Presentation submitted with KB-JWT
Verifier: signature valid, nonce matches
Demo vs production¶
In production, the wallet receives the authorization request via a deep link or QR code. The response goes to the verifier's response_uri endpoint. The redirect_uri is not used in OID4VP; response_uri is the correct parameter.
Common mistakes¶
- Using
redirect_uriinstead ofresponse_uriin the authorization request (OID4VP usesresponse_uri) - Forgetting the nonce in the authorization request (required for replay protection)