Skip to content

Commit 1893165

Browse files
authored
feat: add support for OAuth2 resource parameter in JWT authentication (#269)
1 parent 2861d7b commit 1893165

File tree

7 files changed

+232
-1
lines changed

7 files changed

+232
-1
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ operate:
5151
client-id:
5252
client-secret:
5353
scope: # optional
54+
resource: # optional
5455
```
5556
5657
To adjust the (meaningful) default properties, you can also override them:
@@ -66,6 +67,7 @@ operate:
6667
client-id:
6768
client-secret:
6869
scope: # optional
70+
resource: # optional
6971
client-assertion-keystore-path: # optional
7072
client-assertion-keystore-password: # optional
7173
client-assertion-keystore-key-alias: # optional
@@ -102,6 +104,14 @@ operate:
102104
client-secret:
103105
```
104106

107+
### Environment Variables
108+
109+
All configuration properties can also be set via environment variables using Spring Boot's standard naming convention. For example:
110+
111+
- `operate.client.resource` → `OPERATE_CLIENT_RESOURCE`
112+
- `operate.client.client-id` → `OPERATE_CLIENT_CLIENT_ID`
113+
- `operate.client.client-secret` → `OPERATE_CLIENT_CLIENT_SECRET`
114+
105115
### Plain Java
106116

107117
Add the dependency to your project:
@@ -156,6 +166,8 @@ CamundaOperateClientConfiguration configuration =
156166
CamundaOperateClient client = new CamundaOperateClient(configuration);
157167
```
158168

169+
> **Note**: The `JwtCredential` constructor also supports an optional `resource` parameter. If you need to specify a resource in your OAuth2 token requests, you can use the extended constructor that includes the resource parameter.
170+
159171
Build a Camunda Operate client for Saas:
160172

161173
```java

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ private HttpPost buildRequest() throws Exception {
9393
if (jwtCredential.scope() != null && !jwtCredential.scope().isEmpty()) {
9494
formParams.add(new BasicNameValuePair("scope", jwtCredential.scope()));
9595
}
96+
if (jwtCredential.resource() != null && !jwtCredential.resource().isEmpty()) {
97+
formParams.add(new BasicNameValuePair("resource", jwtCredential.resource()));
98+
}
9699
httpPost.setEntity(new UrlEncodedFormEntity(formParams));
97100
return httpPost;
98101
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ public record JwtCredential(
99
String audience,
1010
URL authUrl,
1111
String scope,
12+
String resource,
1213
Path clientAssertionKeystorePath,
1314
String clientAssertionKeystorePassword,
1415
String clientAssertionKeystoreKeyAlias,
1516
String clientAssertionKeystoreKeyPassword) {
1617

1718
public JwtCredential(
1819
String clientId, String clientSecret, String audience, URL authUrl, String scope) {
19-
this(clientId, clientSecret, audience, authUrl, scope, null, null, null, null);
20+
this(clientId, clientSecret, audience, authUrl, scope, null, null, null, null, null);
2021
}
2122
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package io.camunda.operate.auth;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.net.URI;
6+
import java.net.URL;
7+
import org.apache.hc.client5.http.classic.methods.HttpPost;
8+
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
9+
import org.apache.hc.core5.http.io.entity.EntityUtils;
10+
import org.junit.jupiter.api.Test;
11+
12+
public class JwtAuthenticationTest {
13+
14+
@Test
15+
void shouldBuildBasicTokenRequest() throws Exception {
16+
// Given
17+
URL authUrl = URI.create("https://auth.example.com/token").toURL();
18+
JwtCredential credential =
19+
new JwtCredential(
20+
"test-client-id",
21+
"test-client-secret",
22+
"test-audience",
23+
authUrl,
24+
"test-scope",
25+
null, // no resource parameter - backward compatibility
26+
null,
27+
null,
28+
null,
29+
null);
30+
31+
JwtAuthentication authentication = new JwtAuthentication(credential);
32+
33+
// Use reflection to access private method for testing
34+
java.lang.reflect.Method buildRequestMethod =
35+
JwtAuthentication.class.getDeclaredMethod("buildRequest");
36+
buildRequestMethod.setAccessible(true);
37+
38+
// When
39+
HttpPost request = (HttpPost) buildRequestMethod.invoke(authentication);
40+
41+
// Then
42+
UrlEncodedFormEntity entity = (UrlEncodedFormEntity) request.getEntity();
43+
String entityContent = EntityUtils.toString(entity);
44+
45+
// Verify standard OAuth2 parameters are included
46+
assertTrue(entityContent.contains("client_id=test-client-id"));
47+
assertTrue(entityContent.contains("audience=test-audience"));
48+
assertTrue(entityContent.contains("scope=test-scope"));
49+
assertTrue(entityContent.contains("grant_type=client_credentials"));
50+
// Verify resource parameter is not included when null
51+
assertFalse(entityContent.contains("resource="));
52+
}
53+
54+
@Test
55+
void shouldIncludeResourceParameterInTokenRequest() throws Exception {
56+
// Given
57+
URL authUrl = URI.create("https://auth.example.com/token").toURL();
58+
JwtCredential credential =
59+
new JwtCredential(
60+
"test-client-id",
61+
"test-client-secret",
62+
"test-audience",
63+
authUrl,
64+
"test-scope",
65+
"test-resource",
66+
null,
67+
null,
68+
null,
69+
null);
70+
71+
JwtAuthentication authentication = new JwtAuthentication(credential);
72+
73+
// Use reflection to access private method for testing
74+
java.lang.reflect.Method buildRequestMethod =
75+
JwtAuthentication.class.getDeclaredMethod("buildRequest");
76+
buildRequestMethod.setAccessible(true);
77+
78+
// When
79+
HttpPost request = (HttpPost) buildRequestMethod.invoke(authentication);
80+
81+
// Then
82+
UrlEncodedFormEntity entity = (UrlEncodedFormEntity) request.getEntity();
83+
String entityContent = EntityUtils.toString(entity);
84+
85+
assertTrue(entityContent.contains("resource=test-resource"));
86+
assertTrue(entityContent.contains("client_id=test-client-id"));
87+
assertTrue(entityContent.contains("audience=test-audience"));
88+
assertTrue(entityContent.contains("scope=test-scope"));
89+
assertTrue(entityContent.contains("grant_type=client_credentials"));
90+
}
91+
92+
@Test
93+
void shouldNotIncludeResourceParameterWhenNull() throws Exception {
94+
// Given
95+
URL authUrl = URI.create("https://auth.example.com/token").toURL();
96+
JwtCredential credential =
97+
new JwtCredential(
98+
"test-client-id",
99+
"test-client-secret",
100+
"test-audience",
101+
authUrl,
102+
"test-scope",
103+
null, // resource is null
104+
null,
105+
null,
106+
null,
107+
null);
108+
109+
JwtAuthentication authentication = new JwtAuthentication(credential);
110+
111+
// Use reflection to access private method for testing
112+
java.lang.reflect.Method buildRequestMethod =
113+
JwtAuthentication.class.getDeclaredMethod("buildRequest");
114+
buildRequestMethod.setAccessible(true);
115+
116+
// When
117+
HttpPost request = (HttpPost) buildRequestMethod.invoke(authentication);
118+
119+
// Then
120+
UrlEncodedFormEntity entity = (UrlEncodedFormEntity) request.getEntity();
121+
String entityContent = EntityUtils.toString(entity);
122+
123+
assertFalse(entityContent.contains("resource="));
124+
assertTrue(entityContent.contains("client_id=test-client-id"));
125+
assertTrue(entityContent.contains("audience=test-audience"));
126+
assertTrue(entityContent.contains("scope=test-scope"));
127+
}
128+
129+
@Test
130+
void shouldNotIncludeResourceParameterWhenEmpty() throws Exception {
131+
// Given
132+
URL authUrl = URI.create("https://auth.example.com/token").toURL();
133+
JwtCredential credential =
134+
new JwtCredential(
135+
"test-client-id",
136+
"test-client-secret",
137+
"test-audience",
138+
authUrl,
139+
"test-scope",
140+
"", // resource is empty
141+
null,
142+
null,
143+
null,
144+
null);
145+
146+
JwtAuthentication authentication = new JwtAuthentication(credential);
147+
148+
// Use reflection to access private method for testing
149+
java.lang.reflect.Method buildRequestMethod =
150+
JwtAuthentication.class.getDeclaredMethod("buildRequest");
151+
buildRequestMethod.setAccessible(true);
152+
153+
// When
154+
HttpPost request = (HttpPost) buildRequestMethod.invoke(authentication);
155+
156+
// Then
157+
UrlEncodedFormEntity entity = (UrlEncodedFormEntity) request.getEntity();
158+
String entityContent = EntityUtils.toString(entity);
159+
160+
assertFalse(entityContent.contains("resource="));
161+
assertTrue(entityContent.contains("client_id=test-client-id"));
162+
assertTrue(entityContent.contains("audience=test-audience"));
163+
assertTrue(entityContent.contains("scope=test-scope"));
164+
}
165+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public Authentication operateAuthentication() {
8282
properties.audience(),
8383
properties.authUrl(),
8484
properties.scope(),
85+
properties.resource(),
8586
properties.clientAssertionKeystorePath(),
8687
properties.clientAssertionKeystorePassword(),
8788
properties.clientAssertionKeystoreKeyAlias(),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public record OperateClientConfigurationProperties(
2121
URL authUrl,
2222
String audience,
2323
String scope,
24+
String resource,
2425
Path clientAssertionKeystorePath,
2526
String clientAssertionKeystorePassword,
2627
String clientAssertionKeystoreKeyAlias,

extension/spring-boot-starter-camunda-operate/src/test/java/io/camunda/operate/spring/OperateClientConfigurationPropertiesProfileOidcTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,52 @@ void shouldApplyProfiles() throws MalformedURLException {
3131
"http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token")
3232
.toURL());
3333
}
34+
35+
@SpringBootTest(
36+
properties = {
37+
"operate.client.profile=oidc",
38+
"operate.client.client-id=test-client",
39+
"operate.client.client-secret=test-secret",
40+
"operate.client.resource=test-resource"
41+
})
42+
static class WithResourcePropertyTest {
43+
@Autowired OperateClientConfigurationProperties properties;
44+
45+
@Test
46+
void shouldApplyResourceProperty() throws MalformedURLException {
47+
assertThat(properties.profile()).isEqualTo(oidc);
48+
assertThat(properties.clientId()).isEqualTo("test-client");
49+
assertThat(properties.clientSecret()).isEqualTo("test-secret");
50+
assertThat(properties.resource()).isEqualTo("test-resource");
51+
assertThat(properties.baseUrl()).isEqualTo(URI.create("http://localhost:8081").toURL());
52+
assertThat(properties.enabled()).isEqualTo(true);
53+
assertThat(properties.authUrl())
54+
.isEqualTo(
55+
URI.create(
56+
"http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token")
57+
.toURL());
58+
}
59+
}
60+
61+
@SpringBootTest(
62+
properties = {
63+
"operate.client.profile=oidc",
64+
"operate.client.client-id=env-test-client",
65+
"operate.client.client-secret=env-test-secret",
66+
"operate.client.resource=env-test-resource",
67+
"operate.client.scope=env-test-scope"
68+
})
69+
static class EnvironmentVariableTest {
70+
@Autowired OperateClientConfigurationProperties properties;
71+
72+
@Test
73+
void shouldSupportEnvironmentVariableMapping() {
74+
// Verify that properties can be set via environment-style configuration
75+
assertThat(properties.profile()).isEqualTo(oidc);
76+
assertThat(properties.clientId()).isEqualTo("env-test-client");
77+
assertThat(properties.clientSecret()).isEqualTo("env-test-secret");
78+
assertThat(properties.resource()).isEqualTo("env-test-resource");
79+
assertThat(properties.scope()).isEqualTo("env-test-scope");
80+
}
81+
}
3482
}

0 commit comments

Comments
 (0)