AzureAD / microsoft-authentication-library-for-java

Microsoft Authentication Library (MSAL) for Java http://aka.ms/aadv2
MIT License
282 stars 137 forks source link

Documentation on how to incorporate MSAL4J with KeyVault for client credentials? #767

Open dylanmccurry-msft opened 6 months ago

dylanmccurry-msft commented 6 months ago

MSAL client type

Confidential

Problem Statement

I have integrated KeyVault with my application, and am able to obtain a KeyVaultCertificateWithPolicy. The corresponding object has a method for getCer() which returns a byte array representing the DER format of the certificate.

It's not clear to me how I can use this KeyVaultCertificateWithPolicy object to create an instance of a ClientCertificate credential using the ClientCredentialFactory.createFromCertificate() method.

How do I build an instance of a ConfidentialClientApplication using a certificate from KeyVault?

Proposed solution

Please document required steps for instantiating a confidential client application with a certificate downloaded from keyvault.

Alternatives

n/a

dylanmccurry-msft commented 6 months ago

Here are some code snippets I've tried, but no matter what I do- I cannot seem to get an IClientCertificate generated.

String certName = "First-Party-Client-Authentication";

KeyVaultCertificateWithPolicy cert = keyVaultService.getCertificate(certName);
KeyVaultKey key = keyVaultService.getKey(certName);
KeyVaultSecret secret = keyVaultService.getSecret(certName);

// Get the x509 certificate
InputStream certStream = new ByteArrayInputStream(cert.getCer());
X509Certificate x509Certificate = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certStream);

// create with an empty password and the cert stream. 
// Throws an IO exception: "Short read of DER length"
final IClientCertificate credential0 = ClientCredentialFactory.createFromCertificate(certStream, "");

// create with an empty password and the x509 encoded stream. 
// Throws an IO exception: "DER input, Integer tag error"
final IClientCertificate credential1 = ClientCredentialFactory.createFromCertificate(new ByteArrayInputStream(x509Certificate.getEncoded()), "");

// create with the secret stored under the same name as the certificate. Not sure what this is?
// Throws an IO Exception: "Short read of DER length"
final IClientCertificate credential2 = ClientCredentialFactory.createFromCertificate(certStream, secret.getValue());

KeyType type = key.getKeyType(); // returns RSA
JsonWebKey webKey = key.getKey();

// This throws a Null Reference Exception when set to true
KeyPair rsaKeyPair = webKey.toRsa(false);

// else if I set it to false, then this throws an exception when you call getPrivate()
// "PrivateKey is null or empty"
final IClientCertificate credential3 = ClientCredentialFactory.createFromCertificate(rsaKeyPair.getPrivate(), x509Certificate);

// If I omit the KeyPair, this throws a null reference
// "PrivateKey is null or empty"
final IClientCertificate credential4 = ClientCredentialFactory.createFromCertificate(null, x509Certificate);

// How do I generate the credential based on the certificate from keyvault?
confClientInstance = ConfidentialClientApplication.builder(Config.CLIENT_ID, credential).authority(Config.AUTHORITY).build();
dylanmccurry-msft commented 6 months ago

Here's a code snippet that works:

String certName = "My-Cert-Name";

KeyVaultSecret secret = keyVaultService.getSecret(certName);
byte[] pemBytes = Base64.getDecoder().decode(secret.getValue());

IClientCertificate credential = ClientCredentialFactory.createFromCertificate(new ByteArrayInputStream(pemBytes), "");

confClientInstance = ConfidentialClientApplication.builder(Config.CLIENT_ID, credential).authority(Config.AUTHORITY).build();

Note that in this case: 1) this cert is being issued by keyvault 2) the cert is being auto rotated and is generated by the OneCert certificate authority 3) we're not using the certificate client and instead are using the Secret Client. I have created my own KeyVaultService and implemented my own getSecret() function. 4) Even though the secret doesn't appear in the Azure Portal UI, the secret exists and has the same name as the certificate. I am not sure if this is KeyVault specific behavior or if the OneCert authority is doing this. I suspect it's KV default behavior.

I have to admit it took an embarrassingly long amount of time to figure out these ~5 lines of code. It would be great to document this integration when talking about certificate based client secrets.

Avery-Dunn commented 6 months ago

Hello @dylanmccurry-msft : Glad to hear you got it working, was just in the middle of typing a response. We do a similar thing for automated tests in our internal CertificateHelper class, where we get the certificate secret and use it to create the credential. The fact that a secret is created with the same name is scattered in various Microsoft docs, like this one, but isn't intuitive and is easy to miss:

When a Key Vault certificate is created, an addressable key and secret are also created with the same name. The Key Vault key allows key operations, and the Key Vault secret allows retrieval of the certificate value as a secret.

We don't have a user-friendly sample showing best practices for using the Azure Key Vault with MSAL, but it's been on our backlog for a while. Unless you have any other issues or questions, we can just treat this thread as a request for a better sample/docs covering this scenario.

dylanmccurry-msft commented 6 months ago

That sounds great to me, thanks for following up!