Skip to content

Commit 538e1a5

Browse files
committed
Add new default method to IClientCertificate to handle SHA256
1 parent bbdc93d commit 538e1a5

File tree

6 files changed

+106
-23
lines changed

6 files changed

+106
-23
lines changed

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCertificate.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,15 @@ final class ClientCertificate implements IClientCertificate {
4646
this.publicKeyCertificateChain = publicKeyCertificateChain;
4747
}
4848

49-
public String publicCertificateHash()
49+
@Override
50+
public String publicCertificateHash256()
5051
throws CertificateEncodingException, NoSuchAlgorithmException {
5152

5253
return Base64.getEncoder().encodeToString(ClientCertificate
5354
.getHashSha256(publicKeyCertificateChain.get(0).getEncoded()));
5455
}
5556

56-
public String publicCertificateHashSha1()
57+
public String publicCertificateHash()
5758
throws CertificateEncodingException, NoSuchAlgorithmException {
5859

5960
return Base64.getEncoder().encodeToString(ClientCertificate

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@ private void initClientAuthentication(IClientCredential clientCredential) {
102102
this.clientCertAuthentication = true;
103103
this.clientCertificate = (ClientCertificate) clientCredential;
104104
if (Authority.detectAuthorityType(this.authenticationAuthority.canonicalAuthorityUrl()) == AuthorityType.ADFS) {
105-
clientAuthentication = buildValidClientCertificateAuthoritySha1();
105+
//When this was added, ADFS did not support SHA256 hashes for client certificates
106+
clientAuthentication = buildValidClientCertificateAuthority(true);
106107
} else {
107-
clientAuthentication = buildValidClientCertificateAuthority();
108+
clientAuthentication = buildValidClientCertificateAuthority(false);
108109
}
109110
} else if (clientCredential instanceof ClientAssertion) {
110111
clientAuthentication = createClientAuthFromClientAssertion((ClientAssertion) clientCredential);
@@ -119,32 +120,25 @@ protected ClientAuthentication clientAuthentication() {
119120
final Date currentDateTime = new Date(System.currentTimeMillis());
120121
final Date expirationTime = ((PrivateKeyJWT) clientAuthentication).getJWTAuthenticationClaimsSet().getExpirationTime();
121122
if (expirationTime.before(currentDateTime)) {
122-
//The asserted private jwt with the client certificate can expire so rebuild it when the
123-
clientAuthentication = buildValidClientCertificateAuthority();
123+
if (Authority.detectAuthorityType(this.authenticationAuthority.canonicalAuthorityUrl()) == AuthorityType.ADFS) {
124+
clientAuthentication = buildValidClientCertificateAuthority(true);
125+
} else {
126+
clientAuthentication = buildValidClientCertificateAuthority(false);
127+
}
124128
}
125129
}
126130
return clientAuthentication;
127131
}
128132

129-
private ClientAuthentication buildValidClientCertificateAuthority() {
130-
ClientAssertion clientAssertion = JwtHelper.buildJwt(
131-
clientId(),
132-
clientCertificate,
133-
this.authenticationAuthority.selfSignedJwtAudience(),
134-
sendX5c,
135-
false);
136-
return createClientAuthFromClientAssertion(clientAssertion);
137-
}
138-
139133
//The library originally used SHA-1 for thumbprints as other algorithms were not supported server-side,
140134
// and while support for SHA-256 has been added certain flows still only allow SHA-1
141-
private ClientAuthentication buildValidClientCertificateAuthoritySha1() {
135+
private ClientAuthentication buildValidClientCertificateAuthority(boolean useSha1) {
142136
ClientAssertion clientAssertion = JwtHelper.buildJwt(
143137
clientId(),
144138
clientCertificate,
145139
this.authenticationAuthority.selfSignedJwtAudience(),
146140
sendX5c,
147-
true);
141+
useSha1);
148142
return createClientAuthFromClientAssertion(clientAssertion);
149143
}
150144

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IClientCertificate.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,20 @@ public interface IClientCertificate extends IClientCredential {
2323
PrivateKey privateKey();
2424

2525
/**
26-
* Base64 encoded hash of the the public certificate.
26+
* Base64 encoded SHA-256 hash of the public certificate.
27+
*
28+
* @return base64 encoded string
29+
* @throws CertificateEncodingException if an encoding error occurs
30+
* @throws NoSuchAlgorithmException if requested algorithm is not available in the environment
31+
*/
32+
default String publicCertificateHash256() throws CertificateEncodingException, NoSuchAlgorithmException {
33+
//Default implementation that returns null, to add backwards compatibility for those implementing this public interface.
34+
//If left as null, the library will default to the older publicCertificateHash() method and SHA-1 hashing.
35+
return null;
36+
}
37+
38+
/**
39+
* Base64 encoded SHA-1 hash of the public certificate.
2740
*
2841
* @return base64 encoded string
2942
* @throws CertificateEncodingException if an encoding error occurs

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@ static ClientAssertion buildJwt(String clientId, final ClientCertificate credent
5656
builder.x509CertChain(certs);
5757
}
5858

59-
//SHA-256 is preferred, however certain flows still require SHA-1 due to what is supported server-side
60-
if (useSha1) {
61-
builder.x509CertThumbprint(new Base64URL(credential.publicCertificateHashSha1()));
59+
//SHA-256 is preferred, however certain flows still require SHA-1 due to what is supported server-side. If SHA-256
60+
// is not supported or the IClientCredential.publicCertificateHash256() method is not implemented, the library will default to SHA-1.
61+
if (useSha1 || credential.publicCertificateHash256() == null) {
62+
builder.x509CertThumbprint(new Base64URL(credential.publicCertificateHash()));
6263
} else {
63-
builder.x509CertSHA256Thumbprint(new Base64URL(credential.publicCertificateHash()));
64+
builder.x509CertSHA256Thumbprint(new Base64URL(credential.publicCertificateHash256()));
6465
}
6566

6667
jwt = new SignedJWT(builder.build(), claimsSet);

msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@
88

99
import static org.junit.jupiter.api.Assertions.assertEquals;
1010
import static org.junit.jupiter.api.Assertions.assertNotNull;
11+
import static org.junit.jupiter.api.Assertions.assertNull;
1112
import static org.junit.jupiter.api.Assertions.assertThrows;
1213
import static org.mockito.Mockito.doReturn;
1314
import static org.mockito.Mockito.mock;
1415

1516
import java.math.BigInteger;
17+
import java.security.NoSuchAlgorithmException;
1618
import java.security.PrivateKey;
19+
import java.security.cert.CertificateException;
1720
import java.security.interfaces.RSAPrivateKey;
21+
import java.util.Collections;
22+
import java.util.List;
1823

1924
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
2025
class ClientCertificateTest {
@@ -36,4 +41,35 @@ void testGetClient() {
3641
final ClientCertificate kc = ClientCertificate.create(key, null);
3742
assertNotNull(kc);
3843
}
44+
45+
@Test
46+
void testIClientCertificateInterface_Sha256AndSha1() throws NoSuchAlgorithmException, CertificateException {
47+
//See https://github.com/AzureAD/microsoft-authentication-library-for-java/issues/863 for context on this test.
48+
//Essentially, it aims to test compatibility for customers that implemented IClientCertificate in older versions of the library.
49+
50+
//IClientCertificate.publicCertificateHash256() returns null by default if not implemented...
51+
IClientCertificate certificate = new TestClientCredential();
52+
assertNull(certificate.publicCertificateHash256());
53+
54+
//... but ClientCredentialFactory has an implemented version, so it should not be null.
55+
certificate = ClientCredentialFactory.createFromCertificate(TestHelper.getPrivateKey(), TestHelper.getX509Cert());
56+
assertNotNull(certificate.publicCertificateHash256());
57+
}
58+
59+
class TestClientCredential implements IClientCertificate {
60+
@Override
61+
public PrivateKey privateKey() {
62+
return null;
63+
}
64+
65+
@Override
66+
public String publicCertificateHash() {
67+
return "";
68+
}
69+
70+
@Override
71+
public List<String> getEncodedPublicKeyCertificateChain() {
72+
return Collections.emptyList();
73+
}
74+
}
3975
}

msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@
77
import com.nimbusds.jose.crypto.RSASSASigner;
88
import com.nimbusds.jose.jwk.RSAKey;
99
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
10+
import sun.security.tools.keytool.CertAndKeyGen;
11+
import sun.security.x509.X500Name;
1012

1113
import java.io.File;
1214
import java.io.FileWriter;
1315
import java.io.IOException;
1416
import java.net.URISyntaxException;
1517
import java.nio.file.Files;
1618
import java.nio.file.Paths;
19+
import java.security.*;
20+
import java.security.cert.CertificateException;
21+
import java.security.cert.X509Certificate;
1722
import java.util.Collections;
1823
import java.util.HashMap;
1924
import java.util.List;
@@ -29,6 +34,9 @@ class TestHelper {
2934
"\"expires_on\": %d ,\"expires_in\": %d," +
3035
"\"token_type\":\"Bearer\"}";
3136

37+
static X509Certificate x509Cert = getX509Cert();
38+
static PrivateKey privateKey = getPrivateKey();
39+
3240
static String readResource(Class<?> classInstance, String resource) {
3341
try {
3442
return new String(Files.readAllBytes(Paths.get(classInstance.getResource(resource).toURI())));
@@ -97,4 +105,34 @@ static HttpResponse expectedResponse(int statusCode, String response) {
97105

98106
return httpResponse;
99107
}
108+
109+
static void setPrivateKeyAndCert() {
110+
try {
111+
CertAndKeyGen certGen = new CertAndKeyGen("RSA", "SHA256WithRSA", null);
112+
certGen.generate(2048);
113+
X509Certificate cert = certGen.getSelfCertificate(
114+
new X500Name(""), 3600);
115+
116+
x509Cert = cert;
117+
privateKey = certGen.getPrivateKey();
118+
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException | IOException | CertificateException | SignatureException e) {
119+
throw new RuntimeException(e);
120+
}
121+
}
122+
123+
static X509Certificate getX509Cert() {
124+
if (x509Cert == null) {
125+
setPrivateKeyAndCert();
126+
}
127+
128+
return x509Cert;
129+
}
130+
131+
static PrivateKey getPrivateKey() {
132+
if (privateKey == null) {
133+
setPrivateKeyAndCert();
134+
}
135+
136+
return privateKey;
137+
}
100138
}

0 commit comments

Comments
 (0)