I am following this guide to the client credentials flow and this guide to the JWT token needed. The ultimate objective is to access calendar information through the Microsoft Graph API. Calls to the Graph API need to be authenticated with an access token which is obtained from the Microsoft token endpoint.
But calling the token endpoint itself requires some form of authentication, which can be either by shared secret or by a certificate-signed JWT token. The shared secret approach is well-documented and appears to work fine, except that the Graph service I am trying to call rejects the access token as not secure enough - it seems a certificate-signed JWT token is required. I have been unable to find any Java examples of this second approach, and what I have implemented so far following the above guides doesn't work.
Using JJWT, the code for generating the token looks like:
PrivateKey key = loadPrivateKey();
String jwt = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "RS256")
.setHeaderParam("x5t", "A7...89")
.setSubject(clientId)
.setExpiration(new Date(System.currentTimeMillis() + 200000))
.setIssuer(clientId)
.setNotBefore(new Date())
.setAudience("https://login.microsoftonline.com/" + tenantId + "/oauth2/token")
.setId(UUID.randomUUID().toString())
.signWith(
SignatureAlgorithm.RS256,
key)
.compact();
loadPrivateKey() uses BouncyCastle classes:
KeyFactory factory = KeyFactory.getInstance("RSA");
PemObject pemObject;
PemReader pemReader = new PemReader(new StringReader(pemFileContent));
try {
pemObject = pemReader.readPemObject();
} finally {
pemReader.close();
}
byte[] content = pemObject.getContent();
PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(content);
return factory.generatePrivate(privKeySpec);
I created a key pair in the apps.dev.microsoft.com console, downloaded the private key and converted it to PEM format using openssl, then pasted the PEM content into the pemFileContent variable used above. openssl calculates the same thumbprint for the PEM version as the MS application console.
I use the JWT token in a call to a Retrofit service:
default Call<APIToken> getAccessToken(String tenantId, String clientId, String clientAssertion)
{
return this.getAccessToken(
tenantId,
clientId,
clientAssertion,
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"https://graph.microsoft.com/.default",
"client_credentials");
}
@FormUrlEncoded
@Headers({
"Host: login.microsoftonline.com",
"Content-Type: application/x-www-form-urlencoded"
})
@POST("/{tenant_id}/oauth2/token")
Call<APIToken> getAccessToken(
@Path("tenant_id") String tenantId,
@Field("client_id") String clientId,
@Field("client_assertion") String clientAssertion,
@Field("client_assertion_type") String clientAssertionType,
@Field("scope") String scope,
@Field("grant_type") String grantType);
Everything looks good in the debugging output:
header={typ=JWT, alg=RS256, x5t=A7...89},body={sub=b0a0fd13-1e86-4ef3-a003-c53eaf21daa4, exp=1517656715, iss=b0a0fd13-1e86-4ef3-a003-c53eaf21daa4, nbf=1517656515, aud=https://login.microsoftonline.com/87ad3067-2703-49ea-8cd2-a094fc3ee413/oauth2/token, jti=4fa5db4f-76b7-4122-9c5a-092ac74fd0fa},signature=....342 characters....
But the response is 401 Unauthorized, with the error report:
{"error":"invalid_client","error_description":"AADSTS70002: Error validating credentials. AADSTS50012: Client assertion contains an invalid signature. [Reason - The key was not found., Thumbprint of key used by client: '03B....3D', Configured keys: [Key0:Start=02/03/2018, End=12/31/2099, Thumbprint=lQ...M4;]]\r\nTrace ID: b4775394-51cc-4a4a-b927-50bd05421100\r\nCorrelation ID: f9264339-9b85-4942-9404-af941aa0331c\r\nTimestamp: 2018-02-03 11:15:18Z","error_codes":[70002,50012],"timestamp":"2018-02-03 11:15:18Z","trace_id":"b4775394-51cc-4a4a-b927-50bd05421100","correlation_id":"f9264339-9b85-4942-9404-af941aa0331c"}
I am at a loss as to where to look for the problem. Neither of the thumbprint values mentioned in the error correspond to the thumbprint of the private key. Others have reported this error message when using the wrong signature algorithm, but RS256 is the documented algorithm to use. I'm looking for either a pointer to what I'm getting wrong, or a working example in Java (I'm not fussy about which libraries are used) of the client credentials flow using a signed JWT token (I have found examples using a shared secret, and they work OK, but the MS Graph API rejects the token as not being secure enough).