Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.azure.identity.implementation.util.LoggingUtil;
import reactor.core.publisher.Mono;

import java.io.InputStream;
import java.util.Objects;

/**
Expand All @@ -35,16 +36,19 @@ public class ClientCertificateCredential implements TokenCredential {
* @param tenantId the tenant ID of the application
* @param clientId the client ID of the application
* @param certificatePath the PEM file or PFX file containing the certificate
* @param certificate the PEM or PFX certificate
* @param certificatePassword the password protecting the PFX file
* @param identityClientOptions the options to configure the identity client
*/
ClientCertificateCredential(String tenantId, String clientId, String certificatePath, String certificatePassword,
IdentityClientOptions identityClientOptions) {
Objects.requireNonNull(certificatePath, "'certificatePath' cannot be null.");
ClientCertificateCredential(String tenantId, String clientId, String certificatePath, InputStream certificate,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have both the path to the file and the InputStream? If the inputstream is given, then we don't need the path right? Maybe have separate overloads - one with input stream and one with file path.

String certificatePassword, IdentityClientOptions identityClientOptions) {
Objects.requireNonNull(certificatePath == null ? certificate : certificatePath,
"'certificate' and 'certificatePath' cannot both be null.");
identityClient = new IdentityClientBuilder()
.tenantId(tenantId)
.clientId(clientId)
.certificatePath(certificatePath)
.certificate(certificate)
.certificatePassword(certificatePassword)
.identityClientOptions(identityClientOptions)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

package com.azure.identity;

import com.azure.core.util.logging.ClientLogger;
import com.azure.identity.implementation.util.ValidationUtil;

import java.io.InputStream;
import java.util.HashMap;

/**
Expand All @@ -13,31 +15,59 @@
* @see ClientCertificateCredential
*/
public class ClientCertificateCredentialBuilder extends AadCredentialBuilderBase<ClientCertificateCredentialBuilder> {
private String clientCertificate;
private String clientCertificatePath;
private InputStream clientCertificate;
private String clientCertificatePassword;
private final ClientLogger logger = new ClientLogger(ClientCertificateCredentialBuilder.class);

/**
* Sets the client certificate for authenticating to AAD.
* Sets the path of the PEM certificate for authenticating to AAD.
*
* @param certificatePath the PEM file containing the certificate
* @return An updated instance of this builder.
*/
public ClientCertificateCredentialBuilder pemCertificate(String certificatePath) {
ValidationUtil.validateFilePath(getClass().getSimpleName(), certificatePath, "Pem Certificate Path");
this.clientCertificate = certificatePath;
this.clientCertificatePath = certificatePath;
return this;
}

/**
* Sets the client certificate for authenticating to AAD.
* Sets the input stream holding the PEM certificate for authenticating to AAD.
*
* @param certificate the input stream containing the PEM certificate
* @return An updated instance of this builder.
*/
public ClientCertificateCredentialBuilder pemCertificate(InputStream certificate) {
this.clientCertificate = certificate;
return this;
}

/**
* Sets the path and password of the PFX certificate for authenticating to AAD.
*
* @param certificatePath the password protected PFX file containing the certificate
* @param clientCertificatePassword the password protecting the PFX file
* @return An updated instance of this builder.
*/
public ClientCertificateCredentialBuilder pfxCertificate(String certificatePath, String clientCertificatePassword) {
public ClientCertificateCredentialBuilder pfxCertificate(String certificatePath,
String clientCertificatePassword) {
Copy link

@schaabs schaabs Oct 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like we're requiring a password for PFX and not supporting a password for PEM. If we support password protected certificates, I think we should support them by adding a separate property such as Password or CertificatePassword

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per discussion offline - this will be addressed in a future PR. Issue link coming soon

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussing this offline, this is something we should address in a subsequent release.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: #15955

ValidationUtil.validateFilePath(getClass().getSimpleName(), certificatePath, "Pfx Certificate Path");
this.clientCertificate = certificatePath;
this.clientCertificatePath = certificatePath;
this.clientCertificatePassword = clientCertificatePassword;
return this;
}

/**
* Sets the input stream holding the PFX certificate and its password for authenticating to AAD.
*
* @param certificate the input stream containing the password protected PFX certificate
* @param clientCertificatePassword the password protecting the PFX file
* @return An updated instance of this builder.
*/
public ClientCertificateCredentialBuilder pfxCertificate(InputStream certificate,
String clientCertificatePassword) {
this.clientCertificate = certificate;
this.clientCertificatePassword = clientCertificatePassword;
return this;
}
Expand Down Expand Up @@ -86,9 +116,14 @@ public ClientCertificateCredential build() {
ValidationUtil.validate(getClass().getSimpleName(), new HashMap<String, Object>() {{
put("clientId", clientId);
put("tenantId", tenantId);
put("clientCertificate", clientCertificate);
put("clientCertificate", clientCertificate == null ? clientCertificatePath : clientCertificate);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message from Validation Util, won't be clear for user.
I think, we should custom handle this with the error message "A certificate source as input stream or file path should be configured on the builder."

We should also throw an exception, if both path and inputstream are configured.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the error message will break the current certificate path scenario - people may be depending on the error message.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, then we should atleast add an additive check for the scenario and throw an exception if both path and inputstream are configured.

}});
return new ClientCertificateCredential(tenantId, clientId, clientCertificate, clientCertificatePassword,
identityClientOptions);
if (clientCertificate != null && clientCertificatePath != null) {
throw logger.logExceptionAsWarning(new IllegalArgumentException("Both certificate input stream and "
+ "certificate path are provided in ClientCertificateCredentialBuilder. Only one of them should "
+ "be provided."));
}
Comment on lines +121 to +125
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are doing this, then maybe the ClientCertificateCredential constructor should only have the input stream arg. If the path is given, the builder can wrap the path with FileInputStream and create the credential instance. This saves multiple if-else checks in ClientCertificateCredential.

return new ClientCertificateCredential(tenantId, clientId, clientCertificatePath, clientCertificate,
clientCertificatePassword, identityClientOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ public class EnvironmentCredential implements TokenCredential {
} else if (verifyNotNull(certPath)) {
// 1.2 Attempt ClientCertificateCredential
logger.info("Azure Identity => EnvironmentCredential invoking ClientCertificateCredential");
targetCredential = new ClientCertificateCredential(tenantId, clientId, certPath,
null, identityClientOptions);
ValidationUtil.validateFilePath(getClass().getSimpleName(), certPath, "Pem Certificate Path");
targetCredential = new ClientCertificateCredential(tenantId, clientId, certPath, null, null,
identityClientOptions);
} else {
// 1.3 Log error if neither is found
logger.error("Azure Identity => ERROR in EnvironmentCredential: Failed to create a "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@
import reactor.core.publisher.Mono;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
Expand Down Expand Up @@ -122,6 +124,7 @@ public class IdentityClient {
private final String tenantId;
private final String clientId;
private final String clientSecret;
private final InputStream certificate;
private final String certificatePath;
private final String certificatePassword;
private HttpPipelineAdapter httpPipelineAdapter;
Expand All @@ -135,13 +138,14 @@ public class IdentityClient {
* @param clientId the client ID of the application.
* @param clientSecret the client secret of the application.
* @param certificatePath the path to the PKCS12 or PEM certificate of the application.
* @param certificate the PKCS12 or PEM certificate of the application.
* @param certificatePassword the password protecting the PFX certificate.
* @param isSharedTokenCacheCredential Indicate whether the credential is
* {@link com.azure.identity.SharedTokenCacheCredential} or not.
* @param options the options configuring the client.
*/
IdentityClient(String tenantId, String clientId, String clientSecret,
String certificatePath, String certificatePassword, boolean isSharedTokenCacheCredential,
IdentityClient(String tenantId, String clientId, String clientSecret, String certificatePath,
InputStream certificate, String certificatePassword, boolean isSharedTokenCacheCredential,
IdentityClientOptions options) {
if (tenantId == null) {
tenantId = "organizations";
Expand All @@ -153,6 +157,7 @@ public class IdentityClient {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.certificatePath = certificatePath;
this.certificate = certificate;
this.certificatePassword = certificatePassword;
this.options = options;

Expand All @@ -172,10 +177,10 @@ private ConfidentialClientApplication getConfidentialClientApplication() {
IClientCredential credential;
if (clientSecret != null) {
credential = ClientCredentialFactory.createFromSecret(clientSecret);
} else if (certificatePath != null) {
} else if (certificate != null || certificatePath != null) {
try {
if (certificatePassword == null) {
byte[] pemCertificateBytes = Files.readAllBytes(Paths.get(certificatePath));
byte[] pemCertificateBytes = getCertificateBytes();

List<X509Certificate> x509CertificateList = CertificateUtil.publicKeyFromPem(pemCertificateBytes);
PrivateKey privateKey = CertificateUtil.privateKeyFromPem(pemCertificateBytes);
Expand All @@ -187,8 +192,9 @@ private ConfidentialClientApplication getConfidentialClientApplication() {
privateKey, x509CertificateList);
}
} else {
InputStream pfxCertificateStream = getCertificateInputStream();
credential = ClientCredentialFactory.createFromCertificate(
new FileInputStream(certificatePath), certificatePassword);
pfxCertificateStream, certificatePassword);
}
} catch (IOException | GeneralSecurityException e) {
throw logger.logExceptionAsError(new RuntimeException(
Expand Down Expand Up @@ -1038,4 +1044,31 @@ public String getClientId() {
private boolean isADFSTenant() {
return this.tenantId.equals(ADFS_TENANT);
}

private byte[] getCertificateBytes() throws IOException {
if (certificatePath != null) {
return Files.readAllBytes(Paths.get(certificatePath));
} else if (certificate != null) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int read = certificate.read(buffer, 0, buffer.length);
while (read != -1) {
outputStream.write(buffer, 0, read);
read = certificate.read(buffer, 0, buffer.length);
}
return outputStream.toByteArray();
} else {
return new byte[0];
}
}

private InputStream getCertificateInputStream() throws IOException {
if (certificatePath != null) {
return new FileInputStream(certificatePath);
} else if (certificate != null) {
return certificate;
} else {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import com.azure.identity.SharedTokenCacheCredential;

import java.io.InputStream;

/**
* Fluent client builder for instantiating an {@link IdentityClient}.
*
Expand All @@ -16,6 +18,7 @@ public final class IdentityClientBuilder {
private String clientId;
private String clientSecret;
private String certificatePath;
private InputStream certificate;
private String certificatePassword;
private boolean sharedTokenCacheCred;

Expand Down Expand Up @@ -60,6 +63,17 @@ public IdentityClientBuilder certificatePath(String certificatePath) {
return this;
}

/**
* Sets the client certificate for the client.
*
* @param certificate the PEM/PFX certificate
* @return the IdentityClientBuilder itself
*/
public IdentityClientBuilder certificate(InputStream certificate) {
this.certificate = certificate;
return this;
}

/**
* Sets the client certificate for the client.
*
Expand Down Expand Up @@ -96,7 +110,7 @@ public IdentityClientBuilder sharedTokenCacheCredential(boolean isSharedTokenCac
* @return a {@link IdentityClient} with the current configurations.
*/
public IdentityClient build() {
return new IdentityClient(tenantId, clientId, clientSecret, certificatePath,
return new IdentityClient(tenantId, clientId, clientSecret, certificatePath, certificate,
certificatePassword, sharedTokenCacheCred, identityClientOptions);
}
}
Loading