Skip to content

Commit ec7bf07

Browse files
committed
feat(auth): support client assertions
1 parent e55461a commit ec7bf07

File tree

6 files changed

+115
-5
lines changed

6 files changed

+115
-5
lines changed

extension/java-client-operate/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
<groupId>com.fasterxml.jackson.core</groupId>
3939
<artifactId>jackson-annotations</artifactId>
4040
</dependency>
41+
<dependency>
42+
<groupId>io.jsonwebtoken</groupId>
43+
<artifactId>jjwt-api</artifactId>
44+
</dependency>
4145
<dependency>
4246
<groupId>org.junit.jupiter</groupId>
4347
<artifactId>junit-jupiter-api</artifactId>

extension/java-client-operate/src/main/java/io/camunda/operate/auth/JwtAuthentication.java

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@
33
import com.fasterxml.jackson.core.type.TypeReference;
44
import com.fasterxml.jackson.databind.ObjectMapper;
55
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;
713
import java.time.LocalDateTime;
14+
import java.time.temporal.ChronoUnit;
815
import java.util.ArrayList;
16+
import java.util.Base64;
17+
import java.util.Date;
918
import java.util.List;
1019
import java.util.Map;
20+
import java.util.UUID;
1121
import org.apache.hc.client5.http.classic.methods.HttpPost;
1222
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
1323
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
@@ -63,18 +73,91 @@ private TokenResponse retrieveToken() {
6373
}
6474
}
6575

66-
private HttpPost buildRequest() throws URISyntaxException {
76+
private HttpPost buildRequest() throws Exception {
6777
HttpPost httpPost = new HttpPost(jwtCredential.authUrl().toURI());
6878
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded");
6979
List<NameValuePair> formParams = new ArrayList<>();
7080
formParams.add(new BasicNameValuePair("grant_type", "client_credentials"));
7181
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+
73100
formParams.add(new BasicNameValuePair("audience", jwtCredential.audience()));
74101
if (jwtCredential.scope() != null && !jwtCredential.scope().isEmpty()) {
75102
formParams.add(new BasicNameValuePair("scope", jwtCredential.scope()));
76103
}
77104
httpPost.setEntity(new UrlEncodedFormEntity(formParams));
78105
return httpPost;
79106
}
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+
}
80163
}

extension/java-client-operate/src/main/java/io/camunda/operate/auth/JwtCredential.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,16 @@
33
import java.net.URL;
44

55
public record JwtCredential(
6-
String clientId, String clientSecret, String audience, URL authUrl, String scope) {}
6+
String clientId,
7+
String clientSecret,
8+
String audience,
9+
URL authUrl,
10+
String scope,
11+
String clientAssertionCertPath,
12+
String clientAssertionCertStorePassword) {
13+
14+
public JwtCredential(
15+
String clientId, String clientSecret, String audience, URL authUrl, String scope) {
16+
this(clientId, clientSecret, audience, authUrl, scope, null, null);
17+
}
18+
}

extension/spring-boot-starter-camunda-operate/src/main/java/io/camunda/operate/spring/OperateClientConfiguration.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ public Authentication operateAuthentication() {
7878
properties.clientSecret(),
7979
properties.audience(),
8080
properties.authUrl(),
81-
properties.scope()),
81+
properties.scope(),
82+
properties.clientAssertionCertPath(),
83+
properties.clientAssertionCertStorePassword()),
8284
new TypeReferenceHttpClientResponseHandler<>(new TypeReference<>() {}, objectMapper));
8385
}
8486
default -> throw new IllegalStateException("Unsupported profile: " + properties.profile());

extension/spring-boot-starter-camunda-operate/src/main/java/io/camunda/operate/spring/OperateClientConfigurationProperties.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public record OperateClientConfigurationProperties(
2020
URL authUrl,
2121
String audience,
2222
String scope,
23+
String clientAssertionCertPath,
24+
String clientAssertionCertStorePassword,
2325
// saas auth properies
2426
String region,
2527
String clusterId) {

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
<jackson.version>2.19.1</jackson.version>
2727
<commons-lang.version>3.15.0</commons-lang.version>
2828

29+
<jjwt.version>0.12.6</jjwt.version>
30+
2931
<plugin.version.maven-enforcer-plugin>3.5.0</plugin.version.maven-enforcer-plugin>
3032
<plugin.version.function-maven-plugin>0.11.1</plugin.version.function-maven-plugin>
3133
<plugin.version.maven-install-plugin>3.1.4</plugin.version.maven-install-plugin>
@@ -87,6 +89,11 @@
8789
<artifactId>jackson-annotations</artifactId>
8890
<version>${jackson.version}</version>
8991
</dependency>
92+
<dependency>
93+
<groupId>io.jsonwebtoken</groupId>
94+
<artifactId>jjwt-api</artifactId>
95+
<version>${jjwt.version}</version>
96+
</dependency>
9097
</dependencies>
9198
</dependencyManagement>
9299

0 commit comments

Comments
 (0)