Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -75,12 +83,16 @@ public Result<String> authorizationHeader(AuthorizationProfile profile) {
public Result<String> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,32 +57,39 @@
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();

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
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
Expand Down