← Duende SkillsCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Duende Skills
Snapshot Sep 30, 2026 · 23:14 UTC · version 0.3.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Advanced token security features in Duende IdentityServer including DPoP, mTLS certificate binding, Pushed Authorization Requests (PAR), JWT Secured Authorization Requests (JAR), and FAPI 2.0 compliance configuration.",
"included_files": [],
"name": "identityserver-token-security",
"skill_md_contents": "---\nname: identityserver-token-security\ndescription: Advanced token security features in Duende IdentityServer including DPoP, mTLS certificate binding, Pushed Authorization Requests (PAR), JWT Secured Authorization Requests (JAR), and FAPI 2.0 compliance configuration.\ninvocable: false\n---\n\n# Advanced Token Security (DPoP, mTLS, PAR, JAR, FAPI)\n\n## When to Use This Skill\n\n- Implementing Proof-of-Possession (PoP) tokens with DPoP or mTLS\n- Configuring Pushed Authorization Requests (PAR) for front-channel parameter security\n- Setting up JWT Secured Authorization Requests (JAR) for tamperproof authorize requests\n- Building FAPI 2.0 compliant authorization servers\n- Choosing between DPoP and mTLS for sender-constrained tokens\n- Configuring APIs to validate proof-of-possession tokens\n- Meeting regulatory or industry security requirements (open banking, e-health, e-government)\n\nDocs: https://docs.duendesoftware.com/identityserver/tokens/\n\n## Proof-of-Possession Tokens: Why They Matter\n\nDefault OAuth access tokens are **bearer tokens** -- anyone who possesses the token can use it. If a token leaks, a malicious third party can impersonate the client/user.\n\n**Proof-of-Possession (PoP) tokens** are cryptographically bound to the client that requested them via the `cnf` (confirmation) claim:\n\n```json\n{\n \"iss\": \"https://identity.example.com\",\n \"aud\": \"urn:api\",\n \"client_id\": \"web_app\",\n \"sub\": \"88421113\",\n \"cnf\": \"confirmation_method\"\n}\n```\n\nWhen using reference tokens, the `cnf` claim is returned from the introspection endpoint.\n\n## DPoP vs mTLS: Decision Matrix\n\n| Factor | DPoP | mTLS |\n| ------------------------- | ---------------------------------- | -------------------------------------------------- |\n| **Edition required** | Enterprise | All editions (binding); Enterprise (some features) |\n| **Minimum version** | 6.3 | All versions |\n| **Key management** | Application-layer JWK (dynamic) | X.509 certificate (TLS layer) |\n| **Infrastructure** | No TLS changes needed | Requires TLS client certificate infrastructure |\n| **Deployment complexity** | Lower | Higher (certificate distribution, renewal) |\n| **Protocol layer** | HTTP headers (`DPoP` header) | TLS channel |\n| **Public clients** | Supported (mobile/SPA) | Harder for public clients |\n| **FAPI 2.0** | Accepted | Accepted |\n| **Replay protection** | Nonce mechanism + `iat` validation | TLS channel binding |\n| **Recommendation** | Start here for most use cases | When TLS infrastructure already exists |\n\n## Mutual TLS (mTLS)\n\n### How It Works\n\nIdentityServer embeds the SHA-256 thumbprint of the client's X.509 certificate into the access token via the `cnf` claim:\n\n```json\n{\n \"cnf\": { \"x5t#S256\": \"bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2\" }\n}\n```\n\nThe client must use the same certificate when calling APIs. APIs validate the `cnf` claim against the TLS client certificate thumbprint.\n\n### mTLS for Client Authentication\n\nConfigure IdentityServer to accept client certificates:\n\n```csharp\n// Program.cs\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n options.MutualTls.Enabled = true;\n options.MutualTls.DomainName = \"mtls\"; // mTLS endpoints on mtls subdomain\n options.MutualTls.ClientCertificateAuthenticationScheme = \"Certificate\";\n});\n\nidsvrBuilder.AddMutualTlsSecretValidators();\n\nbuilder.Services.AddAuthentication()\n .AddCertificate(\"Certificate\", options =>\n {\n options.AllowedCertificateTypes = CertificateTypes.SelfSigned;\n options.ValidateCertificateUse = true;\n });\n```\n\nConfigure the client with certificate-based secrets:\n\n```csharp\nnew Client\n{\n ClientId = \"mtls.client\",\n AllowedGrantTypes = GrantTypes.ClientCredentials,\n AllowedScopes = { \"api1\" },\n ClientSecrets =\n {\n // PKI-based (by distinguished name)\n new Secret(@\"CN=client, OU=production, O=company\", \"client.dn\")\n {\n Type = SecretTypes.X509CertificateName\n },\n // Self-issued (by thumbprint)\n new Secret(\"bca0d040847f843c5ee0fa6eb494837470155868\", \"mtls.tb\")\n {\n Type = SecretTypes.X509CertificateThumbprint\n }\n }\n}\n```\n\nUse `SecretTypes.X509CertificateName` for PKI/chained certificates (matched by distinguished name) and `SecretTypes.X509CertificateThumbprint` for self-issued certificates (matched by thumbprint).\n\n### mTLS Endpoint URL Strategies\n\n`options.MutualTls.DomainName` controls where the mTLS-protected endpoints live:\n\n| `DomainName` value | Endpoint layout | Example |\n| ------------------ | --------------------------- | ---------------------------------------- |\n| `null` / empty | Path-based on the main host | `https://host/connect/mtls/token` |\n| `\"mtls\"` | Sub-domain | `https://mtls.host/connect/token` |\n| full domain | Separate dedicated domain | `https://mtls.example.com/connect/token` |\n\nThe mTLS endpoint URLs are published in discovery under `mtls_endpoint_aliases`. Clients must read them from there rather than the standard endpoints:\n\n```csharp\nvar tokenEndpoint = disco.MtlsEndpointAliases?.TokenEndpoint;\n```\n\n### mTLS Deployment: Kestrel (dev) vs Reverse Proxy (production)\n\n**Development — Kestrel terminates TLS directly.** Use `mkcert` to create a locally-trusted CA (the private key stays on your machine, unlike shared sample certificates that ship public private keys). Accept — but don't require — client certificates; IdentityServer's mTLS middleware enforces them on the mTLS endpoint:\n\n```csharp\nbuilder.WebHost.ConfigureKestrel(kestrel =>\n{\n kestrel.ConfigureHttpsDefaults(https =>\n {\n // Accept but do not require; the mTLS endpoint enforces the certificate\n https.ClientCertificateMode = ClientCertificateMode.AllowCertificate;\n });\n});\n```\n\n> On .NET 10, Kestrel auto-resolves `*.localhost` sub-domains, so the `mtls.localhost` sub-domain strategy works locally with no hosts-file entry.\n\n**Production — a reverse proxy terminates TLS** and forwards the client certificate to Kestrel via a request header. Kestrel itself does not negotiate the certificate (`ClientCertificateMode.NoCertificate`); use certificate forwarding:\n\n```csharp\n// Kestrel: the proxy terminates TLS, so Kestrel never asks for a certificate\nbuilder.WebHost.ConfigureKestrel(k =>\n k.ConfigureHttpsDefaults(h => h.ClientCertificateMode = ClientCertificateMode.NoCertificate));\n\nbuilder.Services.AddCertificateForwarding(options =>\n{\n options.CertificateHeader = \"X-SSL-CERT\"; // match your proxy\n options.HeaderConverter = headerValue =>\n {\n if (string.IsNullOrWhiteSpace(headerValue)) return null!;\n // e.g. Nginx sends URL-encoded PEM; IIS sends base64 DER — decode accordingly\n var pem = Uri.UnescapeDataString(headerValue);\n return X509Certificate2.CreateFromPem(pem);\n };\n});\n\nbuilder.Services.AddAuthentication()\n .AddCertificate(\"Certificate\", options =>\n {\n // Production PKI certificates are chained, not self-signed\n options.AllowedCertificateTypes = CertificateTypes.Chained;\n });\n\n// ...\napp.UseCertificateForwarding(); // MUST come before UseAuthentication()\napp.UseAuthentication();\napp.UseAuthorization();\n```\n\nProxy header conventions:\n\n| Proxy | Header | Value format |\n| ------ | ------------------ | -------------------------------------------- |\n| IIS | `X-ARR-ClientCert` | base64 DER |\n| Nginx | `X-SSL-CERT` | `$ssl_client_escaped_cert` (URL-encoded PEM) |\n| Apache | `X-SSL-CERT` | `%{SSL_CLIENT_CERT}s` (PEM) |\n\n> **Security:** the proxy must strip or overwrite the certificate header on all inbound requests so a client cannot spoof a certificate by sending the header directly.\n\n### mTLS without Client Authentication\n\nYou can bind tokens to a client certificate without using the certificate for client authentication. This works with any authentication method, including public clients:\n\n```csharp\n// Program.cs\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n options.MutualTls.AlwaysEmitConfirmationClaim = true;\n});\n```\n\nThe client creates a certificate on the fly and uses it to establish the TLS channel:\n\n```csharp\nstatic X509Certificate2 CreateClientCertificate(string name)\n{\n X500DistinguishedName distinguishedName = new X500DistinguishedName($\"CN={name}\");\n\n using (RSA rsa = RSA.Create(2048))\n {\n var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n request.CertificateExtensions.Add(\n new X509KeyUsageExtension(\n X509KeyUsageFlags.DataEncipherment |\n X509KeyUsageFlags.KeyEncipherment |\n X509KeyUsageFlags.DigitalSignature, false));\n request.CertificateExtensions.Add(\n new X509EnhancedKeyUsageExtension(\n new OidCollection { new Oid(\"1.3.6.1.5.5.7.3.2\") }, false));\n\n return request.CreateSelfSigned(\n new DateTimeOffset(DateTime.UtcNow.AddDays(-1)),\n new DateTimeOffset(DateTime.UtcNow.AddDays(10)));\n }\n}\n```\n\n### .NET Client Requesting mTLS Token\n\n```csharp\nstatic async Task<TokenResponse> RequestTokenAsync()\n{\n var handler = new SocketsHttpHandler();\n var cert = new X509Certificate2(\"client.p12\", \"password\");\n handler.SslOptions.ClientCertificates = new X509CertificateCollection { cert };\n\n var client = new HttpClient(handler);\n\n var disco = await client.GetDiscoveryDocumentAsync(Constants.Authority);\n if (disco.IsError) throw new Exception(disco.Error);\n\n var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest\n {\n Address = disco.MtlsEndpointAliases.TokenEndpoint,\n ClientCredentialStyle = ClientCredentialStyle.PostBody,\n ClientId = \"mtls.client\",\n Scope = \"api1\"\n });\n\n if (response.IsError) throw new Exception(response.Error);\n return response;\n}\n```\n\n### Validating mTLS in APIs\n\nAdd custom middleware to compare the `cnf` claim against the TLS client certificate:\n\n```csharp\n// API middleware pipeline\napp.UseAuthentication();\napp.UseConfirmationValidation(); // custom middleware\napp.UseAuthorization();\n```\n\nThe middleware validates the `x5t#S256` value in the `cnf` claim against the SHA-256 thumbprint of the client certificate on the TLS channel.\n\n## DPoP (Demonstrating Proof-of-Possession at the Application Layer)\n\n**Version:** >= 6.3 (Enterprise Edition)\n\nDPoP binds an asymmetric key (stored as a JWK) to an access token via the `cnf` claim:\n\n```json\n{\n \"cnf\": {\n \"jkt\": \"JGSVlE73oKtQQI1dypYg8_JNat0xJjsQNyOI5oxaZf4\"\n }\n}\n```\n\nThe client proves possession of the private key by sending a signed JWT (proof token) via the `DPoP` HTTP header on every request.\n\n### Enabling DPoP in IdentityServer\n\nDPoP can be used dynamically with no server configuration, or enforced per-client:\n\n```csharp\nnew Client\n{\n ClientId = \"dpop_client\",\n RequireDPoP = true,\n\n // Optional: control DPoP proof token expiration validation\n // DPoPValidationMode = DPoPTokenExpirationValidationMode.Iat (default)\n // DPoPClockSkew = TimeSpan.FromMinutes(5) (default)\n}\n```\n\n### Client-Side DPoP Configuration\n\nUse `Duende.AccessTokenManagement` for automatic DPoP proof token handling.\n\n**Client credentials flow:**\n\n```csharp\n// Program.cs\nbuilder.Services.AddClientCredentialsTokenManagement()\n .AddClient(\"demo_dpop_client\", client =>\n {\n client.TokenEndpoint = \"https://identity.example.com/connect/token\";\n client.DPoPJsonWebKey = \"...\"; // JWK string\n });\n```\n\n**Authorization code flow:**\n\n```csharp\n// Program.cs\nbuilder.Services.AddAuthentication(...)\n .AddCookie(\"cookie\", ...)\n .AddOpenIdConnect(\"oidc\", ...);\n\nbuilder.Services.AddOpenIdConnectAccessTokenManagement(options =>\n{\n options.DPoPJsonWebKey = \"...\"; // JWK string\n});\n```\n\n### Creating a DPoP JWK\n\n```csharp\nvar rsaKey = new RsaSecurityKey(RSA.Create(2048));\nvar jsonWebKey = JsonWebKeyConverter.ConvertFromSecurityKey(rsaKey);\njsonWebKey.Alg = \"PS256\";\nstring jwk = JsonSerializer.Serialize(jsonWebKey);\n```\n\nThe `DPoPJsonWebKey` is a critical secret. If lost, tokens bound to it cannot be used. If leaked, the security benefits of DPoP are lost.\n\n### DPoP Client Settings Reference\n\n| Property | Default | Description |\n| -------------------- | --------------------------------------- | ----------------------------------------------- |\n| `RequireDPoP` | `false` | Require DPoP for this client |\n| `DPoPValidationMode` | `DPoPTokenExpirationValidationMode.Iat` | Validate via client `iat` and/or server `nonce` |\n| `DPoPClockSkew` | 5 minutes | Clock skew for `iat` claim validation |\n\n### Validating DPoP in APIs\n\nInstall the DPoP validation package:\n\n```bash\ndotnet add package Duende.AspNetCore.Authentication.JwtBearer\n```\n\nConfigure JWT bearer with DPoP:\n\n```csharp\n// API Program.cs\nbuilder.Services.AddAuthentication(\"token\")\n .AddJwtBearer(\"token\", options =>\n {\n options.Authority = Constants.Authority;\n options.TokenValidationParameters.ValidateAudience = false;\n options.MapInboundClaims = false;\n options.TokenValidationParameters.ValidTypes = new[] { \"at+jwt\" };\n });\n\n// Extend with DPoP processing and validation\nbuilder.Services.ConfigureDPoPTokensForScheme(\"token\");\n```\n\nDPoP validation requires a distributed cache for replay detection:\n\n```csharp\n// Use any IDistributedCache implementation (Redis, CosmosDB, SQL Server, etc.)\nbuilder.Services.AddDistributedMemoryCache(); // in-memory for development only\n```\n\n### DPoP Validation Steps (handled by the library)\n\n1. Validate the access token as normal\n2. Validate the DPoP proof token from the `DPoP` HTTP request header\n3. Ensure the authorization header uses the `DPoP` scheme\n4. Validate the JWT format of the proof token\n5. Verify the `cnf` claim matches between tokens\n6. Validate the HTTP method and URL match the request\n7. Detect replay attacks using distributed cache storage\n8. Manage nonce generation and validation\n9. Handle clock skew between systems\n10. Return appropriate error response headers when validation fails\n\n## Pushed Authorization Requests (PAR)\n\n**Version:** >= 7.0 (Business and Enterprise Edition)\n\nPAR moves authorization parameters from the front channel (browser redirect URLs) to the back channel (direct HTTP POST), preventing parameter leakage and tampering.\n\n### Why PAR\n\n- Prevents exposure of authorization parameters (PII in scopes, claims)\n- Prevents tampering with parameters (attacker changing scope)\n- Keeps request URLs short (avoids browser/infrastructure URL length limits)\n- Required by FAPI 2.0 Security Profile\n\n### Server Configuration\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer(options =>\n{\n // Require PAR globally\n options.PushedAuthorization.Required = false; // default\n\n // Lifetime of pushed authorization requests in seconds (default: 600 = 10 minutes)\n options.PushedAuthorization.Lifetime = 600; // seconds (int, not TimeSpan)\n\n // Allow redirect URIs not pre-registered (default: false)\n options.PushedAuthorization.AllowUnregisteredPushedRedirectUris = false;\n});\n```\n\n### Per-Client Configuration\n\n```csharp\nnew Client\n{\n ClientId = \"par_client\",\n RequirePushedAuthorization = true, // require PAR for this client\n PushedAuthorizationLifetime = 600 // 10 minutes, overrides global\n}\n```\n\n### Client Usage (.NET 9+)\n\n```csharp\n// Program.cs\nbuilder.Services\n .AddAuthentication(options =>\n {\n options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;\n options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;\n })\n .AddCookie()\n .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, oidcOptions =>\n {\n // PushedAuthorizationBehavior.UseIfAvailable is the default in .NET 9+\n // To require PAR:\n oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Require;\n });\n```\n\n### Disabling PAR (Starter Edition)\n\nPAR requests are not processed in the Starter edition. Disable the endpoint to reflect this in discovery:\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer(options =>\n{\n options.Endpoints.EnablePushedAuthorizationEndpoint = false;\n});\n```\n\n### PAR Configuration Reference\n\n| Property | Default | Description |\n| --------------------------------------------------------- | ---------- | -------------------------------- |\n| `PushedAuthorization.Required` | `false` | Require PAR globally |\n| `PushedAuthorization.Lifetime` | `600` (seconds, `int`) | PAR request lifetime |\n| `PushedAuthorization.AllowUnregisteredPushedRedirectUris` | `false` | Allow unregistered redirect URIs |\n| `Client.RequirePushedAuthorization` | `false` | Per-client PAR requirement |\n| `Client.PushedAuthorizationLifetime` | `null` | Per-client lifetime override |\n| `Endpoints.EnablePushedAuthorizationEndpoint` | `true` | Enable/disable PAR endpoint |\n\n## JWT Secured Authorization Requests (JAR)\n\nJAR packages authorization request parameters in a signed JWT, making them tamperproof and enabling front-channel client authentication.\n\n### Server Configuration\n\nConfigure the client to require signed request objects:\n\n```csharp\nvar client = new Client\n{\n ClientId = \"foo\",\n RequireRequestObject = true,\n\n ClientSecrets =\n {\n new Secret\n {\n // X509 cert base64-encoded\n Type = IdentityServerConstants.SecretTypes.X509CertificateBase64,\n Value = Convert.ToBase64String(cert.Export(X509ContentType.Cert))\n },\n new Secret\n {\n // RSA key as JWK\n Type = IdentityServerConstants.SecretTypes.JsonWebKey,\n Value = \"{'e':'AQAB','kid':'...','kty':'RSA','n':'...'}\"\n }\n }\n};\n```\n\nThe same key can be shared between client authentication (private_key_jwt) and signed authorize requests.\n\n### Request JWTs by Reference\n\nIf using `request_uri`, IdentityServer fetches the JWT from the specified URL:\n\n```csharp\n// Program.cs\nidsvrBuilder.AddJwtRequestUriHttpClient(client =>\n{\n client.Timeout = TimeSpan.FromSeconds(30);\n})\n .AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(new[]\n {\n TimeSpan.FromSeconds(1),\n TimeSpan.FromSeconds(2),\n TimeSpan.FromSeconds(3)\n }));\n```\n\nRequest URI processing is disabled by default. Enable it on the `Endpoints` options.\n\n### Accessing Request Object Data\n\n- In `ValidatedAuthorizeRequest`: use the `RequestObjectValues` dictionary\n- In UI code: call `IIdentityServerInteractionService.GetAuthorizationContextAsync`, then access `RequestObjectValues` on the returned `AuthorizationRequest`\n\n## FAPI 2.0 Compliance\n\n**Version:** >= 7.3 (Enterprise Edition)\n\nThe FAPI 2.0 Security Profile is a set of OAuth security best practices for high-value scenarios (open banking, e-health, e-government).\n\n### FAPI 2.0 Authorization Server Requirements Checklist\n\n| Requirement | IdentityServer Status | Configuration Needed |\n| ------------------------------------------------ | -------------------------------------- | --------------------------------------------------- |\n| Distribute discovery metadata | Default behavior | None |\n| Reject resource owner password grant | Default behavior (when not configured) | None |\n| Only support confidential clients | Configure per-client | Set `RequireClientSecret = true` |\n| Only issue sender-constrained tokens | Configure | Enable DPoP or mTLS |\n| Authenticate via mTLS or `private_key_jwt` | Configure | Set client secrets accordingly |\n| No open redirectors | Default behavior | None |\n| Accept only issuer as `aud` in client assertions | Enable strict validation | `StrictClientAssertionAudienceValidation` |\n| No refresh token rotation (except extraordinary) | Configure | Set `RefreshTokenUsage = ReUse` |\n| DPoP server-provided nonce | Configure | Set `DPoPValidationMode` |\n| Authorization code max 60 seconds | Configure | Set `AuthorizationCodeLifetime = 60` |\n| JWT clock skew max 10 seconds future | Configure | `JwtValidationClockSkew = TimeSpan.FromSeconds(10)` |\n| PAR required | Configure per-client or globally | Set `RequirePushedAuthorization = true` on client or `PushedAuthorization.Required = true` globally |\n| PKCE required | Configure per-client | Set `RequirePkce = true` on client (strongly recommended) |\n\n### FAPI 2.0 Server Setup\n\n```csharp\nbuilder.Services.AddIdentityServer(opt =>\n{\n // Key management with PS256 support\n opt.KeyManagement.SigningAlgorithms.Add(\n new SigningAlgorithmOptions(SecurityAlgorithms.RsaSsaPssSha256));\n\n // DPoP signing algorithms\n opt.DPoP.SupportedDPoPSigningAlgorithms = [\n SecurityAlgorithms.RsaSsaPssSha256,\n SecurityAlgorithms.RsaSsaPssSha384,\n SecurityAlgorithms.RsaSsaPssSha512,\n SecurityAlgorithms.EcdsaSha256,\n SecurityAlgorithms.EcdsaSha384,\n SecurityAlgorithms.EcdsaSha512\n ];\n\n // Client assertion signing algorithms\n opt.SupportedClientAssertionSigningAlgorithms = [\n SecurityAlgorithms.RsaSsaPssSha256,\n SecurityAlgorithms.RsaSsaPssSha384,\n SecurityAlgorithms.RsaSsaPssSha512,\n SecurityAlgorithms.EcdsaSha256,\n SecurityAlgorithms.EcdsaSha384,\n SecurityAlgorithms.EcdsaSha512\n ];\n\n // Request object signing algorithms\n opt.SupportedRequestObjectSigningAlgorithms = [\n SecurityAlgorithms.RsaSsaPssSha256,\n SecurityAlgorithms.RsaSsaPssSha384,\n SecurityAlgorithms.RsaSsaPssSha512,\n SecurityAlgorithms.EcdsaSha256,\n SecurityAlgorithms.EcdsaSha384,\n SecurityAlgorithms.EcdsaSha512\n ];\n\n // FAPI 2.0 clock skew requirement\n opt.JwtValidationClockSkew = TimeSpan.FromSeconds(10);\n});\n```\n\n### FAPI 2.0 Client Configuration\n\n```csharp\nnew Client\n{\n ClientId = \"fapi_client\",\n ClientSecrets = [\n new Secret\n {\n Type = IdentityServerConstants.SecretTypes.JsonWebKey,\n Value = \"<JWT Key goes here>\"\n }\n ],\n AllowedGrantTypes = GrantTypes.Code,\n RedirectUris = [\n \"https://example.com/callback\",\n ],\n AllowOfflineAccess = true,\n AllowedScopes = [ \"openid\", \"profile\", \"api\" ],\n RequireDPoP = true, // sender-constrained tokens\n RequirePushedAuthorization = true // PAR required\n}\n```\n\n### FAPI 2.0 API Configuration\n\n```csharp\nbuilder.Services.AddAuthentication()\n .AddJwtBearer(options =>\n {\n options.Authority = configuration.Authority;\n options.TokenValidationParameters.ValidateAudience = false;\n options.MapInboundClaims = false;\n options.TokenValidationParameters.ValidTypes = [\"at+jwt\"];\n });\n\nbuilder.Services.ConfigureDPoPTokensForScheme(JwtBearerDefaults.AuthenticationScheme,\n dpopOptions =>\n {\n dpopOptions.ProofTokenValidationParameters.ValidAlgorithms =\n [\n SecurityAlgorithms.RsaSsaPssSha256,\n SecurityAlgorithms.RsaSsaPssSha384,\n SecurityAlgorithms.RsaSsaPssSha512,\n SecurityAlgorithms.EcdsaSha256,\n SecurityAlgorithms.EcdsaSha384,\n SecurityAlgorithms.EcdsaSha512\n ];\n });\n```\n\n### FAPI 2.0 HTTP Redirects\n\nStarting in v8.0, IdentityServer unconditionally uses HTTP 303 (See Other) redirects from POST endpoints, in compliance with FAPI 2.0 Section 5.3.2.2.\n\n### Private Key JWT vs mTLS for FAPI 2.0\n\nStart with private key JWTs. mTLS is relatively challenging to maintain in production. Both are supported and FAPI 2.0 compliant.\n\n## Edition Requirements Summary\n\n| Feature | Starter | Business | Enterprise |\n| ----------------------------- | ------- | -------- | ----------- |\n| Static key management | Yes | Yes | Yes |\n| Automatic key management | No | Yes | Yes |\n| PAR | No | Yes | Yes |\n| DPoP | No | No | Yes |\n| Resource isolation (RFC 8707) | No | No | Yes |\n| FAPI 2.0 conformance report | No | No | Yes (v8.0+) |\n| mTLS client authentication | Yes | Yes | Yes |\n| mTLS token binding | Yes | Yes | Yes |\n| JAR (signed requests) | Yes | Yes | Yes |\n\n## Common Pitfalls\n\n1. **Using `ClientCredentialStyle.AuthorizationHeader` with mTLS** - The default `AuthorizationHeader` style does not work in mTLS scenarios. Use `ClientCredentialStyle.PostBody` instead.\n\n2. **Missing distributed cache for DPoP** - DPoP replay detection requires `IDistributedCache`. Without it, replay attacks are possible. Use Redis, SQL Server, or another durable cache in production.\n\n3. **PAR lifetime too short** - The default 10 minutes balances security (FAPI 2.0 recommendation) with usability. If users take longer to authenticate (MFA, consent), increase the lifetime.\n\n4. **DPoP key management** - The `DPoPJsonWebKey` must persist for the lifetime of tokens bound to it. Losing the key makes bound tokens unusable. Leaking it nullifies DPoP's security benefits.\n\n5. **Confusing DPoP with client authentication** - DPoP proves token possession at the application layer. It is separate from client authentication (which proves client identity at the token endpoint). A client can use shared secrets for authentication and DPoP for token binding.\n\n6. **Not enabling `AlwaysEmitConfirmationClaim` for mTLS without mTLS auth** - If you want certificate binding without certificate-based client authentication, you must set `MutualTls.AlwaysEmitConfirmationClaim = true`.\n\n7. **Forgetting to configure DPoP proof validation algorithms** - For FAPI 2.0, explicitly set `ProofTokenValidationParameters.ValidAlgorithms` on the API side. Without this, the API may accept weaker algorithms.\n\n8. **PAR not available in Starter edition** - PAR requests are rejected in the Starter edition. Disable the endpoint via `options.Endpoints.EnablePushedAuthorizationEndpoint = false` so discovery accurately reflects this.\n"
}SHA-256 of public snapshot: 6e5dd16144dda31ffd5d061f7eeed2b0c97a69dced7ecf572be7907bebae818b