Skip to content

Commit

Permalink
feat: jwks extension
Browse files Browse the repository at this point in the history
  • Loading branch information
efiege committed Dec 4, 2023
1 parent 5bbc05d commit 3397c23
Show file tree
Hide file tree
Showing 17 changed files with 548 additions and 45 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md).

#### Minor Changes

- Added `JWKS-Extension`, which provides an endpoint in the default API, that returns the JWKS of the connector.

#### Patch Changes

- Improved `:extensions:wrapper:wrapper-common-mappers` for broker: `AssetJsonLdUtils`, made some methods public.
Expand Down
34 changes: 34 additions & 0 deletions extensions/jwks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!-- PROJECT LOGO -->
<br />
<div align="center">
<a href="https://github.com/sovity/edc-extensions">
<img src="https://raw.githubusercontent.com/sovity/edc-ui/main/src/assets/images/sovity_logo.svg" alt="Logo" width="300">
</a>

<h3 align="center">EDC-Connector Extension:<br />Last Commit Info</h3>

<p align="center">
<a href="https://github.com/sovity/edc-extensions/issues/new?template=bug_report.md">Report Bug</a>
·
<a href="https://github.com/sovity/edc-extensions/issues/new?template=feature_request.md">Request Feature</a>
</p>
</div>

## About this Extension
The JWKS-Extension provides an endpoint in the default API of the EDC-Connector, that returns the JWKS of the connector.
It can be accessed using the `:{WEB_HTTP_PORT}/{WEB_HTTP_PATH}/jwks` (default: `:11001/api/jwks`) endpoint.

## Why does this extension exist?
The JWKS-endpoint can be used to validate tokens issued by the EDC-Connector. This can be particular useful for the DAPS.

## Configuration

### X509 Secret Alias
The alias of the pem-encoded X509-certificate stored in the `Vault` is determined
by the `edc.transfer.proxy.token.verifier.publickey.alias` property.

## License
Apache License 2.0 - see [LICENSE](../../LICENSE)

## Contact
sovity GmbH - [email protected]
3 changes: 3 additions & 0 deletions extensions/jwks/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ val mockitoVersion: String by project
val lombokVersion: String by project
val jettyVersion: String by project
val jettyGroup: String by project
val nimbusJoseJwtVersion: String by project

plugins {
`java-library`
`maven-publish`
}

dependencies {
implementation("com.nimbusds:nimbus-jose-jwt:${nimbusJoseJwtVersion}")

annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
compileOnly("org.projectlombok:lombok:${lombokVersion}")

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,25 @@

package de.sovity.edc.extension.jwks;

import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration;
import de.sovity.edc.extension.jwks.controller.JwksController;
import de.sovity.edc.extension.jwks.controller.JwksJsonTransformerImpl;
import de.sovity.edc.extension.jwks.jwk.VaultJwkFactoryImpl;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.web.spi.WebService;


public class JwksExtension implements ServiceExtension {

public static final String EXTENSION_NAME = "JwksExtension";
public static final String TOKEN_VERIFIER_PUBLIC_KEY_ALIAS =
"edc.transfer.proxy.token.verifier.publickey.alias";
@Inject
private WebService webService;
@Inject
private Vault vault;

@Override
public String name() {
Expand All @@ -33,7 +41,17 @@ public String name() {

@Override
public void initialize(ServiceExtensionContext context) {
var controller = new JwksController();
var monitor = context.getMonitor();
var pemSecretAlias = context.getSetting(TOKEN_VERIFIER_PUBLIC_KEY_ALIAS, null);
if (pemSecretAlias == null) {
monitor.warning(() -> "No vault alias provided for JWKS-Extension");
}
var controller = new JwksController(
new VaultJwkFactoryImpl(vault),
new JwksJsonTransformerImpl(),
pemSecretAlias,
monitor);
webService.registerResource(controller);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2023 sovity GmbH
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* sovity GmbH - initial API and implementation
*
*/

package de.sovity.edc.extension.jwks.controller;

import de.sovity.edc.extension.jwks.JwksExtension;
import de.sovity.edc.extension.jwks.jwk.VaultJwkFactory;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.monitor.Monitor;

import java.util.Objects;

@Produces({MediaType.APPLICATION_JSON})
@Path(JwksController.JWKS_PATH)
public class JwksController {

static final String ALIAS_NOT_SET_MESSAGE = String.format(
"No alias for JWKS-Extension configured. Please set the %s property",
JwksExtension.TOKEN_VERIFIER_PUBLIC_KEY_ALIAS);
static final String JWKS_RESPONSE_FAILED_MESSAGE_TEMPLATE =
"Creating JWKS response failed: %s";
public static final String JWKS_PATH = "/jwks";
private final VaultJwkFactory vaultJkwFactory;
private final JwksJsonTransformer jwksJsonTransformer;
private final String pemSecretAlias;
private final Monitor monitor;

public JwksController(
VaultJwkFactory vaultJkwFactory,
JwksJsonTransformer jwksJsonTransformer,
String pemSecretAlias,
Monitor monitor) {
this.vaultJkwFactory = vaultJkwFactory;
this.jwksJsonTransformer = jwksJsonTransformer;
this.pemSecretAlias = pemSecretAlias;
this.monitor = monitor;
}

@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getJwks() {
try {
validateAliasSet(pemSecretAlias);
var jwk = vaultJkwFactory.publicX509JwkFromAlias(pemSecretAlias);
return Response
.ok(jwksJsonTransformer.toJwksJson(jwk), MediaType.APPLICATION_JSON)
.build();
} catch (EdcException e) {
monitor.warning(String.format(JWKS_RESPONSE_FAILED_MESSAGE_TEMPLATE, e.getMessage()));
return Response
.status(Response.Status.INTERNAL_SERVER_ERROR)
.build();
}
}

private void validateAliasSet(String pemSecretAlias) {
if (Objects.isNull(pemSecretAlias) || pemSecretAlias.isBlank()) {
throw new EdcException(ALIAS_NOT_SET_MESSAGE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.sovity.edc.extension.jwks.controller;

import com.nimbusds.jose.jwk.JWK;

public interface JwksJsonTransformer {
String toJwksJson(JWK jwk);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 sovity GmbH
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* sovity GmbH - initial API and implementation
*
*/

package de.sovity.edc.extension.jwks.controller;

import com.nimbusds.jose.jwk.JWK;
import jakarta.json.Json;

public class JwksJsonTransformerImpl implements JwksJsonTransformer {

@Override
public String toJwksJson(JWK jwk) {
var jwkJsonObject = Json.createObjectBuilder(jwk.toJSONObject());
var jwksJsonArray = Json.createArrayBuilder()
.add(jwkJsonObject)
.build();
return Json.createObjectBuilder()
.add("keys", jwksJsonArray)
.build().toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package de.sovity.edc.extension.jwks.jwk;

import com.nimbusds.jose.jwk.JWK;

public interface VaultJwkFactory {

String RESOLVE_ALIAS_FROM_VAULT_FAILED_MESSAGE =
"Could not resolve PEM-Encoded-X509-Certificate for alias %s";

String PARSE_VALUE_FROM_VAULT_FAILED_MESSAGE =
"Could not parse PEM-Encoded-X509-Certificate for alias %s, Reason: %s";

JWK publicX509JwkFromAlias(String alias);


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2023 sovity GmbH
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* sovity GmbH - initial API and implementation
*
*/

package de.sovity.edc.extension.jwks.jwk;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.JWK;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.security.Vault;

import java.util.Optional;

public class VaultJwkFactoryImpl implements VaultJwkFactory {

private final Vault vault;

public VaultJwkFactoryImpl(Vault vault) {
this.vault = vault;
}

@Override
public JWK publicX509JwkFromAlias(String alias) {
return Optional
.ofNullable(vault.resolveSecret(alias))
.map(pemString -> parseX509Cert(pemString, alias))
.orElseThrow(() -> new EdcException(String.format(RESOLVE_ALIAS_FROM_VAULT_FAILED_MESSAGE, alias)));
}

private JWK parseX509Cert(String pem, String alias) {
try {
return JWK.parseFromPEMEncodedX509Cert(pem);
} catch (JOSEException e) {
throw new EdcException(String.format(
PARSE_VALUE_FROM_VAULT_FAILED_MESSAGE,
alias,
e.getMessage()));
}
}

}
Loading

0 comments on commit 3397c23

Please sign in to comment.