Authentication using certificate - Entra ID
If you want to do authentication for your multi-tenant application, you can use a secret (which you shouldn’t!) or a certificate. In almost all the samples I’ve seen, the certificate is stored in the Key Vault “to keep it safe”. But how does the authentication work? And are you sure this is the most secure way?
Process
The short version of the process is:
- Get the certificate from the Key Vault
- Create an unsigned JWT (header and payload)
- Sign the JWT with the certificate
- Ask Entra ID for a token
Unsigned JWT
The unsigned JWT is a JSON object with two parts: the header and the payload. The header contains the algorithm used to sign the JWT, and the payload contains the claims.
JWT Header
{
"alg": "RS256",
"typ": "JWT"
"x5t": "...."
}
Specify the algorithm used to sign the JWT, in this case, RS256
and we specify that this is in fact a JWT
.
The x5t
is the thumbprint of the certificate, but it’s not the regular thumbprint!
It’s the base64 url encoded SHA256 hash of the certificate 🤯, with some replacements.
// Load the certificate, just the public part is needed!!
// Either use a crt file path or a byte array with the raw data.
var cert = new X509Certificate2({path-to-crt-file}) // new X509Certificate2({byte[]})
var xt5 = Base64UrlEncode(cert.GetCertHash());
JWT Payload
{
"aud": "https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token",
"iss": "{your-client-id}",
"sub": "{your-client-id}",
"jti": "7430eac2-d22d-42ee-9d50-289ad11beb6e",
"nbf": 1622872800,
"exp": 1622876400
}
This is just a regular JWT payload, but instead of the audience being the API you want to access, it’s the token endpoint of the identity provider. The iss
and sub
are your client ID, and the jti
is a unique identifier for the client assertion (use New-Guid
and be done with it). The nbf
and exp
are the not before and expiration times.
Claim | Description |
---|---|
aud | The audience of the token. This is the token endpoint of the identity provider. |
iss | The issuer of the token. This is your client ID. |
sub | The subject of the token. This is also your client ID. |
jti | The unique identifier of the token. Just generate a guid an it works |
nbf | The not before time of the token. In seconds since 1970-01-01 |
exp | The expiration time of the token. In seconds since 1970-01-01 |
Encoding
Both the header and the payload are converted to an UTF8 string and then base64 url encoded. The result is then concatenated with a .
between them. Using the regular base64 encoding might result in it not being useable in an url, so you have to do some additional encoding.
Code to do this in c# can be found here.
public static string Base64UrlEncode(byte[] input)
{
char Base64PadCharacter = '=';
char Base64Character62 = '+';
char Base64Character63 = '/';
char Base64UrlCharacter62 = '-';
char Base64UrlCharacter63 = '_';
string s = Convert.ToBase64String(input);
s = s.Split(Base64PadCharacter)[0]; // Remove any trailing padding
s = s.Replace(Base64Character62, Base64UrlCharacter62); // 62nd char of encoding
s = s.Replace(Base64Character63, Base64UrlCharacter63); // 63rd char of encoding
return s;
}
public static string GetUnsignedToken(IDictionary<string, object> header, IDictionary<string, object> payload)
{
var headerBytes = JsonSerializer.SerializeToUtf8Bytes(header);
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(payload);
return Base64UrlEncode(headerBytes) + "." + Base64UrlEncode(payloadBytes);
}
Sign the JWT
Now that you have the unsigned JWT, you’ll need to sign it with the certificate, encoding the signature and appending it to the JWT prefixing it with a .
.
// The unsigned token from previous step
var unsignedToken = ...;
// Load the certificate, to be able to sign something you'll need the private key.
// load it in a way you seem fit.
var certWithPrivateKey = new X509Certificate2(...);
using var rsa = certWithPrivateKey.GetRSAPrivateKey();
string signature = Base64UrlEncode(rsa.SignData(Encoding.UTF8.GetBytes(unsignedToken), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
var signedToken = unsignedToken + "." + signature;
Get the token
Once you have this signed JWT, you use it in the client credentials flow, as the client_assertion
in the request.
POST /{tenantId}/oauth2/v2.0/token HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id={your-client-id}
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={signed-token}
&scope=https://graph.microsoft.com/.default
Conclusion
As shown in this sample code, you do not really need the private key, you just need a method to sign some bytes and get the signature back. In all the samples I’ve found people use a managed identity to download the certificate from the Key Vault and then use it to sign the JWT. This means the private key is in the application at that time and might be extracted or leaked. Which is a security risk you should not be willing to take.
As explained you just need the signature, in fact you can ask Key Vault to sign the JWT, which should be supported in all those libraries talking to various Microsoft services. This way the private key is never in your application and you can be sure it’s secure. But more on that next time.