Skip to content

Commit 5464d37

Browse files
authored
feat: introduce oauth mock test extension (#5403)
1 parent 1810922 commit 5464d37

File tree

3 files changed

+269
-0
lines changed

3 files changed

+269
-0
lines changed

extensions/common/auth/auth-authentication-oauth2-lib/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
plugins {
1616
`java-library`
1717
`maven-publish`
18+
`java-test-fixtures`
1819
}
1920

2021
dependencies {
@@ -34,4 +35,10 @@ dependencies {
3435

3536
testImplementation(project(":core:common:junit"))
3637
testRuntimeOnly(libs.jersey.common) // needs the RuntimeDelegate
38+
39+
testFixturesImplementation(libs.wiremock) {
40+
exclude("com.networknt", "json-schema-validator")
41+
}
42+
testFixturesImplementation(libs.jakarta.json.api)
43+
testFixturesImplementation(libs.nimbus.jwt)
3744
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.api.authentication;
16+
17+
import com.nimbusds.jose.JWSHeader;
18+
import com.nimbusds.jose.crypto.ECDSASigner;
19+
import com.nimbusds.jose.jwk.ECKey;
20+
import com.nimbusds.jwt.JWTClaimsSet;
21+
import com.nimbusds.jwt.SignedJWT;
22+
import org.eclipse.edc.api.auth.spi.ParticipantPrincipal;
23+
24+
import java.time.Instant;
25+
import java.util.HashMap;
26+
import java.util.Map;
27+
import java.util.UUID;
28+
29+
import static com.nimbusds.jose.JWSAlgorithm.ES256;
30+
31+
public class OauthServer {
32+
33+
private final ECKey oauthServerSigningKey;
34+
private final String issuer;
35+
private final String scopes;
36+
37+
public OauthServer(ECKey oauthServerSigningKey, String issuer, String scopes) {
38+
this.oauthServerSigningKey = oauthServerSigningKey;
39+
this.issuer = issuer;
40+
this.scopes = scopes;
41+
}
42+
43+
public String createToken(String participantContextId) {
44+
return createToken(participantContextId, Map.of());
45+
}
46+
47+
public String createToken(String participantContextId, String role) {
48+
return createToken(participantContextId, Map.of(), scopes, role);
49+
}
50+
51+
public String createToken(String participantContextId, String scopes, String role) {
52+
return createToken(participantContextId, Map.of(), scopes, role);
53+
}
54+
55+
public String createToken(String participantContextId, Map<String, String> additionalClaims) {
56+
return createToken(participantContextId, additionalClaims, scopes, ParticipantPrincipal.ROLE_PARTICIPANT);
57+
}
58+
59+
public String createToken(String participantContextId, Map<String, String> additionalClaims, String scopes, String role) {
60+
var defaultClaims = new HashMap<String, Object>(Map.of(
61+
"sub", "test-subject",
62+
"iss", issuer,
63+
"iat", Instant.now().getEpochSecond(),
64+
"exp", Instant.now().plusSeconds(3600).getEpochSecond(),
65+
"jti", UUID.randomUUID().toString(),
66+
"scope", scopes,
67+
"role", role
68+
));
69+
if (participantContextId != null) {
70+
defaultClaims.put("participant_context_id", participantContextId);
71+
}
72+
defaultClaims.putAll(additionalClaims);
73+
return createToken(defaultClaims);
74+
}
75+
76+
public String createToken(Map<String, Object> claims) {
77+
try {
78+
var claimsBuilder = new JWTClaimsSet.Builder();
79+
claims.forEach(claimsBuilder::claim);
80+
var hdr = new JWSHeader.Builder(ES256).keyID(oauthServerSigningKey.getKeyID()).build();
81+
var jwt = new SignedJWT(hdr, claimsBuilder.build());
82+
var signer = new ECDSASigner(oauthServerSigningKey);
83+
jwt.sign(signer);
84+
return jwt.serialize();
85+
} catch (Exception e) {
86+
throw new RuntimeException(e);
87+
}
88+
}
89+
90+
public String createAdminToken() {
91+
return createToken(null, ParticipantPrincipal.ROLE_ADMIN);
92+
}
93+
94+
public String createProvisionerToken() {
95+
return createToken(null, ParticipantPrincipal.ROLE_PROVISIONER);
96+
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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.api.authentication;
16+
17+
import com.github.tomakehurst.wiremock.WireMockServer;
18+
import com.nimbusds.jose.JOSEException;
19+
import com.nimbusds.jose.jwk.Curve;
20+
import com.nimbusds.jose.jwk.ECKey;
21+
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
22+
import org.eclipse.edc.spi.system.configuration.Config;
23+
import org.eclipse.edc.spi.system.configuration.ConfigFactory;
24+
import org.jspecify.annotations.Nullable;
25+
import org.junit.jupiter.api.extension.AfterAllCallback;
26+
import org.junit.jupiter.api.extension.BeforeAllCallback;
27+
import org.junit.jupiter.api.extension.ExtensionContext;
28+
import org.junit.jupiter.api.extension.ParameterContext;
29+
import org.junit.jupiter.api.extension.ParameterResolutionException;
30+
import org.junit.jupiter.api.extension.ParameterResolver;
31+
32+
import java.util.Map;
33+
import java.util.Objects;
34+
import java.util.UUID;
35+
36+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
37+
import static com.github.tomakehurst.wiremock.client.WireMock.any;
38+
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
39+
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
40+
import static jakarta.json.Json.createArrayBuilder;
41+
import static jakarta.json.Json.createObjectBuilder;
42+
43+
/**
44+
* JUnit extension that sets up a mock OAuth2 authorization server with a JWKS endpoint.
45+
* Provides an AuthServer instance for use in tests.
46+
*/
47+
public class OauthServerEndToEndExtension implements BeforeAllCallback, AfterAllCallback, ParameterResolver {
48+
49+
private WireMockServer wireMockServer;
50+
private OauthServer authServer;
51+
private ECKey key;
52+
private String issuer;
53+
private String signingKeyId;
54+
private String scopes = "management-api:read management-api:write";
55+
56+
private OauthServerEndToEndExtension() {
57+
}
58+
59+
private ECKey generateEcKey(String signingKeyId) {
60+
try {
61+
return new ECKeyGenerator(Curve.P_256).keyID(signingKeyId)
62+
.generate();
63+
} catch (JOSEException e) {
64+
throw new RuntimeException(e);
65+
}
66+
}
67+
68+
@Override
69+
public void afterAll(ExtensionContext context) throws Exception {
70+
wireMockServer.stop();
71+
}
72+
73+
public OauthServer getAuthServer() {
74+
return authServer;
75+
}
76+
77+
@Override
78+
public void beforeAll(ExtensionContext context) throws Exception {
79+
// create JWKS with the participant's key
80+
var jwks = createObjectBuilder()
81+
.add("keys", createArrayBuilder().add(createObjectBuilder(
82+
key.toPublicJWK().toJSONObject())))
83+
.build()
84+
.toString();
85+
86+
// use wiremock to host a JWKS endpoint
87+
wireMockServer.stubFor(any(urlPathEqualTo("/.well-known/jwks"))
88+
.willReturn(aResponse()
89+
.withStatus(200)
90+
.withHeader("Content-Type", "application/json")
91+
.withBody(jwks)));
92+
wireMockServer.start();
93+
}
94+
95+
@Override
96+
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
97+
return parameterContext.getParameter().getType().equals(OauthServer.class);
98+
}
99+
100+
@Override
101+
public @Nullable Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
102+
if (parameterContext.getParameter().getType().equals(OauthServer.class)) {
103+
return authServer;
104+
}
105+
return null;
106+
}
107+
108+
public String getJwksUrl() {
109+
return wireMockServer.baseUrl() + "/.well-known/jwks";
110+
}
111+
112+
public Config getConfig() {
113+
return ConfigFactory.fromMap(Map.of(
114+
"edc.iam.oauth2.issuer", issuer,
115+
"edc.iam.oauth2.jwks.url", getJwksUrl()));
116+
}
117+
118+
public static class Builder {
119+
120+
private final OauthServerEndToEndExtension ext;
121+
122+
private Builder() {
123+
ext = new OauthServerEndToEndExtension();
124+
}
125+
126+
public static Builder newInstance() {
127+
return new Builder();
128+
}
129+
130+
public Builder issuer(String issuer) {
131+
ext.issuer = issuer;
132+
return this;
133+
}
134+
135+
public Builder signingKeyId(String signingKeyId) {
136+
ext.signingKeyId = signingKeyId;
137+
return this;
138+
}
139+
140+
public Builder key(ECKey key) {
141+
ext.key = key;
142+
return this;
143+
}
144+
145+
public Builder wireMockServer(WireMockServer server) {
146+
ext.wireMockServer = server;
147+
return this;
148+
}
149+
150+
public Builder scopes(String scopes) {
151+
ext.scopes = scopes;
152+
return this;
153+
}
154+
155+
public OauthServerEndToEndExtension build() {
156+
ext.wireMockServer = Objects.requireNonNullElseGet(ext.wireMockServer, () -> new WireMockServer(wireMockConfig().dynamicPort()));
157+
ext.issuer = Objects.requireNonNullElseGet(ext.issuer, () -> "test-issuer");
158+
ext.signingKeyId = Objects.requireNonNullElseGet(ext.signingKeyId, () -> UUID.randomUUID().toString());
159+
ext.key = Objects.requireNonNullElseGet(ext.key, () -> ext.generateEcKey(ext.signingKeyId));
160+
ext.authServer = new OauthServer(ext.key, ext.issuer, ext.scopes);
161+
return ext;
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)