diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0a9d938 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,11 @@ +# This file provides an overview of responsibilities in this repository. + +# Please note that this file does not represent all contributions to the code. What persons and organizations +# actually contributed to each file can be seen on GitHub and is documented in the license headers. + +# Linked persons with write permissions are automatically added as reviewers when a pull request is opened. + +# Each line is a file pattern followed by one or more contact persons. The last matching pattern has the most precedence. +# For more details, read the following article on GitHub: https://help.github.com/articles/about-codeowners/. + +* @eclipse-dataplane-core/technology-dataplane-core-committers diff --git a/dataplane-sdk-core/src/main/java/org/eclipse/dataplane/domain/registration/Oauth2ClientCredentialsAuthorization.java b/dataplane-sdk-core/src/main/java/org/eclipse/dataplane/domain/registration/Oauth2ClientCredentialsAuthorization.java index cade64b..9e855ec 100644 --- a/dataplane-sdk-core/src/main/java/org/eclipse/dataplane/domain/registration/Oauth2ClientCredentialsAuthorization.java +++ b/dataplane-sdk-core/src/main/java/org/eclipse/dataplane/domain/registration/Oauth2ClientCredentialsAuthorization.java @@ -15,7 +15,8 @@ package org.eclipse.dataplane.domain.registration; import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jose.jwk.source.JWKSourceBuilder; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; import org.eclipse.dataplane.domain.Result; import java.net.URI; @@ -25,14 +26,21 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; +import static com.nimbusds.jose.proc.JWSAlgorithmFamilyJWSKeySelector.fromJWKSource; import static jakarta.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; public class Oauth2ClientCredentialsAuthorization implements Authorization { private final HttpClient httpClient = HttpClient.newHttpClient(); private final ObjectMapper objectMapper = new ObjectMapper(); + private final String jwksUri; + + public Oauth2ClientCredentialsAuthorization(String jwksUri) { + this.jwksUri = Objects.requireNonNull(jwksUri, "jwksUri is required for JWT verification"); + } @Override public String type() { @@ -75,12 +83,16 @@ public Result authorizationHeader(AuthorizationProfile profile) { public Result extractCallerId(String authorizationHeader) { try { var token = authorizationHeader.substring("Bearer ".length()); - var jwt = SignedJWT.parse(token); - var sub = jwt.getJWTClaimsSet().getClaims().get("sub"); - if (sub instanceof String callerId) { - return Result.success(callerId); + + var jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSKeySelector(fromJWKSource(JWKSourceBuilder.create(URI.create(jwksUri).toURL()).build())); + + var claimsSet = jwtProcessor.process(token, null); + var sub = claimsSet.getSubject(); + if (sub == null) { + return Result.failure(new RuntimeException("JWT missing sub claim")); } - return Result.failure(new RuntimeException("JWT sub claim %s is not a string".formatted(sub))); + return Result.success(sub); } catch (Exception e) { return Result.failure(e); } diff --git a/e2e-tests/src/test/java/org/eclipse/dataplane/scenario/AuthorizationOauth2Test.java b/e2e-tests/src/test/java/org/eclipse/dataplane/scenario/AuthorizationOauth2Test.java index 722a59b..fe01ac4 100644 --- a/e2e-tests/src/test/java/org/eclipse/dataplane/scenario/AuthorizationOauth2Test.java +++ b/e2e-tests/src/test/java/org/eclipse/dataplane/scenario/AuthorizationOauth2Test.java @@ -18,11 +18,15 @@ import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -53,24 +57,31 @@ public class AuthorizationOauth2Test { private final HttpServer httpServer = new HttpServer(); - private final Oauth2ClientCredentialsAuthorization oauth2ClientCredentialsAuthorization = new Oauth2ClientCredentialsAuthorization(); + private Oauth2ClientCredentialsAuthorization oauth2ClientCredentialsAuthorization; private ControlPlane controlPlane; - private final Dataplane dataPlane = Dataplane.newInstance() - .id("data-plane") - .registerAuthorization(oauth2ClientCredentialsAuthorization) - .onPrepare(dataFlow -> { - dataFlow.transitionToPreparing(); - return Result.success(dataFlow); - }) - .build(); + private Dataplane dataPlane; + private Oauth2TokenController tokenController; private final String clientId = UUID.randomUUID().toString(); private final String clientSecret = UUID.randomUUID().toString(); @BeforeEach - void setUp() { + void setUp() throws JOSEException { httpServer.start(); + tokenController = new Oauth2TokenController(clientId, clientSecret); + var jwksUri = "http://localhost:" + httpServer.port() + "/oauth2/jwks"; + oauth2ClientCredentialsAuthorization = new Oauth2ClientCredentialsAuthorization(jwksUri); + + dataPlane = Dataplane.newInstance() + .id("data-plane") + .registerAuthorization(oauth2ClientCredentialsAuthorization) + .onPrepare(dataFlow -> { + dataFlow.transitionToPreparing(); + return Result.success(dataFlow); + }) + .build(); + controlPlane = ControlPlane.newInstance() .authorizationTokenGenerator(() -> oauth2ClientCredentialsAuthorization.authorizationHeader(oauth2AuthorizationProfile())) .build(); @@ -78,7 +89,7 @@ void setUp() { controlPlane.initialize(httpServer, "/data-plane", "/data-plane"); httpServer.deploy("/data-plane", new DataPlaneSignalingApiController(dataPlane)); - httpServer.deploy("/oauth2", new Oauth2TokenController(clientId, clientSecret)); + httpServer.deploy("/oauth2", tokenController); } @AfterEach @@ -126,13 +137,14 @@ private AuthorizationProfile oauth2AuthorizationProfile() { @Path("/") public static class Oauth2TokenController { - private final String clientId; private final String clientSecret; + private final RSAKey rsaKey; - public Oauth2TokenController(String clientId, String clientSecret) { + public Oauth2TokenController(String clientId, String clientSecret) throws JOSEException { this.clientId = clientId; this.clientSecret = clientSecret; + this.rsaKey = new RSAKeyGenerator(2048).keyID("key-1").generate(); } @POST @@ -153,29 +165,35 @@ public Response token( return Response.ok(responseBody).build(); } + @GET + @Path("/jwks") + @Produces(APPLICATION_JSON) + public Response jwks() { + var jwkSet = new JWKSet(rsaKey.toPublicJWK()); + return Response.ok(jwkSet.toJSONObject()).build(); + } + public String issueJwt(String sub) { var now = new Date(); var claimsSet = new JWTClaimsSet.Builder() .subject(sub) .issuer("https://your-app.com") - .audience("https://api.your-app.com") - .expirationTime(new Date(now.getTime() + 1000)) + .expirationTime(new Date(now.getTime() + 60_000)) .notBeforeTime(now) .issueTime(now) .jwtID(UUID.randomUUID().toString()) .build(); - var header = new JWSHeader.Builder(JWSAlgorithm.HS256) + var header = new JWSHeader.Builder(JWSAlgorithm.RS256) + .keyID(rsaKey.getKeyID()) .type(JOSEObjectType.JWT) .build(); var signedJwt = new SignedJWT(header, claimsSet); - var secret = "random-256-bit-secret-" + UUID.randomUUID(); try { - var signer = new MACSigner(secret.getBytes()); - signedJwt.sign(signer); + signedJwt.sign(new RSASSASigner(rsaKey)); } catch (JOSEException e) { throw new RuntimeException(e); }