Skip to content

Commit

Permalink
Generate RSA-256 keys on dev mode
Browse files Browse the repository at this point in the history
  • Loading branch information
mcruzdev committed Nov 4, 2024
1 parent 0fbfc31 commit d539007
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 2 deletions.
16 changes: 16 additions & 0 deletions docs/src/main/asciidoc/security-jwt.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,22 @@ Please see the xref:security-openid-connect-client-reference.adoc#token-propagat
[[integration-testing]]
=== Testing

[[dev-mode]]
==== {extension-name} Dev Mode

If you do not want to generate and configure a public and private key pair, the {extension-name} extension automatically provides an RSA-256 key pair for you in Dev Mode.

You can disable this feature by setting at least one of the following properties:

* `mp.jwt.verify.publickey.location`
* `mp.jwt.verify.publickey`
* `smallrye.jwt.sign.key.location`
* `smallrye.jwt.sign.key`

[NOTE]

Additionally, if you do not provide the issuer information (set by the `mp.jwt.verify.issuer property`), the {extension-name} extension provides a default issuer called `https://quarkus.io/issuer` for you.

[[integration-testing-wiremock]]
==== Wiremock

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@
/**
* The deployment processor for MP-JWT applications
*/
class SmallRyeJwtProcessor {
public class SmallRyeJwtProcessor {

private static final Logger log = Logger.getLogger(SmallRyeJwtProcessor.class.getName());

private static final String MP_JWT_VERIFY_KEY_LOCATION = "mp.jwt.verify.publickey.location";
public static final String MP_JWT_VERIFY_KEY_LOCATION = "mp.jwt.verify.publickey.location";
private static final String MP_JWT_DECRYPT_KEY_LOCATION = "mp.jwt.decrypt.key.location";

private static final DotName CLAIM_NAME = DotName.createSimple(Claim.class.getName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package io.quarkus.smallrye.jwt.deployment.dev;

import static io.quarkus.smallrye.jwt.deployment.SmallRyeJwtProcessor.MP_JWT_VERIFY_KEY_LOCATION;

import java.security.Key;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;

import io.quarkus.deployment.Feature;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.DevServicesResultBuildItem;
import io.quarkus.deployment.builditem.LiveReloadBuildItem;
import io.quarkus.runtime.LaunchMode;
import io.smallrye.jwt.util.KeyUtils;

public class SmallryeJwtDevServiceProcessor {

private static final String MP_JWT_VERIFY_PUBLIC_KEY = "mp.jwt.verify.publickey";
private static final String MP_JWT_VERIFY_ISSUER = "mp.jwt.verify.issuer";

private static final String SMALLRYE_JWT_SIGN_KEY_LOCATION = "smallrye.jwt.sign.key.location";
private static final String SMALLRYE_JWT_SIGN_KEY = "smallrye.jwt.sign.key";

private static final String DEFAULT_DEV_MP_JWT_VERIFY_ISSUER = "https://quarkus.io/issuer";

private static final int KEY_SIZE = 2048;

private static final Set<String> JWT_SIGN_KEY_PROPERTIES = Set.of(
MP_JWT_VERIFY_KEY_LOCATION,
MP_JWT_VERIFY_PUBLIC_KEY,
SMALLRYE_JWT_SIGN_KEY_LOCATION,
SMALLRYE_JWT_SIGN_KEY);

@BuildStep(onlyIf = { IsDevelopmentOrTest.class })
void generateSignKeys(BuildProducer<DevServicesResultBuildItem> devServices,
LiveReloadBuildItem liveReloadBuildItem) throws NoSuchAlgorithmException {

Config configProvider = ConfigProvider.getConfig();
List<String> configuredKeyProperties = JWT_SIGN_KEY_PROPERTIES.stream()
.map(prop -> configProvider.getOptionalValue(prop, String.class).orElse(""))
.filter(Predicate.not(String::isBlank))
.toList();

if (!configuredKeyProperties.isEmpty()) {
return;
}

String issuer = configProvider.getOptionalValue(MP_JWT_VERIFY_ISSUER, String.class)
.orElse(DEFAULT_DEV_MP_JWT_VERIFY_ISSUER);

if (!liveReloadBuildItem.isLiveReload()) {
// first execution
KeyPair keyPair = KeyUtils.generateKeyPair(KEY_SIZE);
String publicKey = getStringKey(keyPair.getPublic());
String privateKey = getStringKey(keyPair.getPrivate());

Map<String, String> properties = generateDevServiceProperties(publicKey, privateKey, issuer);

devServices.produce(smallryeJwtDevServiceWith(properties));
}
}

private DevServicesResultBuildItem smallryeJwtDevServiceWith(Map<String, String> properties) {
return new DevServicesResultBuildItem(
Feature.SMALLRYE_JWT.name(), null, properties);
}

private static Map<String, String> generateDevServiceProperties(String publicKey, String privateKey, String issuer) {
return Map.of(
MP_JWT_VERIFY_PUBLIC_KEY, publicKey,
SMALLRYE_JWT_SIGN_KEY, privateKey,
MP_JWT_VERIFY_ISSUER, issuer);
}

private static String getStringKey(Key key) {
return Base64.getEncoder()
.encodeToString(key.getEncoded());
}

static class IsDevelopmentOrTest implements BooleanSupplier {

private final LaunchMode launchMode;

public IsDevelopmentOrTest(LaunchMode launchMode) {
this.launchMode = launchMode;
}

@Override
public boolean getAsBoolean() {
return this.launchMode == LaunchMode.TEST || this.launchMode == LaunchMode.DEVELOPMENT;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.jwt.test;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/only-user")
public class GreetingResource {

@GET
@Produces(MediaType.TEXT_PLAIN)
@RolesAllowed({ "User" })
public String hello() {
return "Hello from Quarkus REST";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.quarkus.jwt.test.dev;

import jakarta.annotation.security.PermitAll;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import org.eclipse.microprofile.jwt.Claims;
import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.jwt.test.GreetingResource;
import io.quarkus.test.QuarkusDevModeTest;
import io.restassured.RestAssured;
import io.restassured.http.Header;
import io.smallrye.jwt.build.Jwt;

public class SmallryeJwtProcessorDevServiceProcessorTest {

@RegisterExtension
static QuarkusDevModeTest devMode = new QuarkusDevModeTest()
.withApplicationRoot((jar) -> jar.addClasses(
GreetingResource.class, TokenResource.class).addAsResource(
new StringAsset(""), "application.properties"));

@Test
public void shouldNotBeNecessaryToAddSignKeysOnApplicationProperties() {
String token = RestAssured.given()
.header(new Header("Accept", "text/plain"))
.get("/token")
.andReturn()
.body()
.asString();

RestAssured.given()
.header(new Header("Authorization", "Bearer " + token))
.get("/only-user")
.then().assertThat().statusCode(200);
}

@Test
public void shouldUseTheSameKeyPairOnLiveReload() {
String token = RestAssured.given()
.header(new Header("Accept", "text/plain"))
.get("/token")
.andReturn()
.body()
.asString();

devMode.modifySourceFile("GreetingResource.java", s -> s.replace("Hello from Quarkus", "Hello from JWT"));

// there is no need to get another token
RestAssured.given()
.header(new Header("Authorization", "Bearer " + token))
.get("/only-user")
.then().assertThat().statusCode(200)
.body(Matchers.containsString("Hello from JWT"));
}

@Path("/token")
static class TokenResource {

@GET
@Produces(MediaType.TEXT_PLAIN)
@PermitAll
public String hello() {
return Jwt.upn("[email protected]")
.issuer("https://quarkus.io/issuer")
.groups("User")
.claim(Claims.birthdate.name(), "2001-07-13")
.sign();
}
}
}

0 comments on commit d539007

Please sign in to comment.