Skip to content

Commit c8aee7a

Browse files
feat: upstream OAuth2-JWT-based authn/authz (#5373)
1 parent 64362c9 commit c8aee7a

26 files changed

Lines changed: 1363 additions & 7 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2025 Metaform Systems, Inc.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Metaform Systems, Inc. - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.token.rules;
16+
17+
import org.eclipse.edc.spi.iam.ClaimToken;
18+
import org.eclipse.edc.spi.result.Result;
19+
import org.eclipse.edc.token.spi.TokenValidationRule;
20+
import org.jetbrains.annotations.NotNull;
21+
import org.jetbrains.annotations.Nullable;
22+
23+
import java.util.Map;
24+
import java.util.Objects;
25+
26+
public record IssuerEqualsValidationRule(String expectedIssuer) implements TokenValidationRule {
27+
28+
@Override
29+
public Result<Void> checkRule(@NotNull ClaimToken toVerify, @Nullable Map<String, Object> additional) {
30+
var iss = toVerify.getStringClaim("iss");
31+
return Objects.equals(iss, expectedIssuer) ?
32+
Result.success() :
33+
Result.failure("'iss' claim does not match expected issuer, expected '%s', got '%s'".formatted(expectedIssuer, iss));
34+
}
35+
}

core/common/token-core/src/test/java/org/eclipse/edc/jwt/rules/AudienceValidationRuleTest.java renamed to core/common/lib/token-lib/src/test/java/org/eclipse/edc/token/rules/AudienceValidationRuleTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
*
1313
*/
1414

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

1717
import org.eclipse.edc.spi.iam.ClaimToken;
18-
import org.eclipse.edc.token.rules.AudienceValidationRule;
1918
import org.eclipse.edc.token.spi.TokenValidationRule;
2019
import org.junit.jupiter.api.Test;
2120

core/common/token-core/src/test/java/org/eclipse/edc/jwt/rules/ExpirationIssuedAtValidationRuleTest.java renamed to core/common/lib/token-lib/src/test/java/org/eclipse/edc/token/rules/ExpirationIssuedAtValidationRuleTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
*
1313
*/
1414

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

1717
import org.eclipse.edc.spi.iam.ClaimToken;
18-
import org.eclipse.edc.token.rules.ExpirationIssuedAtValidationRule;
1918
import org.eclipse.edc.token.spi.TokenValidationRule;
2019
import org.junit.jupiter.api.Test;
2120

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) 2025 Metaform Systems, Inc.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Metaform Systems, Inc. - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.token.rules;
16+
17+
import org.eclipse.edc.spi.iam.ClaimToken;
18+
import org.junit.jupiter.api.Test;
19+
20+
import static org.junit.jupiter.api.Assertions.assertFalse;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
23+
class IssuerEqualsValidationRuleTest {
24+
25+
@Test
26+
void checkRule_successWhenIssuerMatches() {
27+
var token = createToken("issuer-a");
28+
29+
var rule = new IssuerEqualsValidationRule("issuer-a");
30+
var result = rule.checkRule(token, null);
31+
32+
assertTrue(result.succeeded());
33+
}
34+
35+
@Test
36+
void checkRule_failureWhenIssuerDiffers() {
37+
var token = createToken("issuer-b");
38+
39+
var rule = new IssuerEqualsValidationRule("issuer-a");
40+
var result = rule.checkRule(token, null);
41+
42+
assertFalse(result.succeeded());
43+
// failure message contains mismatch details
44+
assertTrue(result.getFailureMessages().stream().anyMatch(m -> m.contains("does not match expected issuer")));
45+
}
46+
47+
@Test
48+
void checkRule_failureWhenIssuerNullButExpectedNotNull() {
49+
var token = createToken(null);
50+
51+
var rule = new IssuerEqualsValidationRule("issuer-a");
52+
var result = rule.checkRule(token, null);
53+
54+
assertFalse(result.succeeded());
55+
}
56+
57+
@Test
58+
void checkRule_successWhenBothNull() {
59+
var token = createToken(null);
60+
61+
var rule = new IssuerEqualsValidationRule(null);
62+
var result = rule.checkRule(token, null);
63+
64+
assertTrue(result.succeeded());
65+
}
66+
67+
private ClaimToken createToken(String issuer) {
68+
return ClaimToken.Builder.newInstance().claim("iss", issuer).build();
69+
}
70+
}

core/common/token-core/src/test/java/org/eclipse/edc/jwt/rules/NotBeforeValidationRuleTest.java renamed to core/common/lib/token-lib/src/test/java/org/eclipse/edc/token/rules/NotBeforeValidationRuleTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
2+
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
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
@@ -12,10 +12,9 @@
1212
*
1313
*/
1414

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

1717
import org.eclipse.edc.spi.iam.ClaimToken;
18-
import org.eclipse.edc.token.rules.NotBeforeValidationRule;
1918
import org.eclipse.edc.token.spi.TokenValidationRule;
2019
import org.junit.jupiter.api.Test;
2120

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2025 Metaform Systems, Inc.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Metaform Systems, Inc. - initial API and implementation
12+
*
13+
*/
14+
15+
plugins {
16+
`java-library`
17+
`maven-publish`
18+
}
19+
20+
dependencies {
21+
api(project(":spi:common:core-spi"))
22+
api(project(":spi:common:token-spi"))
23+
api(project(":spi:common:auth-spi"))
24+
api(project(":spi:common:connector-participant-context-spi"))
25+
26+
27+
implementation(project(":spi:common:web-spi"))
28+
implementation(project(":core:common:lib:util-lib"))
29+
30+
31+
implementation(libs.jakarta.rsApi)
32+
implementation(libs.jakarta.annotation)
33+
implementation(libs.nimbus.jwt)
34+
35+
testImplementation(project(":core:common:junit"))
36+
testRuntimeOnly(libs.jersey.common) // needs the RuntimeDelegate
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2025 Metaform Systems, Inc.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Metaform Systems, Inc. - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.virtualized.api.authentication;
16+
17+
import com.nimbusds.jose.jwk.JWKSet;
18+
import org.eclipse.edc.keys.spi.KeyParserRegistry;
19+
import org.eclipse.edc.keys.spi.PublicKeyResolver;
20+
import org.eclipse.edc.spi.EdcException;
21+
import org.eclipse.edc.spi.result.Result;
22+
import org.eclipse.edc.util.collection.Cache;
23+
24+
import java.net.URL;
25+
import java.security.PublicKey;
26+
27+
public class JwksResolver implements PublicKeyResolver {
28+
private final Cache<URL, JWKSet> jwksCache;
29+
private final URL jwksUrl;
30+
private final KeyParserRegistry keyParserRegistry;
31+
32+
public JwksResolver(URL jwksUrl, KeyParserRegistry keyParserRegistry, long cacheExpirationInMillis) {
33+
this.jwksUrl = jwksUrl;
34+
this.keyParserRegistry = keyParserRegistry;
35+
jwksCache = new Cache<>(this::loadJwks, cacheExpirationInMillis);
36+
}
37+
38+
@Override
39+
public Result<PublicKey> resolveKey(String kid) {
40+
var jwks = jwksCache.get(jwksUrl);
41+
var jwk = jwks.getKeyByKeyId(kid);
42+
if (jwk == null) {
43+
return Result.failure("JWK not found for kid: " + kid);
44+
}
45+
return keyParserRegistry.parsePublic(jwk.toJSONString());
46+
}
47+
48+
private JWKSet loadJwks(URL jwksUrl) {
49+
try {
50+
return JWKSet.load(jwksUrl);
51+
} catch (Exception e) {
52+
throw new EdcException(e);
53+
}
54+
}
55+
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) 2025 Metaform Systems, Inc.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Metaform Systems, Inc. - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.virtualized.api.authentication.filter;
16+
17+
public interface Constants {
18+
String REQUEST_PROPERTY_CLAIMS = "claims";
19+
String TOKEN_CLAIM_ROLE = "role";
20+
String TOKEN_CLAIM_SCOPE = "scope";
21+
String TOKEN_CLAIM_PARTICIPANT_CONTEXT_ID = "participant_context_id";
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (c) 2025 Metaform Systems, Inc.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Metaform Systems, Inc. - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.virtualized.api.authentication.filter;
16+
17+
import jakarta.annotation.Priority;
18+
import jakarta.ws.rs.Priorities;
19+
import jakarta.ws.rs.container.ContainerRequestContext;
20+
import jakarta.ws.rs.container.ContainerRequestFilter;
21+
import jakarta.ws.rs.container.PreMatching;
22+
import jakarta.ws.rs.core.Response;
23+
import org.eclipse.edc.keys.spi.PublicKeyResolver;
24+
import org.eclipse.edc.token.spi.TokenValidationRule;
25+
import org.eclipse.edc.token.spi.TokenValidationService;
26+
27+
import java.util.List;
28+
29+
import static org.eclipse.edc.virtualized.api.authentication.filter.Constants.REQUEST_PROPERTY_CLAIMS;
30+
31+
/**
32+
* Validates the JWT signature against the IdP's public key and validates basic claims, such as {@code iss} and {@code exp}.
33+
*/
34+
@PreMatching
35+
@Priority(Priorities.AUTHENTICATION)
36+
class JwtValidatorFilter implements ContainerRequestFilter {
37+
38+
private final TokenValidationService tokenValidationService;
39+
private final PublicKeyResolver publicKeyResolver;
40+
private final List<TokenValidationRule> rules;
41+
42+
JwtValidatorFilter(TokenValidationService tokenValidationService, PublicKeyResolver publicKeyResolver, List<TokenValidationRule> rules) {
43+
this.publicKeyResolver = publicKeyResolver;
44+
this.rules = rules;
45+
this.tokenValidationService = tokenValidationService;
46+
}
47+
48+
49+
@Override
50+
public void filter(ContainerRequestContext requestContext) {
51+
var authHeader = requestContext.getHeaderString("Authorization");
52+
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
53+
abort(requestContext, "Missing Authorization header");
54+
return;
55+
}
56+
57+
var token = authHeader.substring("Bearer ".length()).trim();
58+
59+
var tokenValidationResult = tokenValidationService.validate(token, publicKeyResolver, rules);
60+
61+
if (tokenValidationResult.failed()) {
62+
abort(requestContext, tokenValidationResult.getFailureDetail());
63+
return;
64+
}
65+
66+
requestContext.setProperty(REQUEST_PROPERTY_CLAIMS, tokenValidationResult.getContent());
67+
}
68+
69+
70+
private void abort(ContainerRequestContext ctx, String message) {
71+
ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED)
72+
.entity("{\"error\":\"" + message + "\"}")
73+
.build());
74+
}
75+
}

0 commit comments

Comments
 (0)