Client Authentication
Confidential and credentialed clients need to authenticate with your IdentityServer before they can request tokens.
Duende IdentityServer has built-in support for various client credential types and authentication methods, and an extensible infrastructure to customize the authentication system.
We recommend using asymmetric client credentials like the private key JWT or Mutual TLS (mTLS) authentication method over shared secrets.
Assigning Secrets
Section titled “Assigning Secrets”A client secret is abstracted by the Secret class. It provides properties for setting the value and type and a description and expiration date.
var secret = new Secret{ Value = "foo", Type = "bar",
Description = "my custom secret", Expiration = new DateTime(2021,12,31)}You can assign multiple secrets to a client to enable roll-over scenarios, e.g.:
var primary = new Secret("foo");var secondary = new Secret("bar");
client.ClientSecrets = new[] { primary, secondary };Secret Parsing
Section titled “Secret Parsing”During request processing, the secret must be somehow extracted from the incoming request. The various specs describe a couple of options, e.g. as part of the authorization header or the body payload.
It is the job of implementations of the ISecretParser interface to accomplish this. You can add secret parsers by calling the AddSecretParser() service provider extension method.
The following secret parsers are part of Duende IdentityServer:
-
Duende.IdentityServer.Validation.BasicAuthenticationSecretParserparses an OAuth basic authentication formatted
Authorizationheader. Enabled by default. -
Duende.IdentityServer.Validation.PostBodySecretParserParses from the
client_idandclient_secretbody fields. Enabled by default. -
Duende.IdentityServer.Validation.JwtBearerClientAssertionSecretParserParses a JWT on the
client_assertionbody field. Can be enabled by calling theAddJwtBearerClientAuthenticationservice provider extension method. -
Duende.IdentityServer.Validation.MutualTlsSecretParserParses the
client_idbody field and TLS client certificate. Can be enabled by calling theAddMutualTlsSecretValidatorsservice provider extension method.
Secret Validation
Section titled “Secret Validation”It is the job of implementations of the ISecretValidator interface to validate the extracted credentials.
You can add secret validators by calling the AddSecretValidator() service provider extension method.
The following secret validators are part of Duende IdentityServer:
-
Duende.IdentityServer.Validation.HashedSharedSecretValidatorValidates shared secrets that are stored hashed. Enabled by default.
-
Duende.IdentityServer.Validation.PlainTextSharedSecretValidatorValidates shared secrets that are stored in plaintext.
-
Duende.IdentityServer.Validation.PrivateKeyJwtSecretValidatorValidates JWTs that are signed with either X.509 certificates or keys wrapped in a JWK. Can be enabled by calling the
AddJwtBearerClientAuthenticationservice provider extension method. -
Duende.IdentityServer.Validation.X509ThumbprintSecretValidatorValidates X.509 client certificates based on a thumbprint. Can be enabled by calling the
AddMutualTlsSecretValidatorsservice provider extension method. -
Duende.IdentityServer.Validation.X509NameSecretValidatorValidates X.509 client certificates based on a common name. Can be enabled by calling the
AddMutualTlsSecretValidatorsservice provider extension method.
Shared Secrets
Section titled “Shared Secrets”Using shared secrets is by far the most common technique for authenticating clients.
From a security point of view, they have some shortcomings:
- the shared secrets must be transmitted over the network during authentication
- they should not be persisted in clear text to reduce the risk of leaking them
- they should have high entropy to avoid brute-force attacks
The following creates a shared secret:
// loadSecret is responsible for loading a SHA256 or SHA512 hash of a good,// high-entropy secret from a secure storage locationvar hash = loadSecretHash();var secret = new Secret(hash);IdentityServer’s Secrets are designed to operate on either a SHA256 or SHA512 hash of the shared secret. The shared secret is not stored in IdentityServer - only the hash. The client on the hand needs access to the clear text of the secret. It must send the clear text to authenticate itself.
IdentityServer provides the Sha256 and Sha512 extension methods on strings
as a convenience to produce their hashes. These extension methods can be used
when prototyping or during demos to get started quickly. However, the clear text
of secrets used in production should never be written down in your source code.
Anyone with access to the repository can see the secret.
var compromisedSecret = new Secret("just for demos, not prod!".Sha256());Authentication Using A Shared Secret
Section titled “Authentication Using A Shared Secret”You can either send the client id/secret combination as part of the POST body:
POST /connect/token
Content-type: application/x-www-form-urlencoded
client_id=client& client_secret=secret&
grant_type=authorization_code& code=hdh922& redirect_uri=https://myapp.com/callback…or as a basic authentication header:
POST /connect/token
Content-type: application/x-www-form-urlencodedAuthorization: Basic xxxxx
grant_type=authorization_code& code=hdh922& redirect_uri=https://myapp.com/callback.NET Client Library
Section titled “.NET Client Library”You can use the Duende IdentityModel client library to programmatically interact with the protocol endpoint from .NET code.
using Duende.IdentityModel.Client;
var client = new HttpClient();
var response = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest{ Address = TokenEndpoint,
ClientId = "client", ClientSecret = "secret",
Code = "...", CodeVerifier = "...", RedirectUri = "https://app.com/callback"});Private Key JWTs
Section titled “Private Key JWTs”The OpenID Connect specification recommends a client authentication method based on asymmetric keys. With this approach, instead of transmitting the shared secret over the network, the client creates a JWT and signs it with its private key. Your IdentityServer only needs to store the corresponding key to be able to validate the signature.
The technique is described here and is based on the OAuth JWT assertion specification (RFC 7523).
Setting Up A Private Key JWT Secret
Section titled “Setting Up A Private Key JWT Secret”The default private key JWT secret validator expects either a base64 encoded X.509 certificate or a JSON Web Key formatted RSA, EC or symmetric key on the secret definition:
var client = new Client{ ClientId = "client.jwt",
ClientSecrets = { new Secret { // base64 encoded X.509 certificate Type = IdentityServerConstants.SecretTypes.X509CertificateBase64,
Value = "MIID...xBXQ=" }, new Secret { // JWK formatted RSA key Type = IdentityServerConstants.SecretTypes.JsonWebKey,
Value = "{'e':'AQAB','kid':'Zz...GEA','kty':'RSA','n':'wWw...etgKw'}" } },
AllowedGrantTypes = GrantTypes.ClientCredentials, AllowedScopes = { "api1", "api2" }};Authentication Using A Private Key JWT
Section titled “Authentication Using A Private Key JWT”On the client side, the caller must first generate the JWT, and then send it on the client_assertion body field:
POST /connect/token
Content-type: application/x-www-form-urlencoded
client_assertion=<jwt>& client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&
grant_type=authorization_code& code=hdh922& redirect_uri=https://myapp.com/callback.NET Client Library
Section titled “.NET Client Library”You can use the Microsoft JWT library to create JSON Web Tokens.
private static string CreateClientToken(SigningCredentials credential, string clientId, string issuer){ var now = DateTime.UtcNow;
var token = new JwtSecurityToken( clientId, issuer, // audience: the authorization server's issuer identifier new List<Claim>() { new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()), new Claim(JwtClaimTypes.Subject, clientId), new Claim(JwtClaimTypes.IssuedAt, now.ToEpochTime().ToString(), ClaimValueTypes.Integer64) }, now, now.AddMinutes(1), credential );
// Mark this JWT as a client authentication token so the authorization server // applies strict audience validation (see "Strict Audience Validation" below). token.Header[JwtClaimTypes.TokenType] = "client-authentication+jwt";
var tokenHandler = new JwtSecurityTokenHandler(); return tokenHandler.WriteToken(token);}…and the Duende IdentityModel client library to programmatically interact with the protocol endpoint from .NET code.
using Duende.IdentityModel.Client;
static async Task<TokenResponse> RequestTokenAsync(SigningCredentials credential){ var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://demo.duendesoftware.com"); if (disco.IsError) throw new Exception(disco.Error);
var clientToken = CreateClientToken(credential, "private.key.jwt", disco.Issuer);
var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = disco.TokenEndpoint, Scope = "api1.scope1",
ClientAssertion = { Type = OidcConstants.ClientAssertionTypes.JwtBearer, Value = clientToken } });
if (response.IsError) throw new Exception(response.Error); return response;}See here for a sample for using JWT-based authentication.
Using ASP.NET Core
Section titled “Using ASP.NET Core”The OpenID Connect authentication handler in ASP.NET Core allows for replacing a static client secret with a dynamically created client assertion.
You can achieve this by handling the various events on the handler. We recommend encapsulating the event handler in a separate type. This makes it easier to consume services from DI:
// some details omittedbuilder.Services.AddTransient<OidcEvents>();
builder.Services.AddAuthentication(options => .AddOpenIdConnect("oidc", options => { options.Authority = Constants.Authority;
// no static client secret options.ClientId = "mvc.jar.jwt";
// specifies type that handles events options.EventsType = typeof(OidcEvents); }));In your event handler you can inject code before the handler redeems the code:
public class OidcEvents : OpenIdConnectEvents{ private readonly AssertionService _assertionService;
public OidcEvents(AssertionService assertionService) { _assertionService = assertionService; }
public override Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context) { context.TokenEndpointRequest.ClientAssertionType = OidcConstants.ClientAssertionTypes.JwtBearer; context.TokenEndpointRequest.ClientAssertion = _assertionService.CreateClientToken();
return Task.CompletedTask; }}The assertion service would be a helper to create the JWT as shown above in the CreateClientToken method.
See here for a sample for using
JWT-based authentication (and signed authorize requests) in ASP.NET Core.
Strict Audience Validation
Section titled “Strict Audience Validation”RFC 10070 closes an audience confusion gap in JWT client authentication. If a client trusts several authorization servers and one of them is malicious or compromised, that server can trick the client into creating an assertion that another server accepts. The fix is to bind each assertion to exactly one authorization server.
Here’s what that means for the assertions your clients create:
- Use the authorization server’s issuer identifier as the only value in
aud. Take it from theissuervalue in the server’s discovery document. Don’t use the token endpoint URL, even though that’s where you send the assertion. - Apply the same rule to assertions you send to the pushed authorization request (PAR) endpoint.
- Set the
typheader toclient-authentication+jwt. The RFC recommends this header, and advises servers not to reject a JWT only because it’s untyped.
This only applies to client authentication. JWT authorization grants (the jwt-bearer grant type) work differently, and
their audience can still be the issuer or the token endpoint.
IdentityServer Behavior
Section titled “IdentityServer Behavior”Strict validation is off by default in both v7 and v8. It applies to an assertion when you enable the option, or when the
assertion has the typ header client-authentication+jwt. In the second case, IdentityServer assumes the client wants
strict validation and applies it to that token.
IdentityServer v8
Section titled “IdentityServer v8”The StrictClientAssertionAudienceValidation option on
IdentityServerOptions defaults to false.
When strict validation doesn’t apply, IdentityServer accepts these legacy audience values:
- The issuer identifier
- The issuer identifier followed by
/connect/token - The token endpoint URL
- The CIBA endpoint URL
- The PAR endpoint URL
This mode exists for compatibility. It doesn’t enforce an issuer-only audience, and it also accepts aud arrays,
including arrays with more than one element.
When strict validation applies, either through the option or the typ header:
audmust be the issuer identifier, as a string or as a single-element array. Arrays with more than one element are rejected.- The
typheader must beclient-authentication+jwt. The RFC only recommends this header, but IdentityServer’s strict mode requires it.
IdentityServer v7
Section titled “IdentityServer v7”In v7, the option is a preview feature named options.Preview.StrictClientAssertionAudienceValidation. It defaults to
false, so if you haven’t opted in, you get the legacy audience behavior.
When strict validation applies, aud must be a string that equals the issuer identifier. Arrays aren’t accepted, not even
single-element ones, and the typ header must be client-authentication+jwt. See the
upgrade guide for how the option moves in v8.
When you roll this out, update your clients first, as RFC 10070 section 6 suggests. Otherwise, clients that still send an endpoint URL as the audience will stop authenticating once you enable the option on the server.
Mutual TLS Client Certificates
Section titled “Mutual TLS Client Certificates”Clients can use an X.509 client certificate as an authentication mechanism to endpoints in your IdentityServer.
For this you need to associate a client certificate with a client in your IdentityServer and enable MTLS support on the options.
var idsvrBuilder = builder.Services.AddIdentityServer(options =>{ options.MutualTls.Enabled = true;})Use the ASP.NET Core service provider extensions methods to add the services to the ASP.NET Core service provider. A default implementation is available to do that either thumbprint or common-name based:
idsvrBuilder.AddMutualTlsSecretValidators();Then add a client secret of type SecretTypes.X509CertificateName (for PKI-based scenarios)
or SecretTypes.X509CertificateThumbprint (for self-issued certificates) to the client you want to authenticate.
For example:
new Client{ ClientId = "mtls.client", AllowedGrantTypes = GrantTypes.ClientCredentials, AllowedScopes = { "api1" },
ClientSecrets = { // name based new Secret(@"CN=client, OU=production, O=company", "client.dn") { Type = SecretTypes.X509CertificateName },
// or thumbprint based new Secret("bca0d040847f843c5ee0fa6eb494837470155868", "mtls.tb") { Type = SecretTypes.X509CertificateThumbprint }, }}Configuring IdentityServer to Accept Client Certificates
Section titled “Configuring IdentityServer to Accept Client Certificates”When configuring mTLS in IdentityServer, you can specify to only expose the mTLS endpoints on a specific domain or subdomain:
var idsvrBuilder = builder.Services.AddIdentityServer(options =>{ options.MutualTls.Enabled = true;
// Only exposes the MTLS endpoints on the mtls subdomain of your IdentityServer host. options.MutualTls.DomainName = "mtls";});Using mTLS however triggers an additional authentication step in the MutualTlsEndpointMiddleware,
calling httpContext.AuthenticateAsync() with the configured client certificate authentication scheme name.
By default, this scheme name is "Certificate", but you can override this when configuring the mTLS options:
var idsvrBuilder = builder.Services.AddIdentityServer(options =>{ options.MutualTls.Enabled = true; options.MutualTls.DomainName = "mtls"; options.MutualTls.ClientCertificateAuthenticationScheme = "Certificate";});In addition, you need to also configure client certificate authentication in ASP.NET Core:
var idsvrBuilder = builder.Services.AddIdentityServer(options =>{ options.MutualTls.Enabled = true; options.MutualTls.DomainName = "mtls"; options.MutualTls.ClientCertificateAuthenticationScheme = "Certificate";});
builder.Services.AddAuthentication() .AddCertificate("Certificate", options => { // Specify which types of certificates to allow: SelfSigned, Chained, or All options.AllowedCertificateTypes = CertificateTypes.SelfSigned; options.ValidateCertificateUse = true; });Further documentation on how to configure client certificate authentication in ASP.NET Core is available on Microsoft Learn
.NET Client Library
Section titled “.NET Client Library”When writing a client to connect to IdentityServer, the SocketsHttpHandler (or HttpClientHandler depending on your
.NET version)
class provides a convenient mechanism to add a client certificate to outgoing requests.
Use such a handler with HttpClient to perform the client certificate authentication handshake at the TLS channel.
The following snippet is using Duende IdentityModel to read the discovery document and
request a token:
static async Task<TokenResponse> RequestTokenAsync(){ var handler = new SocketsHttpHandler(); var cert = new X509Certificate2("client.p12", "password"); handler.SslOptions.ClientCertificates = new X509CertificateCollection { cert };
var client = new HttpClient(handler);
var disco = await client.GetDiscoveryDocumentAsync(Constants.Authority); if (disco.IsError) throw new Exception(disco.Error);
var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = disco.MtlEndpointAliases.TokenEndpoint,
// The default ClientCredentialStyle value is ClientCredentialStyle.AuthorizationHeader, which does not work in a Mutual TLS scenario ClientCredentialStyle = ClientCredentialStyle.PostBody,
ClientId = "mtls.client", Scope = "api1" });
if (response.IsError) throw new Exception(response.Error); return response;}