Skip to content

Commit bf34fe4

Browse files
authored
feat(dps): add JWT claims validation on DPS oauth2 authorization (#5689)
1 parent ca15d1a commit bf34fe4

14 files changed

Lines changed: 81 additions & 77 deletions

File tree

core/common/lib/token-lib/build.gradle.kts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ dependencies {
2222
api(project(":spi:common:token-spi"))
2323
api(project(":spi:common:jwt-spi"))
2424

25-
implementation(project(":core:common:lib:crypto-common-lib")) // for the CryptoConverter
26-
implementation(libs.nimbus.jwt)
2725
api(libs.bouncyCastle.bcpkixJdk18on)
26+
27+
implementation(project(":core:common:lib:crypto-common-lib"))
28+
implementation(libs.nimbus.jwt)
29+
30+
testImplementation(project(":core:common:junit"))
2831
}
2932

3033

extensions/common/crypto/jwt-verifiable-credentials/src/main/java/org/eclipse/edc/verifiablecredentials/jwt/rules/HasSubjectRule.java renamed to core/common/lib/token-lib/src/main/java/org/eclipse/edc/token/rules/HasSubjectRule.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Amadeus
2+
* Copyright (c) 2026 Think-it GmbH
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Apache License, Version 2.0 which is available at
@@ -8,17 +8,16 @@
88
* SPDX-License-Identifier: Apache-2.0
99
*
1010
* Contributors:
11-
* Amadeus - initial API and implementation
11+
* Think-it GmbH - initial API and implementation
1212
*
1313
*/
1414

15-
package org.eclipse.edc.verifiablecredentials.jwt.rules;
15+
package org.eclipse.edc.token.rules;
1616

1717
import com.nimbusds.jwt.JWTClaimNames;
1818
import org.eclipse.edc.spi.iam.ClaimToken;
1919
import org.eclipse.edc.spi.result.Result;
2020
import org.eclipse.edc.token.spi.TokenValidationRule;
21-
import org.eclipse.edc.util.string.StringUtils;
2221
import org.jetbrains.annotations.NotNull;
2322
import org.jetbrains.annotations.Nullable;
2423

@@ -31,8 +30,10 @@ public class HasSubjectRule implements TokenValidationRule {
3130

3231
@Override
3332
public Result<Void> checkRule(@NotNull ClaimToken toVerify, @Nullable Map<String, Object> additional) {
34-
return StringUtils.isNullOrEmpty(toVerify.getStringClaim(JWTClaimNames.SUBJECT)) ?
35-
Result.failure("The '%s' claim is mandatory and must not be null.".formatted(JWTClaimNames.SUBJECT)) :
36-
Result.success();
33+
var subject = toVerify.getStringClaim(JWTClaimNames.SUBJECT);
34+
if (subject == null || subject.isBlank()) {
35+
return Result.failure("The '%s' claim is mandatory and must not be null.".formatted(JWTClaimNames.SUBJECT));
36+
}
37+
return Result.success();
3738
}
3839
}

extensions/common/crypto/jwt-verifiable-credentials/src/main/java/org/eclipse/edc/verifiablecredentials/jwt/rules/JtiValidationRule.java renamed to core/common/lib/token-lib/src/main/java/org/eclipse/edc/token/rules/JtiValidationRule.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
2+
* Copyright (c) 2026 Think-it GmbH
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Apache License, Version 2.0 which is available at
@@ -8,11 +8,11 @@
88
* SPDX-License-Identifier: Apache-2.0
99
*
1010
* Contributors:
11-
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
11+
* Think-it GmbH - initial API and implementation
1212
*
1313
*/
1414

15-
package org.eclipse.edc.verifiablecredentials.jwt.rules;
15+
package org.eclipse.edc.token.rules;
1616

1717
import org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames;
1818
import org.eclipse.edc.jwt.validation.jti.JtiValidationEntry;

extensions/common/crypto/jwt-verifiable-credentials/src/test/java/org/eclipse/edc/verifiablecredentials/jwt/rules/HasSubjectRuleTest.java renamed to core/common/lib/token-lib/src/test/java/org/eclipse/edc/token/rules/HasSubjectRuleTest.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Amadeus
2+
* Copyright (c) 2026 Think-it GmbH
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Apache License, Version 2.0 which is available at
@@ -8,11 +8,11 @@
88
* SPDX-License-Identifier: Apache-2.0
99
*
1010
* Contributors:
11-
* Amadeus - initial API and implementation
11+
* Think-it GmbH - initial API and implementation
1212
*
1313
*/
1414

15-
package org.eclipse.edc.verifiablecredentials.jwt.rules;
15+
package org.eclipse.edc.token.rules;
1616

1717
import org.eclipse.edc.spi.iam.ClaimToken;
1818
import org.junit.jupiter.api.Test;
@@ -27,21 +27,26 @@ class HasSubjectRuleTest {
2727

2828
@Test
2929
void subjectClaimPresent() {
30-
assertThat(rule.checkRule(ClaimToken.Builder.newInstance().claim("sub", "subject-test").build(), Map.of()))
31-
.isSucceeded();
30+
var result = rule.checkRule(ClaimToken.Builder.newInstance().claim("sub", "subject-test").build(), Map.of());
31+
32+
assertThat(result).isSucceeded();
3233
}
3334

3435
@Test
3536
void subjectClaimEmpty() {
36-
assertThat(rule.checkRule(ClaimToken.Builder.newInstance().claim("sub", "").build(), Map.of()))
37+
var result = rule.checkRule(ClaimToken.Builder.newInstance().claim("sub", "").build(), Map.of());
38+
39+
assertThat(result)
3740
.isFailed()
3841
.detail()
3942
.isEqualTo("The 'sub' claim is mandatory and must not be null.");
4043
}
4144

4245
@Test
4346
void subjectClaimNotPresent() {
44-
assertThat(rule.checkRule(ClaimToken.Builder.newInstance().build(), Map.of()))
47+
var result = rule.checkRule(ClaimToken.Builder.newInstance().build(), Map.of());
48+
49+
assertThat(result)
4550
.isFailed()
4651
.detail()
4752
.isEqualTo("The 'sub' claim is mandatory and must not be null.");

extensions/common/crypto/jwt-verifiable-credentials/src/test/java/org/eclipse/edc/verifiablecredentials/jwt/rules/JtiValidationRuleTest.java renamed to core/common/lib/token-lib/src/test/java/org/eclipse/edc/token/rules/JtiValidationRuleTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
2+
* Copyright (c) 2026 Think-it GmbH
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Apache License, Version 2.0 which is available at
@@ -8,11 +8,11 @@
88
* SPDX-License-Identifier: Apache-2.0
99
*
1010
* Contributors:
11-
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
11+
* Think-it GmbH - initial API and implementation
1212
*
1313
*/
1414

15-
package org.eclipse.edc.verifiablecredentials.jwt.rules;
15+
package org.eclipse.edc.token.rules;
1616

1717
import org.eclipse.edc.jwt.validation.jti.JtiValidationEntry;
1818
import org.eclipse.edc.jwt.validation.jti.JtiValidationStore;

core/data-plane-selector/data-plane-selector-core/src/main/java/org/eclipse/edc/connector/dataplane/selector/DataPlaneSelectorManagerConfiguration.java

Lines changed: 0 additions & 26 deletions
This file was deleted.

data-protocols/data-plane-signaling/data-plane-signaling-oauth2/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@ dependencies {
2121
api(project(":spi:common:oauth2-spi"))
2222
api(project(":data-protocols:data-plane-signaling:data-plane-signaling-spi"))
2323

24+
implementation(project(":core:common:lib:token-lib"))
25+
2426
testImplementation(project(":core:common:junit"))
2527
}

data-protocols/data-plane-signaling/data-plane-signaling-oauth2/src/main/java/org/eclipse/edc/signaling/oauth2/DataPlaneSignalingOauth2Extension.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,43 @@
1515
package org.eclipse.edc.signaling.oauth2;
1616

1717
import org.eclipse.edc.iam.oauth2.spi.client.Oauth2Client;
18+
import org.eclipse.edc.jwt.validation.jti.JtiValidationStore;
1819
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
1920
import org.eclipse.edc.signaling.oauth2.logic.Oauth2CredentialsSignalingAuthorization;
2021
import org.eclipse.edc.signaling.spi.authorization.SignalingAuthorizationRegistry;
2122
import org.eclipse.edc.spi.system.ServiceExtension;
2223
import org.eclipse.edc.spi.system.ServiceExtensionContext;
24+
import org.eclipse.edc.token.rules.ExpirationIssuedAtValidationRule;
25+
import org.eclipse.edc.token.rules.HasSubjectRule;
26+
import org.eclipse.edc.token.rules.JtiValidationRule;
27+
import org.eclipse.edc.token.spi.TokenValidationRulesRegistry;
2328
import org.eclipse.edc.token.spi.TokenValidationService;
2429

30+
import java.time.Clock;
31+
2532
public class DataPlaneSignalingOauth2Extension implements ServiceExtension {
2633

34+
public static final String VALIDATION_RULES_CONTEXT = "signaling-api-oauth2";
35+
2736
@Inject
2837
private SignalingAuthorizationRegistry signalingAuthorizationRegistry;
2938
@Inject
3039
private Oauth2Client oauth2Client;
3140
@Inject
3241
private TokenValidationService tokenValidationService;
42+
@Inject
43+
private TokenValidationRulesRegistry tokenValidationRulesRegistry;
44+
@Inject
45+
private Clock clock;
46+
@Inject
47+
private JtiValidationStore jtiValidationStore;
3348

3449
@Override
3550
public void initialize(ServiceExtensionContext context) {
36-
signalingAuthorizationRegistry.register(new Oauth2CredentialsSignalingAuthorization(oauth2Client, tokenValidationService));
51+
tokenValidationRulesRegistry.addRule(VALIDATION_RULES_CONTEXT, new HasSubjectRule());
52+
tokenValidationRulesRegistry.addRule(VALIDATION_RULES_CONTEXT, new JtiValidationRule(jtiValidationStore, context.getMonitor().withPrefix(VALIDATION_RULES_CONTEXT)));
53+
tokenValidationRulesRegistry.addRule(VALIDATION_RULES_CONTEXT, new ExpirationIssuedAtValidationRule(clock, 0, false));
54+
55+
signalingAuthorizationRegistry.register(new Oauth2CredentialsSignalingAuthorization(oauth2Client, tokenValidationService, tokenValidationRulesRegistry));
3756
}
3857
}

data-protocols/data-plane-signaling/data-plane-signaling-oauth2/src/main/java/org/eclipse/edc/signaling/oauth2/logic/Oauth2CredentialsSignalingAuthorization.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
import org.eclipse.edc.connector.dataplane.selector.spi.instance.AuthorizationProfile;
1818
import org.eclipse.edc.iam.oauth2.spi.client.Oauth2Client;
1919
import org.eclipse.edc.iam.oauth2.spi.client.SharedSecretOauth2CredentialsRequest;
20+
import org.eclipse.edc.signaling.oauth2.DataPlaneSignalingOauth2Extension;
2021
import org.eclipse.edc.signaling.spi.authorization.Header;
2122
import org.eclipse.edc.signaling.spi.authorization.SignalingAuthorization;
2223
import org.eclipse.edc.spi.result.Result;
24+
import org.eclipse.edc.token.spi.TokenValidationRulesRegistry;
2325
import org.eclipse.edc.token.spi.TokenValidationService;
2426

2527
import java.util.function.Function;
@@ -55,10 +57,12 @@ public class Oauth2CredentialsSignalingAuthorization implements SignalingAuthori
5557
private static final String BEARER = "Bearer ";
5658
private final Oauth2Client oauth2Client;
5759
private final TokenValidationService tokenValidationService;
60+
private final TokenValidationRulesRegistry tokenValidationRulesRegistry;
5861

59-
public Oauth2CredentialsSignalingAuthorization(Oauth2Client oauth2Client, TokenValidationService tokenValidationService) {
62+
public Oauth2CredentialsSignalingAuthorization(Oauth2Client oauth2Client, TokenValidationService tokenValidationService, TokenValidationRulesRegistry tokenValidationRulesRegistry) {
6063
this.oauth2Client = oauth2Client;
6164
this.tokenValidationService = tokenValidationService;
65+
this.tokenValidationRulesRegistry = tokenValidationRulesRegistry;
6266
}
6367

6468
/**
@@ -88,7 +92,9 @@ public Result<String> isAuthorized(Function<String, String> headerGetter) {
8892

8993
var token = authorization.substring(BEARER.length());
9094

91-
var tokenValidation = tokenValidationService.validate(token, i -> Result.success(null));
95+
var rules = tokenValidationRulesRegistry.getRules(DataPlaneSignalingOauth2Extension.VALIDATION_RULES_CONTEXT);
96+
97+
var tokenValidation = tokenValidationService.validate(token, i -> Result.success(null), rules);
9298
if (tokenValidation.failed()) {
9399
return tokenValidation.mapFailure();
94100
}

data-protocols/data-plane-signaling/data-plane-signaling-oauth2/src/test/java/org/eclipse/edc/signaling/oauth2/logic/Oauth2CredentialsSignalingAuthorizationTest.java

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,26 @@
1414

1515
package org.eclipse.edc.signaling.oauth2.logic;
1616

17-
import com.nimbusds.jose.JOSEException;
18-
import com.nimbusds.jose.jwk.KeyUse;
19-
import com.nimbusds.jose.jwk.RSAKey;
20-
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
2117
import org.eclipse.edc.connector.dataplane.selector.spi.instance.AuthorizationProfile;
2218
import org.eclipse.edc.iam.oauth2.spi.client.Oauth2Client;
2319
import org.eclipse.edc.spi.iam.ClaimToken;
2420
import org.eclipse.edc.spi.iam.TokenRepresentation;
2521
import org.eclipse.edc.spi.result.Result;
22+
import org.eclipse.edc.token.spi.TokenValidationRule;
23+
import org.eclipse.edc.token.spi.TokenValidationRulesRegistry;
2624
import org.eclipse.edc.token.spi.TokenValidationService;
27-
import org.junit.jupiter.api.BeforeEach;
2825
import org.junit.jupiter.api.Nested;
2926
import org.junit.jupiter.api.Test;
3027

28+
import java.util.List;
3129
import java.util.Map;
32-
import java.util.UUID;
3330

3431
import static org.assertj.core.api.Assertions.assertThat;
3532
import static org.mockito.ArgumentMatchers.any;
33+
import static org.mockito.ArgumentMatchers.anyList;
34+
import static org.mockito.ArgumentMatchers.anyString;
3635
import static org.mockito.ArgumentMatchers.eq;
36+
import static org.mockito.ArgumentMatchers.same;
3737
import static org.mockito.Mockito.mock;
3838
import static org.mockito.Mockito.verify;
3939
import static org.mockito.Mockito.when;
@@ -42,17 +42,9 @@ class Oauth2CredentialsSignalingAuthorizationTest {
4242

4343
private final Oauth2Client oauth2Client = mock();
4444
private final TokenValidationService tokenValidationService = mock();
45-
private Oauth2CredentialsSignalingAuthorization authorization;
46-
private RSAKey signingKey;
47-
48-
@BeforeEach
49-
void setUp() throws JOSEException {
50-
authorization = new Oauth2CredentialsSignalingAuthorization(oauth2Client, tokenValidationService);
51-
signingKey = new RSAKeyGenerator(2048)
52-
.keyUse(KeyUse.SIGNATURE)
53-
.keyID(UUID.randomUUID().toString())
54-
.generate();
55-
}
45+
private final TokenValidationRulesRegistry tokenValidationRulesRegistry = mock();
46+
private final Oauth2CredentialsSignalingAuthorization authorization = new Oauth2CredentialsSignalingAuthorization(
47+
oauth2Client, tokenValidationService, tokenValidationRulesRegistry);
5648

5749
@Test
5850
void getType_shouldReturnOauth2ClientCredentials() {
@@ -67,20 +59,22 @@ void shouldReturnSuccess_whenValidJwtWithSubClaim() {
6759
var callerId = "test-caller-id";
6860
var token = "token";
6961
var claimToken = ClaimToken.Builder.newInstance().claim("sub", callerId).build();
70-
when(tokenValidationService.validate(any(), any())).thenReturn(Result.success(claimToken));
62+
when(tokenValidationService.validate(anyString(), any(), anyList())).thenReturn(Result.success(claimToken));
63+
var validationRules = List.of(mock(TokenValidationRule.class));
64+
when(tokenValidationRulesRegistry.getRules(any())).thenReturn(validationRules);
7165

7266
var result = authorization.isAuthorized(header -> "Bearer " + token);
7367

7468
assertThat(result.succeeded()).isTrue();
7569
assertThat(result.getContent()).isEqualTo(callerId);
76-
verify(tokenValidationService).validate(eq(token), any());
70+
verify(tokenValidationService).validate(eq(token), any(), same(validationRules));
7771
}
7872

7973
@Test
8074
void shouldReturnFailure_whenSubClaimIsNotThere() {
8175
var token = "token";
8276
var claimToken = ClaimToken.Builder.newInstance().claim("sub", null).build();
83-
when(tokenValidationService.validate(any(), any())).thenReturn(Result.success(claimToken));
77+
when(tokenValidationService.validate(anyString(), any(), anyList())).thenReturn(Result.success(claimToken));
8478

8579
var result = authorization.isAuthorized(header -> "Bearer " + token);
8680

@@ -89,7 +83,7 @@ void shouldReturnFailure_whenSubClaimIsNotThere() {
8983

9084
@Test
9185
void shouldReturnFailure_whenTokenValidationFails() {
92-
when(tokenValidationService.validate(any(), any())).thenReturn(Result.failure("validation error"));
86+
when(tokenValidationService.validate(anyString(), any(), anyList())).thenReturn(Result.failure("validation error"));
9387

9488
var result = authorization.isAuthorized(header -> "Bearer token");
9589

0 commit comments

Comments
 (0)