IdentityModel: Documentation for Duende's open-source IdentityModel library which provides an object model to interact with the endpoints defined in the various OAuth and OpenId Connect specifications ----- # Duende IdentityModel > Duende.IdentityModel for OpenID Connect and OAuth 2.0 related protocol operations, providing object models and utilities for identity-related operations The `Duende.IdentityModel` package is the base library for OpenID Connect and OAuth 2.0 related protocol operations. It provides an object model to interact with the endpoints defined in the various OAuth and OpenId Connect specifications. The types included represent the requests and responses, and constants defined in the specifications, such as standard scope, claim, and parameter names. The library also contains extension methods to invoke requests and other convenience methods for performing common identity related operations. [GitHub Repository ](https://github.com/DuendeSoftware/foss/tree/main/identity-model)View the source code for this library on GitHub. [NuGet Package ](https://www.nuget.org/packages/Duende.IdentityModel/)View the package on NuGet.org. ----- # Client Assertions > How to use client assertions (private_key_jwt / client_secret_jwt) for client authentication in protocol requests. Client assertions are an alternative to client secrets for authenticating confidential clients at token endpoints. Instead of sending a shared secret, the client creates a signed JWT (or SAML assertion) and includes it in the request. This is defined in [RFC 7523 — JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication](https://datatracker.ietf.org/doc/html/rfc7523) and is commonly known as the `private_key_jwt` or `client_secret_jwt` authentication methods defined in [OpenID Connect Core §9](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication). All protocol request types that derive from `ProtocolRequest` expose two properties for setting client assertions: `ClientAssertion` and `ClientAssertionFactory`. ## ClientAssertion [Section titled “ClientAssertion”](#clientassertion) The `ClientAssertion` property lets you attach a pre-built assertion to any protocol request. Set its `Type` and `Value` and they will be included as the `client_assertion_type` and `client_assertion` parameters: ```csharp var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientAssertion = { Type = OidcConstants.ClientAssertionTypes.JwtBearer, Value = mySignedJwt }, ClientCredentialStyle = ClientCredentialStyle.PostBody }); ``` ## ClientAssertionFactory [Section titled “ClientAssertionFactory”](#clientassertionfactory) *Added in `Duende.IdentityModel` 7.2.0* The `ClientAssertionFactory` property accepts a `Func>` — a factory function that creates a **fresh** `ClientAssertion` on demand. This was introduced to support scenarios where a protocol request may need to be **retried**, and each attempt requires a new assertion with unique `jti` and `iat` claims. The primary motivating scenario is **DPoP** (Demonstrating Proof of Possession). When a DPoP token request receives a `use_dpop_nonce` error, the HTTP handler retries the request with an updated DPoP proof. If the client assertion were static, the server could reject the retry because it has already seen that assertion’s `jti`. The factory solves this by generating a new assertion for each attempt. ```csharp var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientAssertionFactory = () => Task.FromResult(new ClientAssertion { Type = OidcConstants.ClientAssertionTypes.JwtBearer, Value = CreateSignedJwt() // generates a fresh JWT each time }), ClientCredentialStyle = ClientCredentialStyle.PostBody }); ``` When `ClientAssertionFactory` is set, the factory is stored on the `HttpRequestMessage.Options` so that DPoP retry handlers (and other delegating handlers in the pipeline) can invoke it to obtain a new assertion on each attempt. ### Usage with Duende.IdentityModel.OidcClient [Section titled “Usage with Duende.IdentityModel.OidcClient”](#usage-with-duendeidentitymodeloidcclient) Both the `ClientAssertion` and `ClientAssertionFactory` properties exist on `ProtocolRequest` to support [`Duende.IdentityModel.OidcClient`](/identitymodel-oidcclient/). The OidcClient library builds on IdentityModel’s protocol requests internally, and when configured with client assertion-based authentication, it sets these properties on the underlying requests it creates. When `ClientAssertionFactory` is set, it is used during both: * **Pushed Authorization Requests (PAR)** — the factory is invoked to produce a fresh assertion for the PAR endpoint request. * **Token requests** — the factory is invoked again to produce a fresh assertion for the token endpoint request. This ensures each request carries its own unique assertion, which is essential when the authorization server enforces `jti` uniqueness across requests. ----- # Device Authorization Endpoint > Documentation for OAuth 2.0 device flow authorization endpoint using HttpClient extension methods The client library for the [OAuth 2.0 device flow](https://tools.ietf.org/html/rfc7662) device authorization is provided as an extension method for `HttpClient`. The following code sends a device authorization request: ```csharp var client = new HttpClient(); var response = await client.RequestDeviceAuthorizationAsync(new DeviceAuthorizationRequest { Address = "https://demo.duendesoftware.com/connect/device_authorize", ClientId = "device" }); ``` The response is of type `DeviceAuthorizationResponse` and has properties for the standard response parameters. You also have access to the raw response and to a parsed JSON document (via the `Raw` and `Json` properties). Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (response.IsError) throw new Exception(response.Error); var userCode = response.UserCode; var deviceCode = response.DeviceCode; var verificationUrl = response.VerificationUri; var verificationUrlComplete = response.VerificationUriComplete; ``` ----- # Discovery Endpoint > Documentation for using the OpenID Connect discovery endpoint client library, including configuration, validation, and caching features The client library for the [OpenID Connect discovery endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html) is provided as an extension method for `HttpClient`. The `GetDiscoveryDocumentAsync` method returns a `DiscoveryDocumentResponse` object that has both strong and weak typed accessors for the various elements of the discovery document. You should always check the `IsError` and `Error` properties before accessing the contents of the document: ```csharp var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync("https://demo.duendesoftware.com"); if (disco.IsError) throw new Exception(disco.Error); ``` [Standard elements](#discoverydocumentresponse-properties-reference) can be accessed by using properties: ```csharp var tokenEndpoint = disco.TokenEndpoint; var keys = disco.KeySet.Keys; ``` Custom elements (or elements not covered by the standard properties) can be accessed like this: ```csharp // returns string or null var stringValue = disco.TryGetString("some_string_element"); // return a nullable boolean var boolValue = disco.TryGetBoolean("some_boolean_element"); // return array (maybe empty) var arrayValue = disco.TryGetStringArray("some_array_element"); // returns JToken var rawJson = disco.TryGetValue("some_element"); ``` ### Discovery Policy [Section titled “Discovery Policy”](#discovery-policy) By default, the discovery response is validated before it is returned to the client, validation includes: * enforce that HTTPS is used (except for localhost addresses) * enforce that the issuer matches the authority * enforce that the protocol endpoints are on the same DNS name as the `authority` * enforce the existence of a keyset Policy violation errors will set the `ErrorType` property on the `DiscoveryDocumentResponse` to `PolicyViolation`. All the standard validation rules can be modified using the `DiscoveryPolicy` class, e.g. disabling the issuer name check: ```csharp var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest { Address = "https://demo.duendesoftware.com", Policy = { ValidateIssuerName = false } }); ``` #### Cross-Host Endpoints [Section titled “Cross-Host Endpoints”](#cross-host-endpoints) When the URIs in the discovery document are on a different base address than the issuer URI (for example, a [Dynamic Client Registration endpoint](/identityserver/configuration/dcr/#adding-the-registration-endpoint-to-the-discovery-document) hosted on a separate service), the discovery policy will reject those endpoints by default with: ```text Endpoint is on a different host than authority ``` You can resolve this by adding the additional host to `AdditionalEndpointBaseAddresses` (recommended), or by setting `ValidateEndpoints = false` to disable endpoint validation entirely. The same applies to any component that has its own `DiscoveryPolicy`, such as `OAuth2IntrospectionOptions.DiscoveryPolicy`. Each instance needs to be configured independently. ```csharp // Using AdditionalEndpointBaseAddresses (recommended) var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest { Address = "https://authority.example.com", Policy = { AdditionalEndpointBaseAddresses = [ "https://config-api.example.com" ] } }); // Or when using DiscoveryCache var cache = new DiscoveryCache( "https://authority.example.com", () => factory.CreateClient(), new DiscoveryPolicy { AdditionalEndpointBaseAddresses = [ "https://config-api.example.com" ] }); ``` You can also customize validation strategy based on the authority with your own implementation of `IAuthorityValidationStrategy`. By default, comparison uses ordinal string comparison. To switch to `Uri` comparison: ```csharp var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest { Address = "https://demo.duendesoftware.com", Policy = { AuthorityValidationStrategy = new AuthorityUrlValidationStrategy() } }); ``` ### Caching The Discovery Document [Section titled “Caching The Discovery Document”](#caching-the-discovery-document) You should periodically update your local copy of the discovery document, to be able to react to configuration changes on the server. This is especially important for playing nice with automatic key rotation. The `DiscoveryCache` class can help you with that. The following code will set up the cache, retrieve the document the first time it is needed, and then cache it for 24 hours: ```csharp var cache = new DiscoveryCache("https://demo.duendesoftware.com"); ``` You can then access the document like this: ```csharp var disco = await cache.GetAsync(); if (disco.IsError) throw new Exception(disco.Error); ``` You can specify the cache duration using the `CacheDuration` property and also specify a custom discovery policy by passing in a `DiscoveryPolicy` to the constructor. ### Caching And HttpClient Instances [Section titled “Caching And HttpClient Instances”](#caching-and-httpclient-instances) By default, the discovery cache will create a new instance of `HttpClient` every time it needs to access the discovery endpoint. You can modify this behavior in two ways, either by passing in a pre-created instance into the constructor, or by providing a function that will return an `HttpClient` when needed. The following code will set up the discovery cache in the ASP.NET Core service provider and will use the `HttpClientFactory` to create clients: ```csharp services.AddSingleton(r => { var factory = r.GetRequiredService(); return new DiscoveryCache(Constants.Authority, () => factory.CreateClient()); }); ``` ### DiscoveryDocumentResponse Properties Reference [Section titled “DiscoveryDocumentResponse Properties Reference”](#discoverydocumentresponse-properties-reference) The following table lists the standard properties on the `DiscoveryDocumentResponse` class: | Property | Description | | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | Policy | Gets or sets the discovery policy used to configure how the discovery document is processed | | KeySet | Gets or sets the JSON Web Key Set (JWKS) associated with the discovery document | | MtlsEndpointAliases | Gets the mutual TLS (mTLS) endpoint aliases | | Issuer | Gets the issuer identifier for the authorization server | | AuthorizeEndpoint | Gets the authorization endpoint URL | | TokenEndpoint | Gets token endpoint URL | | UserInfoEndpoint | Gets user info endpoint URL | | IntrospectionEndpoint | Gets the introspection endpoint URL | | RevocationEndpoint | Gets the revocation endpoint URL | | DeviceAuthorizationEndpoint | Gets the device authorization endpoint URL | | BackchannelAuthenticationEndpoint | Gets the backchannel authentication endpoint URL | | JwksUri | Gets the URI of the JSON Web Key Set (JWKS) | | EndSessionEndpoint | Gets the end session endpoint URL | | CheckSessionIframe | Gets the check session iframe URL | | RegistrationEndpoint | Gets the dynamic client registration (DCR) endpoint URL | | PushedAuthorizationRequestEndpoint | Gets the pushed authorization request (PAR) endpoint URL | | FrontChannelLogoutSupported | Gets a flag indicating whether front-channel logout is supported | | FrontChannelLogoutSessionSupported | Gets a flag indicating whether a session ID (sid) parameter is supported at the front-channel logout endpoint | | GrantTypesSupported | Gets the supported grant types | | CodeChallengeMethodsSupported | Gets the supported code challenge methods | | ScopesSupported | Gets the supported scopes | | SubjectTypesSupported | Gets the supported subject types | | ResponseModesSupported | Gets the supported response modes | | ResponseTypesSupported | Gets the supported response types | | ClaimsSupported | Gets the supported claims | | TokenEndpointAuthenticationMethodsSupported | Gets the authentication methods supported by the token endpoint | | TokenEndpointAuthenticationSigningAlgorithmsSupported | Gets the signing algorithms supported by the token endpoint for client authentication | | BackchannelTokenDeliveryModesSupported | Gets the supported backchannel token delivery modes | | BackchannelUserCodeParameterSupported | Gets a flag indicating whether the backchannel user code parameter is supported | | RequirePushedAuthorizationRequests | Gets a flag indicating whether the use of pushed authorization requests (PAR) is required | | IntrospectionSigningAlgorithmsSupported | Gets the signing algorithms supported for introspection responses | | IntrospectionEncryptionAlgorithmsSupported | Gets the encryption “alg” values supported for encrypted JWT introspection responses | | IntrospectionEncryptionEncValuesSupported | Gets the encryption “enc” values supported for encrypted JWT introspection responses | | Scopes | The list of scopes associated to the token or an empty array if no `scope` claim is present | | ClientId | The client identifier for the OAuth 2.0 client that requested the token or `null` if the `client_id` claim is missing | | UserName | The human-readable identifier for the resource owner who authorized the token or `null` if the `username` claim is missing | | TokenType | The type of the token as defined in section 5.1 of OAuth 2.0 (RFC6749) or `null` if the `token_type` claim is missing | | Expiration | The expiration time of the token or `null` if the `exp` claim is missing | | IssuedAt | The issuance time of the token or `null` if the `iat` claim is missing | | NotBefore | The validity start time of the token or `null` if the `nbf` claim is missing | | Subject | The subject of the token or `null` if the `sub` claim is missing | | Audiences | The service-specific list of string identifiers representing the intended audience for the token or an empty array if no `aud` claim is present | | Issuer | The string representing the issuer of the token or `null` if the `iss` claim is missing | | JwtId | The string identifier for the token or `null` if the `jti` claim is missing | ----- # Dynamic Client Registration > Documentation for OpenID Connect Dynamic Client Registration library extension method for HttpClient that enables client registration and response handling The client library for [OpenID Connect Dynamic Client Registration](https://openid.net/specs/openid-connect-registration-1_0.html) is provided as an extension method for [`System.Net.Http.HttpClient`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient). The following code sends a registration request: ```csharp var client = new HttpClient(); var response = await client.RegisterClientAsync(new DynamicClientRegistrationRequest { Address = Endpoint, Document = new DynamicClientRegistrationDocument { RedirectUris = { redirectUri }, ApplicationType = "native" } }); ``` The response is of type `RegistrationResponse` and has properties for the standard response parameters. You also have access to the raw response and to a parsed JSON document (via the `Raw` and `Json` properties). Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (response.IsError) throw new Exception(response.Error); var clientId = response.ClientId; var secret = response.ClientSecret; ``` ----- # General Usage > Overview of IdentityModel client libraries common design patterns and usage for OpenID Connect and OAuth 2.0 endpoint interactions. IdentityModel contains client libraries for many interactions with endpoints defined in OpenID Connect and OAuth 2.0. All of these libraries have a common design, let’s examine the various layers using the client for the token endpoint. ## Request and response objects [Section titled “Request and response objects”](#request-and-response-objects) All protocol request are modeled as request objects and have a common base class called `ProtocolRequest` which has properties to set the endpoint address, client ID, client secret, client assertion, and the details of how client secrets are transmitted (e.g. authorization header vs POST body). `ProtocolRequest` derives from `HttpRequestMessage` and thus also allows setting custom headers etc. The following code snippet creates a request for a client credentials grant type: ```csharp var request = new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret" }; ``` While in theory you could now call `Prepare` (which internally sets the headers, body and address) and send the request via a plain `HttpClient`, typically there are more parameters with special semantics and encoding required. That’s why we provide extension methods to do the low level work. Equally, a protocol response has a corresponding `ProtocolResponse` implementation that parses the status codes and response content. The following code snippet would parse the raw HTTP response from a token endpoint and turn it into a `TokenResponse` object: ```csharp var tokenResponse = await ProtocolResponse .FromHttpResponseAsync(httpResponse); ``` Again these steps are automated using the extension methods. So let’s have a look at an example next. ## Extension methods [Section titled “Extension methods”](#extension-methods) For each protocol interaction, an extension method for `HttpMessageInvoker` (that’s the base class of `HttpClient`) exists. The extension methods expect a request object and return a response object. It is your responsibility to set up and manage the lifetime of the `HttpClient`, e.g. manually: ```csharp var client = new HttpClient(); var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret" }); ``` You might want to use other techniques to obtain an `HttpClient`, e.g. via the HTTP client factory: ```csharp var client = HttpClientFactory.CreateClient("my_named_token_client"); var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret" }); ``` All other endpoint client follow the same design. ## Client Credential Style [Section titled “Client Credential Style”](#client-credential-style) Any request type implementing `ProtocolRequest` has the ability to configure the client credential style, which specifies how the client will transmit the client ID and secret. `ClientCredentialStyle` options include `PostBody` and the default value of `AuthorizationHeader`. ```csharp var client = HttpClientFactory.CreateClient("my_named_token_client"); var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret", // set the client credential style ClientCredentialStyle = ClientCredentialStyle.AuthorizationHeader }); ``` For interoperability between OAuth implementations, we allow you to choose either approach, depending on which specification version you are targeting. When using IdentityServer, both header and body approaches are supported and *“it just works”*. [RFC 6749](https://datatracker.ietf.org/doc/rfc6749/), the original OAuth spec, says that support for the basic auth header is mandatory, and that the POST body is optional. OAuth 2.1 reverses this: now the body is mandatory and the header is optional. In the previous OAuth specification version, the header caused bugs and interoperability problems. To follow both RFC 6749 and RFC 2617 (which is where basic auth headers are specified), you have to form url encode the client id and client secret, concatenate them both with a colon in between, and then base64 encode the final value. To try to avoid that complex process, OAuth 2.1 now prefers the POST body mechanism. References: * [RFC 6749](https://datatracker.ietf.org/doc/rfc6749/) section 2.3.1 * [RFC 2617 section 2](https://www.rfc-editor.org/rfc/rfc2617#section-2) * [OAuth 2.1 Draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/) Here is a complete list of `ProtocolRequest` implementors that expose the `ClientCredentialStyle` option: * `Duende.IdentityModel.Client.AuthorizationCodeTokenRequest` * `Duende.IdentityModel.Client.BackchannelAuthenticationRequest` * `Duende.IdentityModel.Client.BackchannelAuthenticationTokenRequest` * `Duende.IdentityModel.Client.ClientCredentialsTokenRequest` * `Duende.IdentityModel.Client.DeviceAuthorizationRequest` * `Duende.IdentityModel.Client.DeviceTokenRequest` * `Duende.IdentityModel.Client.DiscoveryDocumentRequest` * `Duende.IdentityModel.Client.DynamicClientRegistrationRequest` * `Duende.IdentityModel.Client.JsonWebKeySetRequest` * `Duende.IdentityModel.Client.PasswordTokenRequest` * `Duende.IdentityModel.Client.PushedAuthorizationRequest` * `Duende.IdentityModel.Client.RefreshTokenRequest` * `Duende.IdentityModel.Client.TokenExchangeTokenRequest` * `Duende.IdentityModel.Client.TokenIntrospectionRequest` * `Duende.IdentityModel.Client.TokenRequest` * `Duende.IdentityModel.Client.TokenRevocationRequest` * `Duende.IdentityModel.Client.UserInfoRequest` ----- # Token Introspection Endpoint > Learn how to use the OAuth 2.0 token introspection endpoint to validate and inspect access tokens using HttpClient extensions. The client library for [OAuth 2.0 token introspection (RFC 7662)](https://tools.ietf.org/html/rfc7662) is provided by the `IntrospectionClient` class, and as an extension method for `HttpClient`. ## Token Introspection Request [Section titled “Token Introspection Request”](#token-introspection-request) The following code sends a reference token to an introspection endpoint: * Using IntrospectionClient ```csharp var clientOptions = new IntrospectionClientOptions { Address = Endpoint, ClientId = "client", ClientSecret = "secret", ResponseFormat = ResponseFormat.Json }; var httpClient = new HttpClient(); var introspectionClient = new IntrospectionClient(httpClient, clientOptions); var introspectionResponse = await introspectionClient.Introspect("token"); ``` * Using HttpClient extension ```csharp var client = new HttpClient(); var introspectionResponse = await client.IntrospectTokenAsync(new TokenIntrospectionRequest { Address = Endpoint, Token = "token", ResponseFormat = ResponseFormat.Json }); ``` ## Token Introspection Response [Section titled “Token Introspection Response”](#token-introspection-response) The response of a token introspection request is an object of type `TokenIntrospectionResponse`. Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (introspectionResponse.IsError) throw new Exception(introspectionResponse.Error); var isActive = introspectionResponse.IsActive; var claims = introspectionResponse.Claims; ``` The `TokenIntrospectionResponse` class exposes the raw response through its `Raw` property, and to the parsed JSON document through its `Json` property. In addition, it provides access to the following standard response parameters: | Property | Value | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Scopes` | The list of scopes associated to the token or an empty array if no `scope` claim is present. | | `ClientId` | The client identifier for the OAuth 2.0 client that requested the token or `null` if the `client_id` claim is missing. | | `UserName` | The human-readable identifier for the resource owner who authorized the token or `null` if the `username` claim is missing. | | `TokenType` | The type of the token as defined in [section 5.1 of OAuth 2.0 (RFC6749)](https://datatracker.ietf.org/doc/html/rfc6749#section-5.1) or `null` if the `token_type` claim is missing. | | `Expiration` | The expiration time of the token or `null` if the `exp` claim is missing. | | `IssuedAt` | The issuance time of the token or `null` if the `iat` claim is missing. | | `NotBefore` | The validity start time of the token or `null` if the `nbf` claim is missing. | | `Subject` | The subject of the token or `null` if the `sub` claim is missing. | | `Audiences` | The service-specific list of string identifiers representing the intended audience for the token or an empty array if no `aud` claim is present. | | `Issuer` | The string representing the issuer of the token or `null` if the `iss` claim is missing. | | `JwtId` | The string identifier for the token or `null` if the `jti` claim is missing. | ## JWT Response Format v7.1 [Section titled “JWT Response Format ”v7.1](#jwt-response-format) Introspection requests can optionally pass a parameter to indicate that a signed JWT rather than JSON payload is desired. Such a JWT response is most often useful for non-repudiation. For example, an API might rely on the claims from introspection to produce digitally signed documents or issue certificates, with the Authorization Server assuming legal liability for the introspected data. A JWT introspection response can be stored and its signature independently verified as part of an audit. ### Requesting JWT Response Format [Section titled “Requesting JWT Response Format”](#requesting-jwt-response-format) To request the JWT response format, set the `ResponseFormat` option to `ResponseFormat.Jwt`. ```csharp var client = new HttpClient(); var introspectionResponse = await client.IntrospectTokenAsync( new TokenIntrospectionRequest { Address = Endpoint, Token = "token", ResponseFormat = ResponseFormat.Jwt }); ``` ### Validating JWT Signature [Section titled “Validating JWT Signature”](#validating-jwt-signature) By default, when the introspection endpoint returns a JWT, the system performs only a basic format check on the response. Full cryptographic validation of the JWT’s signature and claims is not performed. This approach is generally appropriate because the introspection request is made over a direct back-channel connection from the application to the introspection endpoint. This connection is secured by TLS, which guarantees the authenticity and integrity of the response in transit. The introspected claims can safely be used immediately without an additional cryptographic validation. An extensibility point is available to provide your own implementation of `ITokenIntrospectionJwtResponseValidator`. ITokenIntrospectionJwtResponseValidator.cs ```csharp public interface ITokenIntrospectionJwtResponseValidator { void Validate(string rawJwtResponse); } ``` A custom validator can be applied using the `TokenIntrospectionRequest.JwtResponseValidator` property or using `IntrospectionClientOptions`: ```csharp var client = new HttpClient(); var introspectionResponse = await client.IntrospectTokenAsync( new TokenIntrospectionRequest { Address = Endpoint, Token = "token", ResponseFormat = ResponseFormat.Jwt, JwtResponseValidator = new CustomIntrospectionJwtResponseValidator() }); ``` ----- # Token Revocation Endpoint > Client library implementation for OAuth 2.0 token revocation endpoint using HttpClient extension methods The client library for [OAuth 2.0 token revocation](https://tools.ietf.org/html/rfc7009) is provided as an extension method for `HttpClient`. The following code revokes an access token at a revocation endpoint: ```csharp var client = new HttpClient(); var result = await client.RevokeTokenAsync(new TokenRevocationRequest { Address = "https://demo.duendesoftware.com/connect/revocation", ClientId = "client", ClientSecret = "secret", Token = accessToken }); ``` The response is of type `TokenRevocationResponse` gives you access to the raw response and to a parsed JSON document (via the `Raw` and `Json` properties). Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (response.IsError) throw new Exception(response.Error); ``` ----- # Token Endpoint > Documentation for the OAuth 2.0 and OpenID Connect token endpoint client library, providing extension methods for HttpClient to handle various token request flows The client library for the token endpoint ([OAuth 2.0](https://tools.ietf.org/html/rfc6749#section-3.2) and [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html#tokenendpoint)) is provided as a set of extension methods for `HttpClient`. This allows creating and managing the lifetime of the `HttpClient` the way you prefer: statically or via a factory like the Microsoft `HttpClientFactory`. ## Requesting a token [Section titled “Requesting a token”](#requesting-a-token) The main extension method is called `RequestTokenAsync`. It has direct support for standard parameters like client ID/secret (or assertion) and grant type, but it also allows setting arbitrary other parameters via a dictionary. All other extensions methods ultimately call this method internally: ```csharp var client = new HttpClient(); var response = await client.RequestTokenAsync(new TokenRequest { Address = "https://demo.duendesoftware.com/connect/token", GrantType = "custom", ClientId = "client", ClientSecret = "secret", Parameters = { { "custom_parameter", "custom value"}, { "scope", "api1" } } }); ``` The response is of type `TokenResponse` and has properties for the standard token response parameters like `access_token`, `expires_in` etc. You also have access to the raw response and to a parsed JSON document (via the `Raw` and `Json` properties). Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (response.IsError) throw new Exception(response.Error); var token = response.AccessToken; var custom = response.Json.TryGetString("custom_parameter"); ``` ## Requesting a token using the `client_credentials` Grant Type [Section titled “Requesting a token using the client\_credentials Grant Type”](#requesting-a-token-using-the-client_credentials-grant-type) The `RequestClientCredentialsToken` extension method has convenience properties for the `client_credentials` grant type: ```csharp var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret", Scope = "api1" }); ``` ## Requesting a token using the `password` Grant Type [Section titled “Requesting a token using the password Grant Type”](#requesting-a-token-using-the-password-grant-type) The `RequestPasswordToken` extension method has convenience properties for the `password` grant type: ```csharp var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret", Scope = "api1", UserName = "bob", Password = "bob" }); ``` ## Requesting a token using the `authorization_code` Grant Type [Section titled “Requesting a token using the authorization\_code Grant Type”](#requesting-a-token-using-the-authorization_code-grant-type) The `RequestAuthorizationCodeToken` extension method has convenience properties for the `authorization_code` grant type and PKCE: ```csharp var response = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest { Address = IdentityServerPipeline.TokenEndpoint, ClientId = "client", ClientSecret = "secret", Code = code, RedirectUri = "https://app.com/callback", // optional PKCE parameter CodeVerifier = "xyz" }); ``` ## Requesting a token using the `refresh_token` Grant Type [Section titled “Requesting a token using the refresh\_token Grant Type”](#requesting-a-token-using-the-refresh_token-grant-type) The `RequestRefreshToken` extension method has convenience properties for the `refresh_token` grant type: ```csharp var response = await _client.RequestRefreshTokenAsync(new RefreshTokenRequest { Address = TokenEndpoint, ClientId = "client", ClientSecret = "secret", RefreshToken = "xyz" }); ``` ## Requesting a Device Token [Section titled “Requesting a Device Token”](#requesting-a-device-token) The `RequestDeviceToken` extension method has convenience properties for the `urn:ietf:params:oauth:grant-type:device_code` grant type: ```csharp var response = await client.RequestDeviceTokenAsync(new DeviceTokenRequest { Address = disco.TokenEndpoint, ClientId = "device", DeviceCode = authorizeResponse.DeviceCode }); ``` ----- # UserInfo Endpoint The client library for the [OpenID Connect UserInfo](https://openid.net/specs/openid-connect-core-1_0.html#userinfo) endpoint is provided as an extension method for `HttpClient`. The following code sends an access token to the UserInfo endpoint: ```csharp var client = new HttpClient(); var response = await client.GetUserInfoAsync(new UserInfoRequest { Address = disco.UserInfoEndpoint, Token = token }); ``` The response is of type `UserInfoResponse` and has properties for the standard response parameters. You also have access to the raw response and to a parsed JSON document (via the `Raw` and `Json` properties). Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (response.IsError) throw new Exception(response.Error); var claims = response.Claims; ``` ----- # Base64 URL Encoding > Documentation for Base64 URL encoding and decoding utilities in Duende IdentityModel, used for JWT token serialization JWT serialization involves transforming the three core components of a JWT (Header, Payload, Signature) into a single, compact, URL-safe string. [Base64 URL encoding](https://tools.ietf.org/html/rfc4648#section-5) is used instead of standard Base64 because it doesn’t include characters like `+`, `/`, or `=`, making it safe to use directly in URLs and HTTP headers without requiring further encoding. In newer .NET versions, you can use the `Base64Url` class found in the `System.Buffers.Text` namespace to decode Base64 payloads using the `DecodeFromChars` method: ```csharp using System.Buffers.Text; var jsonString = Base64Url.DecodeFromChars(payload); ``` Encoding can be done using the `EncodeToString` method: ```csharp using System.Buffers.Text; var bytes = Encoding.UTF8.GetBytes("some string"); var encodedString = Base64Url.EncodeToString(bytes); ``` Alternatively, ASP.NET Core has built-in support for Base64 encoding and decoding via [WebEncoders.Base64UrlEncode](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.webutilities.webencoders.base64urlencode) and [WebEncoders.Base64UrlDecode](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.webutilities.webencoders.base64urldecode). To use these methods, ensure you have the following package installed: ```bash dotnet add package Microsoft.AspNetCore.WebUtilities ``` Then use the following code: ```csharp using System.Text; using Microsoft.AspNetCore.WebUtilities; var bytes = "hello"u8.ToArray(); var b64url = WebEncoders.Base64UrlEncode(bytes); bytes = WebEncoders.Base64UrlDecode(b64url); var text = Encoding.UTF8.GetString(bytes); Console.WriteLine(text); ``` ----- # Protocol and Claim Type Constants > Explore constant string classes provided by IdentityModel for OAuth 2.0, OpenID Connect protocol values, and JWT claim types When working with OAuth 2.0, OpenID Connect and claims, there are a lot of **✨magic strings** for claim types and protocol values. IdentityModel provides a couple of constant strings classes to help with that. ## OAuth 2.0 And OpenID Connect Protocol Values [Section titled “OAuth 2.0 And OpenID Connect Protocol Values”](#oauth-20-and-openid-connect-protocol-values) The `OidcConstants` class provides a set of constants for OAuth 2.0 and OpenID Connect protocol values. #### AuthorizeRequest [Section titled “AuthorizeRequest”](#authorizerequest) | Name | Value | | ------------------- | ----------------------- | | Scope | `scope` | | ResponseType | `response_type` | | ClientId | `client_id` | | RedirectUri | `redirect_uri` | | State | `state` | | ResponseMode | `response_mode` | | Nonce | `nonce` | | Display | `display` | | Prompt | `prompt` | | MaxAge | `max_age` | | UiLocales | `ui_locales` | | IdTokenHint | `id_token_hint` | | LoginHint | `login_hint` | | AcrValues | `acr_values` | | CodeChallenge | `code_challenge` | | CodeChallengeMethod | `code_challenge_method` | | Request | `request` | | RequestUri | `request_uri` | | Resource | `resource` | | DPoPKeyThumbprint | `dpop_jkt` | ### AuthorizeErrors [Section titled “AuthorizeErrors”](#authorizeerrors) | Name | Value | | ------------------------------- | ----------------------------------- | | InvalidRequest | `invalid_request` | | UnauthorizedClient | `unauthorized_client` | | AccessDenied | `access_denied` | | UnsupportedResponseType | `unsupported_response_type` | | InvalidScope | `invalid_scope` | | ServerError | `server_error` | | TemporarilyUnavailable | `temporarily_unavailable` | | UnmetAuthenticationRequirements | `unmet_authentication_requirements` | | InteractionRequired | `interaction_required` | | LoginRequired | `login_required` | | AccountSelectionRequired | `account_selection_required` | | ConsentRequired | `consent_required` | | InvalidRequestUri | `invalid_request_uri` | | InvalidRequestObject | `invalid_request_object` | | RequestNotSupported | `request_not_supported` | | RequestUriNotSupported | `request_uri_not_supported` | | RegistrationNotSupported | `registration_not_supported` | | InvalidTarget | `invalid_target` | ### AuthorizeResponse [Section titled “AuthorizeResponse”](#authorizeresponse) | Name | Value | | ---------------- | ------------------- | | Scope | `scope` | | Code | `code` | | AccessToken | `access_token` | | ExpiresIn | `expires_in` | | TokenType | `token_type` | | RefreshToken | `refresh_token` | | IdentityToken | `id_token` | | State | `state` | | SessionState | `session_state` | | Issuer | `iss` | | Error | `error` | | ErrorDescription | `error_description` | ### DeviceAuthorizationResponse [Section titled “DeviceAuthorizationResponse”](#deviceauthorizationresponse) | Name | Value | | ----------------------- | --------------------------- | | DeviceCode | `device_code` | | UserCode | `user_code` | | VerificationUri | `verification_uri` | | VerificationUriComplete | `verification_uri_complete` | | ExpiresIn | `expires_in` | | Interval | `interval` | ### EndSessionRequest [Section titled “EndSessionRequest”](#endsessionrequest) | Name | Value | | --------------------- | -------------------------- | | IdTokenHint | `id_token_hint` | | PostLogoutRedirectUri | `post_logout_redirect_uri` | | State | `state` | | Sid | `sid` | | Issuer | `iss` | | UiLocales | `ui_locales` | ### TokenRequest [Section titled “TokenRequest”](#tokenrequest) | Name | Value | | ----------------------- | ----------------------- | | GrantType | `grant_type` | | RedirectUri | `redirect_uri` | | ClientId | `client_id` | | ClientSecret | `client_secret` | | ClientAssertion | `client_assertion` | | ClientAssertionType | `client_assertion_type` | | Assertion | `assertion` | | Code | `code` | | RefreshToken | `refresh_token` | | Scope | `scope` | | UserName | `username` | | Password | `password` | | CodeVerifier | `code_verifier` | | TokenType | `token_type` | | Algorithm | `alg` | | Key | `key` | | DeviceCode | `device_code` | | Resource | `resource` | | Audience | `audience` | | RequestedTokenType | `requested_token_type` | | SubjectToken | `subject_token` | | SubjectTokenType | `subject_token_type` | | ActorToken | `actor_token` | | ActorTokenType | `actor_token_type` | | AuthenticationRequestId | `auth_req_id` | ### BackchannelAuthenticationRequest [Section titled “BackchannelAuthenticationRequest”](#backchannelauthenticationrequest) | Name | Value | | ----------------------- | --------------------------- | | Scope | `scope` | | ClientNotificationToken | `client_notification_token` | | AcrValues | `acr_values` | | LoginHintToken | `login_hint_token` | | IdTokenHint | `id_token_hint` | | LoginHint | `login_hint` | | BindingMessage | `binding_message` | | UserCode | `user_code` | | RequestedExpiry | `requested_expiry` | | Request | `request` | | Resource | `resource` | | DPoPKeyThumbprint | `dpop_jkt` | ### BackchannelAuthenticationRequestErrors [Section titled “BackchannelAuthenticationRequestErrors”](#backchannelauthenticationrequesterrors) | Name | Value | | --------------------- | -------------------------- | | InvalidRequestObject | `invalid_request_object` | | InvalidRequest | `invalid_request` | | InvalidScope | `invalid_scope` | | ExpiredLoginHintToken | `expired_login_hint_token` | | UnknownUserId | `unknown_user_id` | | UnauthorizedClient | `unauthorized_client` | | MissingUserCode | `missing_user_code` | | InvalidUserCode | `invalid_user_code` | | InvalidBindingMessage | `invalid_binding_message` | | InvalidClient | `invalid_client` | | AccessDenied | `access_denied` | | InvalidTarget | `invalid_target` | ### TokenRequestTypes [Section titled “TokenRequestTypes”](#tokenrequesttypes) | Name | Value | | ------ | -------- | | Bearer | `bearer` | | Pop | `pop` | ### TokenErrors [Section titled “TokenErrors”](#tokenerrors) | Name | Value | | ----------------------- | --------------------------- | | InvalidRequest | `invalid_request` | | InvalidClient | `invalid_client` | | InvalidGrant | `invalid_grant` | | UnauthorizedClient | `unauthorized_client` | | UnsupportedGrantType | `unsupported_grant_type` | | UnsupportedResponseType | `unsupported_response_type` | | InvalidScope | `invalid_scope` | | AuthorizationPending | `authorization_pending` | | AccessDenied | `access_denied` | | SlowDown | `slow_down` | | ExpiredToken | `expired_token` | | InvalidTarget | `invalid_target` | | InvalidDPoPProof | `invalid_dpop_proof` | | UseDPoPNonce | `use_dpop_nonce` | ### TokenResponse [Section titled “TokenResponse”](#tokenresponse) | Name | Value | | ---------------- | ------------------- | | AccessToken | `access_token` | | ExpiresIn | `expires_in` | | TokenType | `token_type` | | RefreshToken | `refresh_token` | | IdentityToken | `id_token` | | Error | `error` | | ErrorDescription | `error_description` | | BearerTokenType | `Bearer` | | DPoPTokenType | `DPoP` | | IssuedTokenType | `issued_token_type` | | Scope | `scope` | ### BackchannelAuthenticationResponse [Section titled “BackchannelAuthenticationResponse”](#backchannelauthenticationresponse) | Name | Value | | ----------------------- | ------------- | | AuthenticationRequestId | `auth_req_id` | | ExpiresIn | `expires_in` | | Interval | `interval` | ### PushedAuthorizationRequestResponse [Section titled “PushedAuthorizationRequestResponse”](#pushedauthorizationrequestresponse) | Name | Value | | ---------- | ------------- | | ExpiresIn | `expires_in` | | RequestUri | `request_uri` | ### TokenIntrospectionRequest [Section titled “TokenIntrospectionRequest”](#tokenintrospectionrequest) | Name | Value | | ------------- | ----------------- | | Token | `token` | | TokenTypeHint | `token_type_hint` | ### RegistrationResponse [Section titled “RegistrationResponse”](#registrationresponse) | Name | Value | | ----------------------- | --------------------------- | | Error | `error` | | ErrorDescription | `error_description` | | ClientId | `client_id` | | ClientSecret | `client_secret` | | RegistrationAccessToken | `registration_access_token` | | RegistrationClientUri | `registration_client_uri` | | ClientIdIssuedAt | `client_id_issued_at` | | ClientSecretExpiresAt | `client_secret_expires_at` | | SoftwareStatement | `software_statement` | ### ClientMetadata [Section titled “ClientMetadata”](#clientmetadata) | Name | Value | | ------------------------------------------- | -------------------------------------- | | RedirectUris | `redirect_uris` | | ResponseTypes | `response_types` | | GrantTypes | `grant_types` | | ApplicationType | `application_type` | | Contacts | `contacts` | | ClientName | `client_name` | | LogoUri | `logo_uri` | | ClientUri | `client_uri` | | PolicyUri | `policy_uri` | | TosUri | `tos_uri` | | JwksUri | `jwks_uri` | | Jwks | `jwks` | | SectorIdentifierUri | `sector_identifier_uri` | | Scope | `scope` | | PostLogoutRedirectUris | `post_logout_redirect_uris` | | FrontChannelLogoutUri | `frontchannel_logout_uri` | | FrontChannelLogoutSessionRequired | `frontchannel_logout_session_required` | | BackchannelLogoutUri | `backchannel_logout_uri` | | BackchannelLogoutSessionRequired | `backchannel_logout_session_required` | | SoftwareId | `software_id` | | SoftwareStatement | `software_statement` | | SoftwareVersion | `software_version` | | SubjectType | `subject_type` | | TokenEndpointAuthenticationMethod | `token_endpoint_auth_method` | | TokenEndpointAuthenticationSigningAlgorithm | `token_endpoint_auth_signing_alg` | | DefaultMaxAge | `default_max_age` | | RequireAuthenticationTime | `require_auth_time` | | DefaultAcrValues | `default_acr_values` | | InitiateLoginUri | `initiate_login_uri` | | RequestUris | `request_uris` | | IdentityTokenSignedResponseAlgorithm | `id_token_signed_response_alg` | | IdentityTokenEncryptedResponseAlgorithm | `id_token_encrypted_response_alg` | | IdentityTokenEncryptedResponseEncryption | `id_token_encrypted_response_enc` | | UserinfoSignedResponseAlgorithm | `userinfo_signed_response_alg` | | UserInfoEncryptedResponseAlgorithm | `userinfo_encrypted_response_alg` | | UserinfoEncryptedResponseEncryption | `userinfo_encrypted_response_enc` | | RequestObjectSigningAlgorithm | `request_object_signing_alg` | | RequestObjectEncryptionAlgorithm | `request_object_encryption_alg` | | RequestObjectEncryptionEncryption | `request_object_encryption_enc` | | RequireSignedRequestObject | `require_signed_request_object` | | AlwaysUseDPoPBoundAccessTokens | `dpop_bound_access_tokens` | | IntrospectionSignedResponseAlgorithm | `introspection_signed_response_alg` | | IntrospectionEncryptedResponseAlgorithm | `introspection_encrypted_response_alg` | | IntrospectionEncryptedResponseEncryption | `introspection_encrypted_response_enc` | ### TokenTypes [Section titled “TokenTypes”](#tokentypes) | Name | Value | | ------------- | --------------- | | AccessToken | `access_token` | | IdentityToken | `id_token` | | RefreshToken | `refresh_token` | ### TokenTypeIdentifiers [Section titled “TokenTypeIdentifiers”](#tokentypeidentifiers) | Name | Value | | ------------- | ------------------------------------------------ | | AccessToken | `urn:ietf:params:oauth:token-type:access_token` | | IdentityToken | `urn:ietf:params:oauth:token-type:id_token` | | RefreshToken | `urn:ietf:params:oauth:token-type:refresh_token` | | Saml11 | `urn:ietf:params:oauth:token-type:saml1` | | Saml2 | `urn:ietf:params:oauth:token-type:saml2` | | Jwt | `urn:ietf:params:oauth:token-type:jwt` | ### AuthenticationSchemes [Section titled “AuthenticationSchemes”](#authenticationschemes) | Name | Value | | ------------------------- | ------------------ | | AuthorizationHeaderBearer | `Bearer` | | AuthorizationHeaderDPoP | `DPoP` | | FormPostBearer | `access_token` | | QueryStringBearer | `access_token` | | AuthorizationHeaderPop | `PoP` | | FormPostPop | `pop_access_token` | | QueryStringPop | `pop_access_token` | ### GrantTypes [Section titled “GrantTypes”](#granttypes) | Name | Value | | ----------------- | ------------------------------------------------- | | Password | `password` | | AuthorizationCode | `authorization_code` | | ClientCredentials | `client_credentials` | | RefreshToken | `refresh_token` | | Implicit | `implicit` | | Saml2Bearer | `urn:ietf:params:oauth:grant-type:saml2-bearer` | | JwtBearer | `urn:ietf:params:oauth:grant-type:jwt-bearer` | | DeviceCode | `urn:ietf:params:oauth:grant-type:device_code` | | TokenExchange | `urn:ietf:params:oauth:grant-type:token-exchange` | | Ciba | `urn:openid:params:grant-type:ciba` | ### ClientAssertionTypes [Section titled “ClientAssertionTypes”](#clientassertiontypes) | Name | Value | | ---------- | ---------------------------------------------------------- | | JwtBearer | `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` | | SamlBearer | `urn:ietf:params:oauth:client-assertion-type:saml2-bearer` | ### ResponseTypes [Section titled “ResponseTypes”](#responsetypes) | Name | Value | | ---------------- | --------------------- | | Code | `code` | | Token | `token` | | IdToken | `id_token` | | IdTokenToken | `id_token token` | | CodeIdToken | `code id_token` | | CodeToken | `code token` | | CodeIdTokenToken | `code id_token token` | ### ResponseModes [Section titled “ResponseModes”](#responsemodes) | Name | Value | | -------- | ----------- | | FormPost | `form_post` | | Query | `query` | | Fragment | `fragment` | ### DisplayModes [Section titled “DisplayModes”](#displaymodes) | Name | Value | | ----- | ------- | | Page | `page` | | Popup | `popup` | | Touch | `touch` | | Wap | `wap` | ### PromptModes [Section titled “PromptModes”](#promptmodes) | Name | Value | | ------------- | ---------------- | | None | `none` | | Login | `login` | | Consent | `consent` | | SelectAccount | `select_account` | | Create | `create` | ### CodeChallengeMethods [Section titled “CodeChallengeMethods”](#codechallengemethods) | Name | Value | | ------ | ------- | | Plain | `plain` | | Sha256 | `S256` | ### ProtectedResourceErrors [Section titled “ProtectedResourceErrors”](#protectedresourceerrors) | Name | Value | | ----------------- | -------------------- | | InvalidToken | `invalid_token` | | ExpiredToken | `expired_token` | | InvalidRequest | `invalid_request` | | InsufficientScope | `insufficient_scope` | ### EndpointAuthenticationMethods [Section titled “EndpointAuthenticationMethods”](#endpointauthenticationmethods) | Name | Value | | ----------------------- | ----------------------------- | | PostBody | `client_secret_post` | | BasicAuthentication | `client_secret_basic` | | PrivateKeyJwt | `private_key_jwt` | | TlsClientAuth | `tls_client_auth` | | SelfSignedTlsClientAuth | `self_signed_tls_client_auth` | ### AuthenticationMethods [Section titled “AuthenticationMethods”](#authenticationmethods) | Name | Value | | ----------------------------------- | -------- | | FacialRecognition | `face` | | FingerprintBiometric | `fpt` | | Geolocation | `geo` | | ProofOfPossessionHardwareSecuredKey | `hwk` | | IrisScanBiometric | `iris` | | KnowledgeBasedAuthentication | `kba` | | MultipleChannelAuthentication | `mca` | | MultiFactorAuthentication | `mfa` | | OneTimePassword | `otp` | | PersonalIdentificationOrPattern | `pin` | | ProofOfPossessionKey | `pop` | | Password | `pwd` | | RiskBasedAuthentication | `rba` | | RetinaScanBiometric | `retina` | | SmartCard | `sc` | | ConfirmationBySms | `sms` | | ProofOfPossessionSoftwareSecuredKey | `swk` | | ConfirmationByTelephone | `tel` | | UserPresenceTest | `user` | | VoiceBiometric | `vbm` | | WindowsIntegratedAuthentication | `wia` | ### Algorithms [Section titled “Algorithms”](#algorithms) #### Symmetric [Section titled “Symmetric”](#symmetric) | Name | Value | | ----- | ------- | | HS256 | `HS256` | | HS384 | `HS384` | | HS512 | `HS512` | #### Asymmetric [Section titled “Asymmetric”](#asymmetric) | Name | Value | | ----- | ------- | | RS256 | `RS256` | | RS384 | `RS384` | | RS512 | `RS512` | | ES256 | `ES256` | | ES384 | `ES384` | | ES512 | `ES512` | | PS256 | `PS256` | | PS384 | `PS384` | | PS512 | `PS512` | ### Discovery [Section titled “Discovery”](#discovery) | Name | Value | | ------------------------------------------- | -------------------------------------------------- | | Issuer | `issuer` | | AuthorizationEndpoint | `authorization_endpoint` | | DeviceAuthorizationEndpoint | `device_authorization_endpoint` | | TokenEndpoint | `token_endpoint` | | UserInfoEndpoint | `userinfo_endpoint` | | IntrospectionEndpoint | `introspection_endpoint` | | RevocationEndpoint | `revocation_endpoint` | | DiscoveryEndpoint | `.well-known/openid-configuration` | | JwksUri | `jwks_uri` | | EndSessionEndpoint | `end_session_endpoint` | | CheckSessionIframe | `check_session_iframe` | | RegistrationEndpoint | `registration_endpoint` | | MtlsEndpointAliases | `mtls_endpoint_aliases` | | PushedAuthorizationRequestEndpoint | `pushed_authorization_request_endpoint` | | FrontChannelLogoutSupported | `frontchannel_logout_supported` | | FrontChannelLogoutSessionSupported | `frontchannel_logout_session_supported` | | BackChannelLogoutSupported | `backchannel_logout_supported` | | BackChannelLogoutSessionSupported | `backchannel_logout_session_supported` | | GrantTypesSupported | `grant_types_supported` | | CodeChallengeMethodsSupported | `code_challenge_methods_supported` | | ScopesSupported | `scopes_supported` | | SubjectTypesSupported | `subject_types_supported` | | ResponseModesSupported | `response_modes_supported` | | ResponseTypesSupported | `response_types_supported` | | ClaimsSupported | `claims_supported` | | TokenEndpointAuthenticationMethodsSupported | `token_endpoint_auth_methods_supported` | | ClaimsLocalesSupported | `claims_locales_supported` | | ClaimsParameterSupported | `claims_parameter_supported` | | ClaimTypesSupported | `claim_types_supported` | | DisplayValuesSupported | `display_values_supported` | | AcrValuesSupported | `acr_values_supported` | | IdTokenEncryptionAlgorithmsSupported | `id_token_encryption_alg_values_supported` | | IdTokenEncryptionEncValuesSupported | `id_token_encryption_enc_values_supported` | | IdTokenSigningAlgorithmsSupported | `id_token_signing_alg_values_supported` | | OpPolicyUri | `op_policy_uri` | | OpTosUri | `op_tos_uri` | | RequestObjectEncryptionAlgorithmsSupported | `request_object_encryption_alg_values_supported` | | RequestObjectEncryptionEncValuesSupported | `request_object_encryption_enc_values_supported` | | RequestObjectSigningAlgorithmsSupported | `request_object_signing_alg_values_supported` | | RequestParameterSupported | `request_parameter_supported` | | RequestUriParameterSupported | `request_uri_parameter_supported` | | RequireRequestUriRegistration | `require_request_uri_registration` | | ServiceDocumentation | `service_documentation` | | TokenEndpointAuthSigningAlgorithmsSupported | `token_endpoint_auth_signing_alg_values_supported` | | UILocalesSupported | `ui_locales_supported` | | UserInfoEncryptionAlgorithmsSupported | `userinfo_encryption_alg_values_supported` | | UserInfoEncryptionEncValuesSupported | `userinfo_encryption_enc_values_supported` | | UserInfoSigningAlgorithmsSupported | `userinfo_signing_alg_values_supported` | | TlsClientCertificateBoundAccessTokens | `tls_client_certificate_bound_access_tokens` | | AuthorizationResponseIssParameterSupported | `authorization_response_iss_parameter_supported` | | PromptValuesSupported | `prompt_values_supported` | | IntrospectionSigningAlgorithmsSupported | `introspection_signing_alg_values_supported` | | IntrospectionEncryptionAlgorithmsSupported | `introspection_encryption_alg_values_supported` | | IntrospectionEncryptionEncValuesSupported | `introspection_encryption_enc_values_supported` | ### BackchannelTokenDeliveryModes [Section titled “BackchannelTokenDeliveryModes”](#backchanneltokendeliverymodes) | Name | Value | | ---- | ------ | | Poll | `poll` | | Ping | `ping` | | Push | `push` | ### Events [Section titled “Events”](#events) | Name | Value | | ----------------- | ---------------------------------------------------- | | BackChannelLogout | `http://schemas.openid.net/event/backchannel-logout` | ### BackChannelLogoutRequest [Section titled “BackChannelLogoutRequest”](#backchannellogoutrequest) | Name | Value | | ----------- | -------------- | | LogoutToken | `logout_token` | ### StandardScopes [Section titled “StandardScopes”](#standardscopes) | Name | Value | Description | | ------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | OpenId | `openid` | REQUIRED. Indicates the Client is making an OpenID Connect request. The behavior is unspecified if this is not included. | | Profile | `profile` | OPTIONAL. Requests access to End-User’s default profile Claims such as `name`, `family_name`, `given_name`, etc. | | Email | `email` | OPTIONAL. Requests access to the `email` and `email_verified` Claims. | | Address | `address` | OPTIONAL. Requests access to the `address` Claim. | | Phone | `phone` | OPTIONAL. Requests access to `phone_number` and `phone_number_verified` Claims. | | OfflineAccess | `offline_access` | MUST NOT be used with the OpenID Connect Implicit Client Implementer’s Guide. Used in accordance with the OpenID Connect Basic Client Implementer’s Guide. | ### HttpHeaders [Section titled “HttpHeaders”](#httpheaders) | Name | Value | | --------- | ------------ | | DPoP | `DPoP` | | DPoPNonce | `DPoP-Nonce` | ## JWT Claim Types [Section titled “JWT Claim Types”](#jwt-claim-types) The `JwtClaimTypes` class has all standard claim types found in the OpenID Connect, JWT and OAuth 2.0 specs -many of them are also aggregated at [IANA](https://www.iana.org/assignments/jwt/jwt.xhtml). | Claim Type | Value | Description/Remarks | | :---------------------------------- | :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Subject | `sub` | Unique Identifier for the End-User at the Issuer. | | Name | `name` | End-User’s full name in displayable form including all name parts, possibly including titles and suffixes, ordered according to the End-User’s locale and preferences. | | GivenName | `given_name` | Given name(s) or first name(s) of the End-User. Note that in some cultures, people can have multiple given names; all can be present, with the names being separated by space characters. | | FamilyName | `family_name` | Surname(s) or last name(s) of the End-User. Note that in some cultures, people can have multiple family names or no family name; all can be present, with the names being separated by space characters. | | MiddleName | `middle_name` | Middle name(s) of the End-User. Note that in some cultures, people can have multiple middle names; all can be present, with the names being separated by space characters. Also note that in some cultures, middle names are not used. | | NickName | `nickname` | Casual name of the End-User that may or may not be the same as the given\_name. For instance, a nickname value of Mike might be returned alongside a given\_name value of Michael. | | PreferredUserName | `preferred_username` | Shorthand name by which the End-User wishes to be referred to at the RP, such as janedoe or j.doe. This value MAY be any valid JSON string including special characters. **Remarks:** The relying party MUST NOT rely upon this value being unique, as discussed in the OpenID Connect specification. | | Profile | `profile` | URL of the End-User’s profile page. The contents of this Web page SHOULD be about the End-User. | | Picture | `picture` | URL of the End-User’s profile picture. This URL MUST refer to an image file (e.g., PNG, JPEG, or GIF image file). **Remarks:** This URL SHOULD specifically reference a profile photo of the End-User rather than an arbitrary photo. | | WebSite | `website` | URL of the End-User’s Web page or blog. This Web page SHOULD contain information published by the End-User or an organization related to the End-User. | | Email | `email` | End-User’s preferred e-mail address. Its value MUST conform to the RFC 5322 syntax. The relying party MUST NOT rely upon this value being unique. | | EmailVerified | `email_verified` | `"true"` if the End-User’s e-mail address has been verified; otherwise `"false"`. **Remarks:** Verification methods vary depending on trust frameworks or agreements. | | Gender | `gender` | End-User’s gender. Allowed values include `"female"` and `"male"`, with additional values permissible when the predefined ones are not applicable. | | BirthDate | `birthdate` | End-User’s birthday in ISO 8601 format (e.g., YYYY-MM-DD). The year MAY be `0000`, indicating it is omitted. | | ZoneInfo | `zoneinfo` | String representing the End-User’s time zone, e.g., `Europe/Paris` or `America/Los_Angeles`. | | Locale | `locale` | End-User’s locale represented as a BCP47 language tag (e.g., `en-US`, `fr-CA`). Compatibility notes suggest some implementations may use underscores instead of dashes. | | PhoneNumber | `phone_number` | End-User’s preferred telephone number. E.164 format is recommended, including extensions. | | PhoneNumberVerified | `phone_number_verified` | `"true"` if the End-User’s phone number has been verified; otherwise `"false"`. **Remarks:** Applies to numbers in E.164 format. | | Address | `address` | End-User’s preferred postal address. Contains a JSON structure with predefined fields from the OpenID Connect specification. | | Audience | `aud` | Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client\_id of the Relying Party. | | Issuer | `iss` | Issuer Identifier for the Issuer of the response in the form of a URL. | | NotBefore | `nbf` | The time before which the JWT MUST NOT be accepted, specified in seconds since 1970-01-01T00:00:00Z. | | Expiration | `exp` | The token’s expiration time in seconds since 1970-01-01T00:00:00Z. | | UpdatedAt | `updated_at` | Time of last update for the End-User’s information, measured in seconds since 1970-01-01T00:00:00Z. | | IssuedAt | `iat` | Time at which the JWT was issued, specified in seconds since 1970-01-01T00:00:00Z. | | AuthenticationMethod | `amr` | JSON array of strings identifying the authentication method(s) used. | | SessionId | `sid` | Session identifier representing an OP session at an RP for a logged-in End-User. | | AuthenticationContextClassReference | `acr` | Specifies the Authentication Context Class Reference value satisfied during authentication. **Remarks:** Example: `"level 0"` indicates authentication did not meet ISO/IEC 29115 level 1. | | AuthenticationTime | `auth_time` | Time of the End-User’s authentication, measured in seconds since 1970-01-01T00:00:00Z. | | AuthorizedParty | `azp` | Authorized party to which the ID Token was issued. | | AccessTokenHash | `at_hash` | Access token hash value derived using a specific hash algorithm. | | AuthorizationCodeHash | `c_hash` | Authorization code hash value derived using a specific hash algorithm. | | StateHash | `s_hash` | State hash value derived using a specific hash algorithm. | | Nonce | `nonce` | Value used to mitigate replay attacks between a Client session and an ID Token. | | JwtId | `jti` | A unique identifier for the token to prevent reuse. | | Events | `events` | Defines a set of event statements to describe a logical event that has occurred. | | ClientId | `client_id` | OAuth 2.0 Client Identifier valid at the Authorization Server. | | Scope | `scope` | OpenID Connect “openid” scope value. Additional scope values can be included. | | Actor | `act` | Identifies the acting party to whom authority has been delegated. | | MayAct | `may_act` | Statement asserting that a party is authorized to act on behalf of another party. | | Id | `id` | An identifier. | | IdentityProvider | `idp` | The identity provider. | | Role | `role` | The role. | | Roles | `roles` | The roles. | | ReferenceTokenId | `reference_token_id` | Reference token identifier. | | Confirmation | `cnf` | The confirmation. | | Algorithm | `alg` | The algorithm. | | JsonWebKey | `jwk` | JSON web key. | | TokenType | `typ` | The token type. | | DPoPHttpMethod | `htm` | DPoP HTTP method. | | DPoPHttpUrl | `htu` | DPoP HTTP URL. | | DPoPAccessTokenHash | `ath` | DPoP access token hash. | ### JwtTypes [Section titled “JwtTypes”](#jwttypes) `JwtTypes` is a nested class that provides a set of constants for confirmation methods. It can be found under the `JwtConstants` class. | Type | Value | Description | | :----------------------- | :-------------------------- | :---------------------------------------------------------- | | AccessToken | `at+jwt` | OAuth 2.0 access token. | | AuthorizationRequest | `oauth-authz-req+jwt` | JWT secured authorization request. | | DPoPProofToken | `dpop+jwt` | DPoP proof token. | | IntrospectionJwtResponse | `token-introspection+jwt` | Token introspection JWT response. | | ClientAuthentication | `client-authentication+jwt` | Client authentication JWT (for use with private\_key\_jwt). | ### ConfirmationMethods [Section titled “ConfirmationMethods”](#confirmationmethods) `ConfirmationMethods` is a nested class that provides a set of constants for confirmation methods. It can be found under the `JwtConstants` class. | Method | Value | Description | | :------------------- | :--------- | :----------------------------------------- | | JsonWebKey | `jwk` | JSON web key. | | JwkThumbprint | `jkt` | JSON web key thumbprint. | | X509ThumbprintSha256 | `x5t#S256` | X.509 certificate thumbprint using SHA256. | ----- # Epoch Time Conversion > Learn about converting between DateTime and Unix/Epoch time formats in Duende IdentityModel for JWT tokens JSON Web Token (JWT) tokens use so-called [Epoch or Unix time](https://en.wikipedia.org/wiki/Unix_time) to represent date/times, which is the number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT). In .NET, you can convert `DateTimeOffset` to Unix/Epoch time via the two methods of `ToUnixTimeSeconds` and `ToUnixTimeMilliseconds`: EpochTimeExamples.cs ```csharp var seconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var milliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); ``` ----- # Creating Authorize and EndSession URLs > Helper utilities for creating OAuth 2.0/OpenID Connect authorization and end session URLs with query parameters The *RequestUrl* class is a helper for creating URLs with query string parameters, e.g.: ```csharp var ru = new RequestUrl("https://server/endpoint"); // produces https://server/endpoint?foo=foo&bar=bar var url = ru.Create(new { foo: "foo", bar: "bar" }); ``` As a parameter to the *Create* method you can either pass in an object, or a string dictionary. In both cases the properties/values will be serialized to key/value pairs. ## Authorization Endpoint [Section titled “Authorization Endpoint”](#authorization-endpoint) For most cases, the [OAuth 2.0](https://tools.ietf.org/html/rfc6749#section-3.1) and [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html#authorizationendpoint) authorization endpoint expects a GET request with a number of query string parameters. The *CreateAuthorizeUrl* extension method creates URLs for the authorize endpoint - it has support the most common parameters: ```csharp /// /// Creates an authorize URL. /// /// The request. /// The client identifier. /// The response type. /// The scope. /// The redirect URI. /// The state. /// The nonce. /// The login hint. /// The acr values. /// The prompt. /// The response mode. /// The code challenge. /// The code challenge method. /// The display option. /// The max age. /// The ui locales. /// The id_token hint. /// Extra parameters. /// public static string CreateAuthorizeUrl(this RequestUrl request, string clientId, string responseType, string scope = null, string redirectUri = null, string state = null, string nonce = null, string loginHint = null, string acrValues = null, string prompt = null, string responseMode = null, string codeChallenge = null, string codeChallengeMethod = null, string display = null, int? maxAge = null, string uiLocales = null, string idTokenHint = null, object extra = null) { ... } ``` Example: ```csharp var ru = new RequestUrl("https://demo.duendesoftware.com/connect/authorize"); var url = ru.CreateAuthorizeUrl( clientId: "client", responseType: "implicit", redirectUri: "https://app.com/callback", nonce: "xyz", scope: "openid"); ``` ## EndSession Endpoint [Section titled “EndSession Endpoint”](#endsession-endpoint) The *CreateEndSessionUrl* extensions methods supports the most common parameters: ```csharp /// /// Creates a end_session URL. /// /// The request. /// The id_token hint. /// The post logout redirect URI. /// The state. /// The extra parameters. /// public static string CreateEndSessionUrl(this RequestUrl request, string idTokenHint = null, string postLogoutRedirectUri = null, string state = null, object extra = null) { ... } ``` ----- # Time-Constant String Comparison > Learn about implementing secure string comparison to prevent timing attacks in security-sensitive contexts using TimeConstantComparer When comparing strings in a security context (e.g. comparing keys), you should avoid leaking timing information. Standard string comparison algorithms are optimized to stop comparing characters as soon as a difference is found. An attacker can exploit this by making many requests with strings that all differ in the first character. The strings that begin with an incorrect first character will make a single character comparison and stop. However, the strings that begin with a correct first character will need to make additional string comparisons, and thus take more time before they stop. Sophisticated attackers can measure this difference and use it to deduce the characters that their input is being compared to. ## Time-Constant String Comparison [Section titled “Time-Constant String Comparison”](#time-constant-string-comparison) ```csharp using System.Security.Cryptography; // Simulated sensitive data (e.g., a secure token or password hash) var storedHash = Convert.FromBase64String("HJG3+eXAIoQsNI1ASD2i+If7xhQAEjZLefBWo5pcuDE="); // Incoming hash to validate (e.g., provided by the user) var providedHash = Convert.FromBase64String("HJG3+eXAIoQsNI1ASD2i+If7xhQAEjZLefBWo5pcuDE="); // Compare the two byte sequences using FixedTimeEquals var isEqual = CryptographicOperations.FixedTimeEquals(storedHash, providedHash); var result = isEqual ? "the hashes match!" : "the hashes do not match!"; Console.WriteLine(result); ``` ## TimeConstantComparer [Section titled “TimeConstantComparer”](#timeconstantcomparer) The *TimeConstantComparer* class defends against these timing attacks by implementing a constant-time string comparison. The string comparison is a constant-time operation in the sense that comparing strings of equal length always performs the same amount of work. Usage example: ```csharp using Duende.IdentityModel; // Simulated sensitive data (e.g., a secure token or password hash) var storedHash = "HJG3+eXAIoQsNI1ASD2i+If7xhQAEjZLefBWo5pcuDE="; // Incoming hash to validate (e.g., provided by the user) var providedHash = "HJG3+eXAIoQsNI1ASD2i+If7xhQAEjZLefBWo5pcuDE="; // Compare the two byte sequences using FixedTimeEquals var isEqual = TimeConstantComparer.IsEqual(storedHash, providedHash); var result = isEqual ? "the hashes match!" : "the hashes do not match!"; Console.WriteLine(result); ``` ----- # Fluent X.509 Certificate Store API > Provides a simplified, fluent API for accessing and managing X.509 certificates in a certificate store. A common place to store X.509 certificates is within a host’s X.509 certificate store. With .NET APIs, this is done using the `X509Store` class. ```csharp using System.Security.Cryptography.X509Certificates; // with .NET APIs using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadOnly); using var certificate = store.Certificates .Find(X509FindType.FindBySubjectDistinguishedName, "CN=localhost", false)[0]; if (certificate == null) throw new InvalidOperationException("Certificate not found"); Console.WriteLine(certificate); ``` The *X509* class in the IdentityModel library is a simplified API to load certificates from a certificate store. The following code loads a certificate by name from the personal machine store: ```csharp using Duende.IdentityModel; using var certificate = X509.CurrentUser .My .SubjectDistinguishedName .Find("CN=localhost", false) .FirstOrDefault(); if (certificate == null) throw new InvalidOperationException("Certificate not found"); Console.WriteLine(certificate); ``` ### Certificate Store Locations [Section titled “Certificate Store Locations”](#certificate-store-locations) You can load certificates from the following machine or user stores: * *My* * *AddressBook* * *TrustedPeople* * *CertificateAuthority* * *TrustedPublisher* ### Certificate Search Options [Section titled “Certificate Search Options”](#certificate-search-options) You can search for a certificate by the following attributes: * Subject name, * Thumbprint * Issuer name * Serial number. ### Debugging Certificates in a Store [Section titled “Debugging Certificates in a Store”](#debugging-certificates-in-a-store) When finding it difficult to find a certificate by name, you can use the following code to list all certificates in a store for debugging purposes: ```csharp using System.Security.Cryptography.X509Certificates; using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadOnly); var certificates = store.Certificates; foreach (var certificate in certificates) { Console.WriteLine($"{certificate.Subject} ({certificate.Thumbprint})"); } ```