msgraph-sdk-java icon indicating copy to clipboard operation
msgraph-sdk-java copied to clipboard

Authenticate with application id using Certificated-based Authentication

Open ghost opened this issue 1 year ago • 10 comments

I registered the app on the Microsoft Entra admin center by referring to this page:

https://learn.microsoft.com/en-us/graph/auth-v2-service?tabs=http

Then I created public/private key using openssl. Here is the commands.

$ openssl genrsa -out ms365-private.key 2048
$ openssl req -new -x509 -key ms365-private.key -out ms365-public.crt -days 365000

Uploaded the public key (ms365-private.key) on the Microsoft Entra admin center, and I created this code.

TokenCredential credential = new ClientCertificateCredentialBuilder().tenantId("xxxxxxxx")
		.clientId("xxxxxxxx")
		.pemCertificate("/xxxx/xxxxx/ms365-private.key")
		.build();
graphClient = new GraphServiceClient(credential);

but if i execute some methods on this graphClient , this following error occurs

[ERROR] com.azure.identity.implementation.util.CertificateUtil.performLogging - PEM certificate provided does not contain -----BEGIN CERTIFICATE-----END CERTIFICATE----- block 
java.lang.IllegalArgumentException: PEM certificate provided does not contain -----BEGIN CERTIFICATE-----END CERTIFICATE----- block
    at com.azure.identity.implementation.util.CertificateUtil.publicKeyFromPem(CertificateUtil.java:79)
    at com.azure.identity.implementation.IdentityClientBase.getConfidentialClient(IdentityClientBase.java:196)
    at com.azure.identity.implementation.IdentitySyncClient.lambda$new$2(IdentitySyncClient.java:91)
    at com.azure.identity.implementation.SynchronousAccessor.getValue(SynchronousAccessor.java:45)
    at com.azure.identity.implementation.IdentitySyncClient.authenticateWithConfidentialClientCache(IdentitySyncClient.java:171)
    at com.azure.identity.ClientCertificateCredential.getTokenSync(ClientCertificateCredential.java:150)
    at com.microsoft.kiota.authentication.AzureIdentityAccessTokenProvider.getAuthorizationToken(AzureIdentityAccessTokenProvider.java:146)
    at com.microsoft.kiota.authentication.BaseBearerTokenAuthenticationProvider.authenticateRequest(BaseBearerTokenAuthenticationProvider.java:46)

Q. Should I also specify public key? If so, how to specify public key by using ClientCertificateCredentialBuilder?

ghost avatar Jul 09 '24 04:07 ghost

I tried the following but all failed.

  • Change a path specify to pemCertificate() from the private key (ms365-private.key) to the public key (ms365-public.crt)
  • Change the format of the private key (ms365-private.key) to pem format (contains -----BEGIN CERTIFICATE-----END CERTIFICATE----- block)

please help.

ghost avatar Jul 29 '24 04:07 ghost

Is anyone working on this issue? I have to resolve this issue as soon as possible.

ghost avatar Aug 07 '24 01:08 ghost

@0B7002 I'll try to reproduce this and get back to you later today. Apologies for the delay

Ndiritu avatar Aug 07 '24 11:08 Ndiritu

When can I get a reply?

ghost avatar Aug 13 '24 00:08 ghost

Is anyone working on this issue?

ghost avatar Aug 19 '24 04:08 ghost

Sorry for the delay @0B7002.

From my understanding of certificate-based authentication & these docs what you should be uploading to your app registration is the X509 certificate generated.

From Azure Identity's logic it seems that we expect a certificate file containing a private key. The certs generated by OpenSSL only have the public key in the encoded payload.

Still investigating this.

Ndiritu avatar Aug 19 '24 13:08 Ndiritu

Thank you for your answer.

As mentioned earlier, I generated and uploaded a public key with the x509 option.

And I read docs but could not find how to generate pem file.

What kind of files should I upload to my app and how to generate it? what value should I specify to pemCertificat()? I have no idea, please help.

ghost avatar Aug 20 '24 04:08 ghost

From my understanding, this error has occured causing private key, not public key.

And as mentioned earlier, I created public/private key using openssl, Here is the commands.

$ openssl genrsa -out ms365-private.key 2048
$ openssl req -new -x509 -key ms365-private.key -out ms365-public.crt -days 365000

So I tried to get public key information from private key by a following command, and suceed.

$ openssl rsa -pubout < ms365-private.key
-----BEGIN PUBLIC KEY-----
XXXXX
-----END PUBLIC KEY-----
writing RSA key

Why pemCertificat() can not read this private key? How should I create a key?

ghost avatar Aug 23 '24 04:08 ghost

Using the same public/private key, I successed to get access token by the following code.

// private key
byte[] encoded = Base64.getDecoder().decode("[private key]");
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(encoded));

// public key
X509Certificate publicKey = (X509Certificate) CertificateFactory.getInstance("X.509")
		.generateCertificate(Files.newInputStream(Paths.get("[public key file path]")));

// parameter
String authority = "https://login.microsoftonline.com/[tenantid]/";
Set<String> scope = Set.of("https://graph.microsoft.com/.default");

// client
IClientCredential credential = ClientCredentialFactory.createFromCertificate(privateKey, publicKey);
ConfidentialClientApplication cca = ConfidentialClientApplication.builder("[clientid]", credential)
		.authority(authority)
		.build();
ClientCredentialParameters parameters = ClientCredentialParameters.builder(scope).build();

// get token
CompletableFuture<IAuthenticationResult> result = cca.acquireToken(parameters);
System.out.println(result.get().accessToken());

Because of this, I think that the public/private key is correct.

How to do this in using GraphServiceClient? And, do I need to have and specify the public key on the client side as well?

ghost avatar Sep 03 '24 02:09 ghost

Using a private key and thumbprint, I successed to authorize by the following code.

import java.security.KeyFactory;
import java.security.interfaces.RSAKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.OffsetDateTime;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import org.apache.commons.codec.binary.Hex;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.azure.core.credential.TokenCredential;
import com.azure.identity.ClientAssertionCredentialBuilder;
import com.microsoft.graph.serviceclient.GraphServiceClient;

public class MyService {

	public GraphServiceClient getGraphClient() throws Exception {
		// private key
		byte[] encoded = Base64.getDecoder().decode("[private key base64 text]");
		PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
		RSAKey privateKey = (RSAKey) KeyFactory.getInstance("RSA").generatePrivate(keySpec);

		// generate JWT
		byte[] bytes = Hex.decodeHex("thumbprint".toCharArray());
		String x5t = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
		Map<String, Object> header = new HashMap<>();
		header.put("x5t", x5t);
		String assertion = JWT.create()
				.withHeader(header)
				.withAudience("https://login.microsoftonline.com/" + "[tenantId]" + "/oauth2/v2.0/token")
				.withExpiresAt(OffsetDateTime.now().plusMinutes(5).toInstant())
				.withIssuer("[clientId]")
				.withJWTId(UUID.randomUUID().toString())
				.withNotBefore(OffsetDateTime.now().toInstant())
				.withSubject("[clientId]")
				.withIssuedAt(OffsetDateTime.now().toInstant())
				.sign(Algorithm.RSA256(privateKey));

		// generate GraphServiceClient
		TokenCredential credential = new ClientAssertionCredentialBuilder().tenantId("[tenantId]")
				.clientId("[clientId]")
				.clientAssertion(() -> assertion)
				.build();
		return new GraphServiceClient(credential);
	}
}

Is this a best practice?

ghost avatar Sep 12 '24 02:09 ghost