Skip to content
Closed
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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ operate:
profile: oidc
client-id:
client-secret:
<<<<<<< HEAD
=======
scope: # optional
resource: # optional
>>>>>>> 1893165 (feat: add support for OAuth2 resource parameter in JWT authentication (#269))
```

To adjust the (meaningful) default properties, you can also override them:
Expand All @@ -64,6 +69,15 @@ operate:
audience: operate-api
client-id:
client-secret:
<<<<<<< HEAD
=======
scope: # optional
resource: # optional
client-assertion-keystore-path: # optional
client-assertion-keystore-password: # optional
client-assertion-keystore-key-alias: # optional
client-assertion-keystore-key-password: # optional
>>>>>>> 1893165 (feat: add support for OAuth2 resource parameter in JWT authentication (#269))
```

Configure a Camunda Operate client for Saas:
Expand Down Expand Up @@ -94,6 +108,14 @@ operate:
client-secret:
```

### Environment Variables

All configuration properties can also be set via environment variables using Spring Boot's standard naming convention. For example:

- `operate.client.resource` → `OPERATE_CLIENT_RESOURCE`
- `operate.client.client-id` → `OPERATE_CLIENT_CLIENT_ID`
- `operate.client.client-secret` → `OPERATE_CLIENT_CLIENT_SECRET`

### Plain Java

Add the dependency to your project:
Expand Down Expand Up @@ -148,6 +170,8 @@ CamundaOperateClientConfiguration configuration =
CamundaOperateClient client = new CamundaOperateClient(configuration);
```

> **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.

Build a Camunda Operate client for Saas:

```java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ private HttpPost buildRequest() throws URISyntaxException {
if (jwtCredential.scope() != null && !jwtCredential.scope().isEmpty()) {
formParams.add(new BasicNameValuePair("scope", jwtCredential.scope()));
}
if (jwtCredential.resource() != null && !jwtCredential.resource().isEmpty()) {
formParams.add(new BasicNameValuePair("resource", jwtCredential.resource()));
}
httpPost.setEntity(new UrlEncodedFormEntity(formParams));
return httpPost;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,23 @@
import java.net.URL;

public record JwtCredential(
<<<<<<< HEAD
String clientId, String clientSecret, String audience, URL authUrl, String scope) {}
=======
String clientId,
String clientSecret,
String audience,
URL authUrl,
String scope,
String resource,
Path clientAssertionKeystorePath,
String clientAssertionKeystorePassword,
String clientAssertionKeystoreKeyAlias,
String clientAssertionKeystoreKeyPassword) {

public JwtCredential(
String clientId, String clientSecret, String audience, URL authUrl, String scope) {
this(clientId, clientSecret, audience, authUrl, scope, null, null, null, null, null);
}
}
>>>>>>> 1893165 (feat: add support for OAuth2 resource parameter in JWT authentication (#269))
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package io.camunda.operate.auth;

import static org.junit.jupiter.api.Assertions.*;

import java.net.URI;
import java.net.URL;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.junit.jupiter.api.Test;

public class JwtAuthenticationTest {

@Test
void shouldBuildBasicTokenRequest() throws Exception {
// Given
URL authUrl = URI.create("https://auth.example.com/token").toURL();
JwtCredential credential =
new JwtCredential(
"test-client-id",
"test-client-secret",
"test-audience",
authUrl,
"test-scope",
null, // no resource parameter - backward compatibility
null,
null,
null,
null);

JwtAuthentication authentication = new JwtAuthentication(credential);

// Use reflection to access private method for testing
java.lang.reflect.Method buildRequestMethod =
JwtAuthentication.class.getDeclaredMethod("buildRequest");
buildRequestMethod.setAccessible(true);

// When
HttpPost request = (HttpPost) buildRequestMethod.invoke(authentication);

// Then
UrlEncodedFormEntity entity = (UrlEncodedFormEntity) request.getEntity();
String entityContent = EntityUtils.toString(entity);

// Verify standard OAuth2 parameters are included
assertTrue(entityContent.contains("client_id=test-client-id"));
assertTrue(entityContent.contains("audience=test-audience"));
assertTrue(entityContent.contains("scope=test-scope"));
assertTrue(entityContent.contains("grant_type=client_credentials"));
// Verify resource parameter is not included when null
assertFalse(entityContent.contains("resource="));
}

@Test
void shouldIncludeResourceParameterInTokenRequest() throws Exception {
// Given
URL authUrl = URI.create("https://auth.example.com/token").toURL();
JwtCredential credential =
new JwtCredential(
"test-client-id",
"test-client-secret",
"test-audience",
authUrl,
"test-scope",
"test-resource",
null,
null,
null,
null);

JwtAuthentication authentication = new JwtAuthentication(credential);

// Use reflection to access private method for testing
java.lang.reflect.Method buildRequestMethod =
JwtAuthentication.class.getDeclaredMethod("buildRequest");
buildRequestMethod.setAccessible(true);

// When
HttpPost request = (HttpPost) buildRequestMethod.invoke(authentication);

// Then
UrlEncodedFormEntity entity = (UrlEncodedFormEntity) request.getEntity();
String entityContent = EntityUtils.toString(entity);

assertTrue(entityContent.contains("resource=test-resource"));
assertTrue(entityContent.contains("client_id=test-client-id"));
assertTrue(entityContent.contains("audience=test-audience"));
assertTrue(entityContent.contains("scope=test-scope"));
assertTrue(entityContent.contains("grant_type=client_credentials"));
}

@Test
void shouldNotIncludeResourceParameterWhenNull() throws Exception {
// Given
URL authUrl = URI.create("https://auth.example.com/token").toURL();
JwtCredential credential =
new JwtCredential(
"test-client-id",
"test-client-secret",
"test-audience",
authUrl,
"test-scope",
null, // resource is null
null,
null,
null,
null);

JwtAuthentication authentication = new JwtAuthentication(credential);

// Use reflection to access private method for testing
java.lang.reflect.Method buildRequestMethod =
JwtAuthentication.class.getDeclaredMethod("buildRequest");
buildRequestMethod.setAccessible(true);

// When
HttpPost request = (HttpPost) buildRequestMethod.invoke(authentication);

// Then
UrlEncodedFormEntity entity = (UrlEncodedFormEntity) request.getEntity();
String entityContent = EntityUtils.toString(entity);

assertFalse(entityContent.contains("resource="));
assertTrue(entityContent.contains("client_id=test-client-id"));
assertTrue(entityContent.contains("audience=test-audience"));
assertTrue(entityContent.contains("scope=test-scope"));
}

@Test
void shouldNotIncludeResourceParameterWhenEmpty() throws Exception {
// Given
URL authUrl = URI.create("https://auth.example.com/token").toURL();
JwtCredential credential =
new JwtCredential(
"test-client-id",
"test-client-secret",
"test-audience",
authUrl,
"test-scope",
"", // resource is empty
null,
null,
null,
null);

JwtAuthentication authentication = new JwtAuthentication(credential);

// Use reflection to access private method for testing
java.lang.reflect.Method buildRequestMethod =
JwtAuthentication.class.getDeclaredMethod("buildRequest");
buildRequestMethod.setAccessible(true);

// When
HttpPost request = (HttpPost) buildRequestMethod.invoke(authentication);

// Then
UrlEncodedFormEntity entity = (UrlEncodedFormEntity) request.getEntity();
String entityContent = EntityUtils.toString(entity);

assertFalse(entityContent.contains("resource="));
assertTrue(entityContent.contains("client_id=test-client-id"));
assertTrue(entityContent.contains("audience=test-audience"));
assertTrue(entityContent.contains("scope=test-scope"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,16 @@ public Authentication operateAuthentication() {
properties.clientSecret(),
properties.audience(),
properties.authUrl(),
<<<<<<< HEAD
properties.scope()),
=======
properties.scope(),
properties.resource(),
properties.clientAssertionKeystorePath(),
properties.clientAssertionKeystorePassword(),
properties.clientAssertionKeystoreKeyAlias(),
properties.clientAssertionKeystoreKeyPassword()),
>>>>>>> 1893165 (feat: add support for OAuth2 resource parameter in JWT authentication (#269))
new TypeReferenceHttpClientResponseHandler<>(new TypeReference<>() {}, objectMapper));
}
default -> throw new IllegalStateException("Unsupported profile: " + properties.profile());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ public record OperateClientConfigurationProperties(
URL authUrl,
String audience,
String scope,
<<<<<<< HEAD
=======
String resource,
Path clientAssertionKeystorePath,
String clientAssertionKeystorePassword,
String clientAssertionKeystoreKeyAlias,
String clientAssertionKeystoreKeyPassword,
>>>>>>> 1893165 (feat: add support for OAuth2 resource parameter in JWT authentication (#269))
// saas auth properies
String region,
String clusterId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,52 @@ void shouldApplyProfiles() throws MalformedURLException {
"http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token")
.toURL());
}

@SpringBootTest(
properties = {
"operate.client.profile=oidc",
"operate.client.client-id=test-client",
"operate.client.client-secret=test-secret",
"operate.client.resource=test-resource"
})
static class WithResourcePropertyTest {
@Autowired OperateClientConfigurationProperties properties;

@Test
void shouldApplyResourceProperty() throws MalformedURLException {
assertThat(properties.profile()).isEqualTo(oidc);
assertThat(properties.clientId()).isEqualTo("test-client");
assertThat(properties.clientSecret()).isEqualTo("test-secret");
assertThat(properties.resource()).isEqualTo("test-resource");
assertThat(properties.baseUrl()).isEqualTo(URI.create("http://localhost:8081").toURL());
assertThat(properties.enabled()).isEqualTo(true);
assertThat(properties.authUrl())
.isEqualTo(
URI.create(
"http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token")
.toURL());
}
}

@SpringBootTest(
properties = {
"operate.client.profile=oidc",
"operate.client.client-id=env-test-client",
"operate.client.client-secret=env-test-secret",
"operate.client.resource=env-test-resource",
"operate.client.scope=env-test-scope"
})
static class EnvironmentVariableTest {
@Autowired OperateClientConfigurationProperties properties;

@Test
void shouldSupportEnvironmentVariableMapping() {
// Verify that properties can be set via environment-style configuration
assertThat(properties.profile()).isEqualTo(oidc);
assertThat(properties.clientId()).isEqualTo("env-test-client");
assertThat(properties.clientSecret()).isEqualTo("env-test-secret");
assertThat(properties.resource()).isEqualTo("env-test-resource");
assertThat(properties.scope()).isEqualTo("env-test-scope");
}
}
}