Confidential client assertions
In order to prove their identity, confidential client applications exchange a secret with Microsoft Entra ID. The secret can be:
- A client secret (application password).
- A certificate, which is used to build a signed assertion containing standard claims.
This secret can also be a signed assertion directly.
MSAL.NET has four methods to provide either credentials or assertions to the confidential client app:
.WithClientSecret()
.WithCertificate()
.WithClientAssertion()
.WithClientClaims()
Note
While it is possible to use the WithClientAssertion()
API to acquire tokens for the confidential client, we do not recommend using it by default as it is more advanced and is designed to handle very specific scenarios which are not common. Using the .WithCertificate()
API will allow MSAL.NET to handle this for you. This api offers you the ability to customize your authentication request if needed but the default assertion created by .WithCertificate()
will suffice for most authentication scenarios. This API can also be used as a workaround in some scenarios where MSAL.NET fails to perform the signing operation internally. The difference between the two is using the WithCertificate()
requires the certificate and private key to be available on the machine creating the assertion, and using the WithClientAssertion()
allows you to compute the assertion somewhere else, like inside the Azure Key Vault or from Managed Identity, or with a Hardware security module.
Client assertions
This is useful if you want to handle the certificate yourself. For example, if you wish to use Azure KeyVault's APIs for signing, which eliminates the need for downloading the certificates. A signed client assertion takes the form of a signed JWT with the payload containing the required authentication claims mandated by Microsoft Entra ID, Base64 encoded. Or it can be a JWT from a different Identity Provider, for the "Federated Identity Credential" scenario.
Use the delegate, which enables you to compute the assertion every time MSAL needs to get a new token from the identity provider. MSAL doesn't invoke your delegate if a token is found in the cache.
string signedClientAssertion = GetOrComputeAssertion();
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithClientAssertion(async (AssertionRequestOptions options) => {
// use 'options.ClientID' or 'options.TokenEndpoint' to generate client assertion
return await GetClientAssertionAsync(options.ClientID, options.TokenEndpoint, options.CancellationToken);
})
.Build();
The claims expected by Microsoft Entra ID in the signed assertion are:
Claim type | Value | Description |
---|---|---|
aud | https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token |
The "aud" (audience) claim identifies the recipients that the JWT is intended for (here Microsoft Entra ID) See RFC 7519, Section 4.1.3. In this case, that recipient is the token endpoint of the identity provider |
exp | 1601519414 | The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. See RFC 7519, Section 4.1.4. This allows the assertion to be used until then, so keep it short - 5-10 minutes after nbf at most. Microsoft Entra ID doesn't place restrictions on the exp time currently. |
iss | {ClientID} | The "iss" (issuer) claim identifies the principal that issued the JWT, in this case your client application. Use the GUID application ID. |
jti | (a Guid) | The "jti" (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be assigned in a manner that ensures that there's a negligible probability that the same value can be accidentally assigned to a different data object. If the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. The "jti" value is a case-sensitive string. RFC 7519, Section 4.1.7 |
nbf | 1601519114 | The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. RFC 7519, Section 4.1.5. Using the current time is appropriate. |
sub | {ClientID} | The "sub" (subject) claim identifies the subject of the JWT, in this case also your application. Use the same value as iss . |
If you use a certificate as a client secret, the certificate must be deployed safely. We recommend that you store the certificate in a secure spot supported by the platform, such as in the certificate store on Windows or by using Azure Key Vault.
Crafting the assertion
This is an example using Microsoft.IdentityModel.JsonWebTokens to create the assertion for you.
string GetSignedClientAssertion(X509Certificate2 certificate, string tenantId, string clientId)
{
// no need to add exp, nbf as JsonWebTokenHandler will add them by default.
var claims = new Dictionary<string, object>()
{
{ "aud", tokenEndpoint },
{ "iss", clientId },
{ "jti", Guid.NewGuid().ToString() },
{ "sub", clientId }
};
var securityTokenDescriptor = new SecurityTokenDescriptor
{
Claims = claims,
SigningCredentials = new X509SigningCredentials(certificate)
};
var handler = new JsonWebTokenHandler();
var signedClientAssertion = handler.CreateToken(securityTokenDescriptor);
}
Alternatively, if you don't wish to use Microsoft.IdentityModel.JsonWebTokens:
static string Base64UrlEncode(byte[] arg)
{
char Base64PadCharacter = '=';
char Base64Character62 = '+';
char Base64Character63 = '/';
char Base64UrlCharacter62 = '-';
char Base64UrlCharacter63 = '_';
string s = Convert.ToBase64String(arg);
s = s.Split(Base64PadCharacter)[0]; // RemoveAccount any trailing padding
s = s.Replace(Base64Character62, Base64UrlCharacter62); // 62nd char of encoding
s = s.Replace(Base64Character63, Base64UrlCharacter63); // 63rd char of encoding
return s;
}
static string GetSignedClientAssertion(X509Certificate2 certificate, string tenantId, string clientId)
{
// Get the RSA with the private key, used for signing.
var rsa = certificate.GetRSAPrivateKey();
//alg represents the desired signing algorithm, which is SHA-256 in this case
//x5t represents the certificate thumbprint base64 url encoded
var header = new Dictionary<string, string>()
{
{ "alg", "PS256"},
{ "typ", "JWT" },
{ "x5t#S256", Base64UrlHelpers.Encode(certificate.GetCertHash(HashAlgorithmName.SHA256))},
};
//Please see the previous code snippet on how to craft claims for the GetClaims() method
var claims = GetClaims(tenantId, clientId);
var headerBytes = JsonSerializer.SerializeToUtf8Bytes(header);
var claimsBytes = JsonSerializer.SerializeToUtf8Bytes(claims);
string token = Base64UrlEncode(headerBytes) + "." + Base64UrlEncode(claimsBytes);
string signature = Base64UrlEncode(rsa.SignData(Encoding.UTF8.GetBytes(token), HashAlgorithmName.SHA256, RSASignaturePadding.Pss));
string signedClientAssertion = string.Concat(token, ".", signature);
return signedClientAssertion;
}
WithClientClaims
In some cases, developers want to inject some claims into the assertions, but would still like MSAL to handle the creation of the assertion and the signing.
WithClientClaims(X509Certificate2 certificate, IDictionary<string, string> claimsToSign, bool mergeWithDefaultClaims = true)
produces a signed assertion containing the claims expected by Microsoft Entra ID plus additional client claims that you want to send.
string ipAddress = "192.168.1.2";
X509Certificate2 certificate = ReadCertificate(config.CertificateName);
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithAuthority(new Uri(config.Authority))
.WithClientClaims(certificate,
new Dictionary<string, string> { { "client_ip", ipAddress } })
.Build();
If one of the claims in the dictionary that you pass in is the same as one of the mandatory claims, the additional claim's value is taken into account. It overrides the claims computed by MSAL.NET.
If you want to provide your own claims, including the mandatory claims expected by Microsoft Entra ID, pass in false
for the mergeWithDefaultClaims
parameter.