This guide walks you through enabling DPoP (Demonstration of Proof-of-Possession) support in your Auth0-protected ASP.NET Core API.
Before enabling DPoP, ensure you have:
- ✅ An Auth0 account with DPoP enabled (contact Auth0 support if needed)
- ✅ An API configured in Auth0 with DPoP support
- ✅ The Auth0 ASP.NET Core API Authentication library installed
- ✅ Basic Auth0 authentication already configured (see Getting Started)
Add .WithDPoP() to your authentication configuration:
using Auth0.AspNetCore.Authentication.Api;
using Microsoft.AspNetCore.Authentication.JwtBearer;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuth0ApiAuthentication(options =>
{
options.Domain = builder.Configuration["Auth0:Domain"];
options.JwtBearerOptions = new JwtBearerOptions()
{
Audience = builder.Configuration["Auth0:Audience"]
};
}).WithDPoP(); // ✨ Enable DPoP with default settings
builder.Services.AddAuthorization();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();That's it! Your API now supports DPoP validation with sensible defaults.
Ensure your Auth0 API is configured to issue DPoP-bound tokens. Follow the Auth0 DPoP configuration guide to set up DPoP for your API and clients.
Note: DPoP requires Auth0 tenant configuration. Contact Auth0 support if you don't see DPoP options.
curl -X GET https://localhost:5001/api/protected \
-H "Authorization: DPoP <dpop-bound-access-token>" \
-H "DPoP: <dpop-proof-token>"curl -X GET https://localhost:5001/api/protected \
-H "Authorization: Bearer <regular-access-token>".WithDPoP(options =>
{
options.Mode = DPoPModes.Allowed;
});Behavior:
- ✅ Accepts DPoP-bound tokens with valid proofs
- ✅ Also accepts standard Bearer tokens
- 🔍 Validates DPoP when the
DPoPheader is present
.WithDPoP(options =>
{
options.Mode = DPoPModes.Required;
});Behavior:
- ✅ Only accepts DPoP-bound tokens with valid proofs
- ❌ Rejects standard Bearer tokens
- ❌ Rejects requests without the
DPoPheader
.WithDPoP(options =>
{
options.Mode = DPoPModes.Disabled;
});Behavior:
- ✅ Only standard JWT Bearer authentication
- ❌ No DPoP validation performed
- 🚫 DPoP headers are ignored
Start with Allowed mode to test DPoP without breaking existing clients:
builder.Services.AddAuth0ApiAuthentication(options =>
{
options.Domain = builder.Configuration["Auth0:Domain"];
options.JwtBearerOptions = new JwtBearerOptions()
{
Audience = builder.Configuration["Auth0:Audience"]
};
}).WithDPoP(options =>
{
options.Mode = DPoPModes.Allowed;
options.Leeway = 30; // 30 seconds tolerance
options.IatOffset = 300; // 5 minutes max age
});Enforce DPoP for all requests:
builder.Services.AddAuth0ApiAuthentication(options =>
{
options.Domain = builder.Configuration["Auth0:Domain"];
options.JwtBearerOptions = new JwtBearerOptions()
{
Audience = builder.Configuration["Auth0:Audience"]
};
}).WithDPoP(options =>
{
options.Mode = DPoPModes.Required;
options.Leeway = 10; // Tighter timing
options.IatOffset = 120; // 2 minutes max age
});Use a custom scheme name:
builder.Services
.AddAuthentication("MyCustomScheme")
.AddAuth0ApiAuthentication("MyCustomScheme", options =>
{
options.Domain = builder.Configuration["Auth0:Domain"];
options.JwtBearerOptions = new JwtBearerOptions()
{
Audience = builder.Configuration["Auth0:Audience"]
};
})
.WithDPoP("MyCustomScheme", options =>
{
options.Mode = DPoPModes.Allowed;
});Once DPoP is enabled, protect your endpoints as usual:
app.MapGet("/api/public", () =>
Results.Ok("Public endpoint - no authentication required"));
app.MapGet("/api/protected", () =>
Results.Ok("Protected endpoint - authentication required"))
.RequireAuthorization();[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
[HttpGet("public")]
public IActionResult Public()
{
return Ok("Public endpoint");
}
[Authorize]
[HttpGet("protected")]
public IActionResult Protected()
{
return Ok("Protected endpoint");
}
}DPoP validation is transparent - access claims normally:
[Authorize]
[HttpGet("user-info")]
public IActionResult GetUserInfo()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var email = User.FindFirst(ClaimTypes.Email)?.Value;
return Ok(new { userId, email });
}Problem: Requests with DPoP header are rejected
Solutions:
- Check that Auth0 issued a DPoP-bound token (look for
cnfclaim) - Verify the
DPoPproof is properly formed - Ensure
htm(HTTP method) andhtu(HTTP URI) match exactly - Check time synchronization -
iatmust be recent
Problem: Standard bearer tokens return 401
Solution: This is expected behavior in Required mode. Either:
- Switch to
DPoPModes.Allowedto support both - Update clients to send DPoP proofs
Problem: Valid-looking DPoP proofs are rejected due to timing
Solutions:
- Increase
IatOffsetto allow older proofs - Increase
Leewayfor clock skew tolerance - Ensure client and server clocks are synchronized
.WithDPoP(options =>
{
options.IatOffset = 600; // 10 minutes
options.Leeway = 60; // 1 minute leeway
});HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="invalid_dpop_proof",
error_description="DPoP proof validation failed"HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="invalid_request",
error_description="DPoP proof is missing"HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="invalid_token",
error_description="The cnf (confirmation) claim is missing or invalid"- DPoP Configuration - Detailed configuration options
- DPoP Overview - Understanding DPoP concepts
- Configuration Guide - General Auth0 configuration