microsoftgraph / msgraph-sdk-java

Microsoft Graph SDK for Java
https://docs.microsoft.com/en-us/graph/sdks/sdks-overview
MIT License
402 stars 134 forks source link

Authenticate with application id using Certificated-based Authentication #2075

Open 0B7002 opened 4 months ago

0B7002 commented 4 months ago

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?

0B7002 commented 3 months ago

I tried the following but all failed.

please help.

0B7002 commented 3 months ago

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

Ndiritu commented 3 months ago

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

0B7002 commented 3 months ago

When can I get a reply?

0B7002 commented 3 months ago

Is anyone working on this issue?

Ndiritu commented 3 months ago

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.

0B7002 commented 3 months ago

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.

0B7002 commented 2 months ago

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?

0B7002 commented 2 months ago

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?

0B7002 commented 2 months ago

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?