Coding Stephan

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:

  1. Get the certificate from the Key Vault
  2. Create an unsigned JWT (header and payload)
  3. Sign the JWT with the certificate
  4. 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.

ClaimDescription
audThe audience of the token. This is the token endpoint of the identity provider.
issThe issuer of the token. This is your client ID.
subThe subject of the token. This is also your client ID.
jtiThe unique identifier of the token. Just generate a guid an it works
nbfThe not before time of the token. In seconds since 1970-01-01
expThe 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.