|
3 | 3 | import com.fasterxml.jackson.core.type.TypeReference; |
4 | 4 | import com.fasterxml.jackson.databind.ObjectMapper; |
5 | 5 | import io.camunda.operate.http.TypeReferenceHttpClientResponseHandler; |
6 | | -import java.net.URISyntaxException; |
| 6 | +import io.jsonwebtoken.Jwts; |
| 7 | +import java.io.FileInputStream; |
| 8 | +import java.security.KeyStore; |
| 9 | +import java.security.MessageDigest; |
| 10 | +import java.security.PrivateKey; |
| 11 | +import java.security.cert.X509Certificate; |
| 12 | +import java.time.Instant; |
7 | 13 | import java.time.LocalDateTime; |
| 14 | +import java.time.temporal.ChronoUnit; |
8 | 15 | import java.util.ArrayList; |
| 16 | +import java.util.Base64; |
| 17 | +import java.util.Date; |
9 | 18 | import java.util.List; |
10 | 19 | import java.util.Map; |
| 20 | +import java.util.UUID; |
11 | 21 | import org.apache.hc.client5.http.classic.methods.HttpPost; |
12 | 22 | import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; |
13 | 23 | import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; |
@@ -63,18 +73,91 @@ private TokenResponse retrieveToken() { |
63 | 73 | } |
64 | 74 | } |
65 | 75 |
|
66 | | - private HttpPost buildRequest() throws URISyntaxException { |
| 76 | + private HttpPost buildRequest() throws Exception { |
67 | 77 | HttpPost httpPost = new HttpPost(jwtCredential.authUrl().toURI()); |
68 | 78 | httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); |
69 | 79 | List<NameValuePair> formParams = new ArrayList<>(); |
70 | 80 | formParams.add(new BasicNameValuePair("grant_type", "client_credentials")); |
71 | 81 | formParams.add(new BasicNameValuePair("client_id", jwtCredential.clientId())); |
72 | | - formParams.add(new BasicNameValuePair("client_secret", jwtCredential.clientSecret())); |
| 82 | + |
| 83 | + boolean isClientAssertionCertPathProvided = |
| 84 | + jwtCredential.clientAssertionCertPath() != null |
| 85 | + && !jwtCredential.clientAssertionCertPath().isEmpty(); |
| 86 | + |
| 87 | + if (!isClientAssertionCertPathProvided) { |
| 88 | + formParams.add(new BasicNameValuePair("client_secret", jwtCredential.clientSecret())); |
| 89 | + } else { |
| 90 | + formParams.add( |
| 91 | + new BasicNameValuePair( |
| 92 | + "client_assertion", |
| 93 | + createClientAssertion( |
| 94 | + jwtCredential.clientId(), |
| 95 | + jwtCredential.authUrl().toString(), |
| 96 | + jwtCredential.clientAssertionCertPath(), |
| 97 | + jwtCredential.clientAssertionCertStorePassword()))); |
| 98 | + } |
| 99 | + |
73 | 100 | formParams.add(new BasicNameValuePair("audience", jwtCredential.audience())); |
74 | 101 | if (jwtCredential.scope() != null && !jwtCredential.scope().isEmpty()) { |
75 | 102 | formParams.add(new BasicNameValuePair("scope", jwtCredential.scope())); |
76 | 103 | } |
77 | 104 | httpPost.setEntity(new UrlEncodedFormEntity(formParams)); |
78 | 105 | return httpPost; |
79 | 106 | } |
| 107 | + |
| 108 | + /** Create JWT client assertion for OAuth2 authentication */ |
| 109 | + private String createClientAssertion( |
| 110 | + String clientId, String issuer, String certPath, String password) throws Exception { |
| 111 | + Instant now = Instant.now(); |
| 112 | + |
| 113 | + var privateKeyData = loadP12Certificate(certPath, password); |
| 114 | + PrivateKey privateKey = privateKeyData.getKey(); |
| 115 | + String keyId = privateKeyData.getValue(); |
| 116 | + |
| 117 | + return Jwts.builder() |
| 118 | + .issuer(clientId) |
| 119 | + .subject(clientId) |
| 120 | + .audience() |
| 121 | + .add(issuer) |
| 122 | + .and() |
| 123 | + .issuedAt(Date.from(now)) |
| 124 | + .notBefore(Date.from(now)) |
| 125 | + .expiration(Date.from(now.plus(5, ChronoUnit.MINUTES))) |
| 126 | + .id(UUID.randomUUID().toString()) |
| 127 | + .header() |
| 128 | + .add("alg", "RS256") |
| 129 | + .add("typ", "JWT") |
| 130 | + .add("x5t", keyId) |
| 131 | + .and() |
| 132 | + .signWith(privateKey, Jwts.SIG.RS256) |
| 133 | + .compact(); |
| 134 | + } |
| 135 | + |
| 136 | + private Map.Entry<PrivateKey, String> loadP12Certificate(String certPath, String password) |
| 137 | + throws Exception { |
| 138 | + KeyStore keyStore = KeyStore.getInstance("PKCS12"); |
| 139 | + |
| 140 | + try (FileInputStream fis = new FileInputStream(certPath)) { |
| 141 | + keyStore.load(fis, password != null ? password.toCharArray() : null); |
| 142 | + } |
| 143 | + |
| 144 | + String alias = keyStore.aliases().nextElement(); |
| 145 | + PrivateKey privateKey = |
| 146 | + (PrivateKey) keyStore.getKey(alias, password != null ? password.toCharArray() : null); |
| 147 | + X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias); |
| 148 | + |
| 149 | + String x5tThumbprint = generateX5tThumbprint(cert); |
| 150 | + |
| 151 | + return Map.entry(privateKey, x5tThumbprint); |
| 152 | + } |
| 153 | + |
| 154 | + private String generateX5tThumbprint(X509Certificate certificate) { |
| 155 | + try { |
| 156 | + MessageDigest digest = MessageDigest.getInstance("SHA-1"); |
| 157 | + byte[] encoded = digest.digest(certificate.getEncoded()); |
| 158 | + return Base64.getUrlEncoder().withoutPadding().encodeToString(encoded); |
| 159 | + } catch (Exception e) { |
| 160 | + throw new RuntimeException("Failed to generate x5t thumbprint", e); |
| 161 | + } |
| 162 | + } |
80 | 163 | } |
0 commit comments