Skip to content

Commit 6d3fb34

Browse files
authored
Merge pull request #48251 from FroMage/jwt-dev-keys-persistent
JWT dev mode keys: add two build items to customise
2 parents 7792f8a + 1babd41 commit 6d3fb34

File tree

7 files changed

+485
-8
lines changed

7 files changed

+485
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.quarkus.smallrye.jwt.deployment;
2+
3+
import io.quarkus.builder.item.SimpleBuildItem;
4+
5+
/**
6+
* Marker build item to enable encrypted dev/test jwt keys.
7+
*/
8+
public final class GenerateEncryptedDevModeJwtKeysBuildItem extends SimpleBuildItem {
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.quarkus.smallrye.jwt.deployment;
2+
3+
import io.quarkus.builder.item.SimpleBuildItem;
4+
5+
/**
6+
* Marker build item to enable restart-persistent jwt keys.
7+
*/
8+
public final class GeneratePersistentDevModeJwtKeysBuildItem extends SimpleBuildItem {
9+
}

extensions/smallrye-jwt/deployment/src/main/java/io/quarkus/smallrye/jwt/deployment/SmallryeJwtDevModeProcessor.java

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,49 @@
22

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

5+
import java.io.File;
6+
import java.io.FileWriter;
7+
import java.io.IOException;
8+
import java.nio.file.Path;
9+
import java.security.GeneralSecurityException;
510
import java.security.Key;
611
import java.security.KeyPair;
712
import java.security.NoSuchAlgorithmException;
813
import java.util.Base64;
14+
import java.util.Collection;
915
import java.util.HashMap;
1016
import java.util.Map;
17+
import java.util.Optional;
1118
import java.util.Set;
1219
import java.util.stream.Collectors;
1320

1421
import org.eclipse.microprofile.config.ConfigProvider;
1522
import org.jboss.logging.Logger;
1623

24+
import io.quarkus.bootstrap.workspace.ArtifactSources;
25+
import io.quarkus.bootstrap.workspace.SourceDir;
1726
import io.quarkus.deployment.Feature;
1827
import io.quarkus.deployment.IsNormal;
1928
import io.quarkus.deployment.annotations.BuildProducer;
2029
import io.quarkus.deployment.annotations.BuildStep;
2130
import io.quarkus.deployment.builditem.DevServicesResultBuildItem;
2231
import io.quarkus.deployment.builditem.LiveReloadBuildItem;
32+
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
2333
import io.smallrye.jwt.util.KeyUtils;
2434

2535
public class SmallryeJwtDevModeProcessor {
2636

2737
private static final Logger LOGGER = Logger.getLogger(SmallryeJwtDevModeProcessor.class);
2838

29-
private static final String MP_JWT_VERIFY_PUBLIC_KEY = "mp.jwt.verify.publickey";
39+
public static final String MP_JWT_VERIFY_PUBLIC_KEY = "mp.jwt.verify.publickey";
3040
private static final String MP_JWT_VERIFY_ISSUER = "mp.jwt.verify.issuer";
41+
private static final String SMALLRYE_JWT_DECRYPT_KEY = "smallrye.jwt.decrypt.key"; // no MP equivalent
3142
private static final String MP_JWT_DECRYPT_KEY_LOCATION = "mp.jwt.decrypt.key.location";
3243

3344
private static final String SMALLRYE_JWT_NEW_TOKEN_ISSUER = "smallrye.jwt.new-token.issuer";
3445
private static final String SMALLRYE_JWT_SIGN_KEY_LOCATION = "smallrye.jwt.sign.key.location";
35-
private static final String SMALLRYE_JWT_SIGN_KEY = "smallrye.jwt.sign.key";
46+
public static final String SMALLRYE_JWT_SIGN_KEY = "smallrye.jwt.sign.key";
47+
private static final String SMALLRYE_JWT_ENCRYPT_KEY = "smallrye.jwt.encrypt.key";
3648
private static final String SMALLRYE_JWT_ENCRYPT_KEY_LOCATION = "smallrye.jwt.encrypt.key.location";
3749

3850
private static final String NONE = "NONE";
@@ -43,10 +55,14 @@ public class SmallryeJwtDevModeProcessor {
4355
private static final Set<String> JWT_SIGN_KEY_PROPERTIES = Set.of(
4456
MP_JWT_VERIFY_KEY_LOCATION,
4557
MP_JWT_VERIFY_PUBLIC_KEY,
58+
SMALLRYE_JWT_DECRYPT_KEY,
4659
MP_JWT_DECRYPT_KEY_LOCATION,
4760
SMALLRYE_JWT_SIGN_KEY_LOCATION,
4861
SMALLRYE_JWT_SIGN_KEY,
62+
SMALLRYE_JWT_ENCRYPT_KEY,
4963
SMALLRYE_JWT_ENCRYPT_KEY_LOCATION);
64+
public static final String DEV_PRIVATE_KEY_PEM = "dev.privateKey.pem";
65+
public static final String DEV_PUBLIC_KEY_PEM = "dev.publicKey.pem";
5066

5167
/**
5268
* This build step generates an RSA-256 key pair for development and test modes.
@@ -58,10 +74,15 @@ public class SmallryeJwtDevModeProcessor {
5874
* this build step will add a default issuer, regardless of the above condition.
5975
*
6076
* @throws NoSuchAlgorithmException if RSA-256 key generation fails.
77+
* @throws IOException if persistent key storage fails
6178
*/
6279
@BuildStep(onlyIfNot = { IsNormal.class })
6380
void generateSignKeys(BuildProducer<DevServicesResultBuildItem> devServices,
64-
LiveReloadBuildItem liveReloadBuildItem) throws NoSuchAlgorithmException {
81+
LiveReloadBuildItem liveReloadBuildItem,
82+
CurateOutcomeBuildItem curateOutcomeBuildItem,
83+
Optional<GeneratePersistentDevModeJwtKeysBuildItem> generatePersistentDevModeJwtKeysBuildItem,
84+
Optional<GenerateEncryptedDevModeJwtKeysBuildItem> generateEncryptedDevModeJwtKeysBuildItem)
85+
throws GeneralSecurityException, IOException {
6586

6687
Set<String> userProps = JWT_SIGN_KEY_PROPERTIES
6788
.stream()
@@ -71,7 +92,8 @@ void generateSignKeys(BuildProducer<DevServicesResultBuildItem> devServices,
7192
if (!userProps.isEmpty()) {
7293
// If the user has set the property, we need to avoid adding or overriding it with the
7394
// smallrye default configuration
74-
Map<String, String> devServiceProps = addDefaultSmallryePropertiesIfMissing(userProps);
95+
Map<String, String> devServiceProps = addDefaultSmallryePropertiesIfMissing(userProps,
96+
generateEncryptedDevModeJwtKeysBuildItem);
7597

7698
if (!isConfigPresent(MP_JWT_VERIFY_ISSUER) && !isConfigPresent(SMALLRYE_JWT_NEW_TOKEN_ISSUER)) {
7799
devServiceProps.put(MP_JWT_VERIFY_ISSUER, DEFAULT_ISSUER);
@@ -88,11 +110,12 @@ void generateSignKeys(BuildProducer<DevServicesResultBuildItem> devServices,
88110
"Please ensure the correct keys/locations are set in production to avoid potential issues.");
89111
if (ctx == null && !liveReloadBuildItem.isLiveReload()) {
90112
// first execution
91-
KeyPair keyPair = KeyUtils.generateKeyPair(KEY_SIZE);
113+
KeyPair keyPair = generateOrReloadKeyPair(curateOutcomeBuildItem, generatePersistentDevModeJwtKeysBuildItem);
92114
String publicKey = getStringKey(keyPair.getPublic());
93115
String privateKey = getStringKey(keyPair.getPrivate());
94116

95-
Map<String, String> devServiceProps = generateDevServiceProperties(publicKey, privateKey);
117+
Map<String, String> devServiceProps = generateDevServiceProperties(publicKey, privateKey,
118+
generateEncryptedDevModeJwtKeysBuildItem);
96119

97120
if (!isConfigPresent(MP_JWT_VERIFY_ISSUER) && !isConfigPresent(SMALLRYE_JWT_NEW_TOKEN_ISSUER)) {
98121
devServiceProps.put(MP_JWT_VERIFY_ISSUER, DEFAULT_ISSUER);
@@ -110,7 +133,67 @@ void generateSignKeys(BuildProducer<DevServicesResultBuildItem> devServices,
110133
}
111134
}
112135

113-
private Map<String, String> addDefaultSmallryePropertiesIfMissing(Set<String> userConfigs) {
136+
private KeyPair generateOrReloadKeyPair(CurateOutcomeBuildItem curateOutcomeBuildItem,
137+
Optional<GeneratePersistentDevModeJwtKeysBuildItem> generatePersistentDevModeJwtKeysBuildItem)
138+
throws GeneralSecurityException, IOException {
139+
if (generatePersistentDevModeJwtKeysBuildItem.isPresent()) {
140+
File buildDir = getBuildDir(curateOutcomeBuildItem);
141+
142+
buildDir.mkdirs();
143+
File privateKey = new File(buildDir, DEV_PRIVATE_KEY_PEM);
144+
File publicKey = new File(buildDir, DEV_PUBLIC_KEY_PEM);
145+
if (!privateKey.exists() || !publicKey.exists()) {
146+
KeyPair keyPair = KeyUtils.generateKeyPair(KEY_SIZE);
147+
LOGGER.infof("Generating private/public keys for DEV/TEST in %s and %s", privateKey, publicKey);
148+
try (FileWriter fw = new FileWriter(privateKey)) {
149+
fw.append("-----BEGIN PRIVATE KEY-----\n");
150+
fw.append(Base64.getMimeEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
151+
fw.append("\n");
152+
fw.append("-----END PRIVATE KEY-----\n");
153+
}
154+
try (FileWriter fw = new FileWriter(publicKey)) {
155+
fw.append("-----BEGIN PUBLIC KEY-----\n");
156+
fw.append(Base64.getMimeEncoder().encodeToString(keyPair.getPublic().getEncoded()));
157+
fw.append("\n");
158+
fw.append("-----END PUBLIC KEY-----\n");
159+
}
160+
return keyPair;
161+
} else {
162+
// read from disk
163+
return new KeyPair(KeyUtils.readPublicKey(publicKey.getName()),
164+
KeyUtils.readPrivateKey(privateKey.getName()));
165+
}
166+
} else {
167+
return KeyUtils.generateKeyPair(KEY_SIZE);
168+
}
169+
}
170+
171+
public static File getBuildDir(CurateOutcomeBuildItem curateOutcomeBuildItem) {
172+
File buildDir = null;
173+
ArtifactSources src = curateOutcomeBuildItem.getApplicationModel().getAppArtifact().getSources();
174+
if (src != null) { // shouldn't be null in dev mode
175+
Collection<SourceDir> srcDirs = src.getResourceDirs();
176+
if (srcDirs.isEmpty()) {
177+
// if the module has no resources dir?
178+
srcDirs = src.getSourceDirs();
179+
}
180+
if (!srcDirs.isEmpty()) {
181+
// pick the first resources output dir
182+
Path resourcesOutputDir = srcDirs.iterator().next().getOutputDir();
183+
buildDir = resourcesOutputDir.toFile();
184+
}
185+
}
186+
if (buildDir == null) {
187+
// the module doesn't have any sources nor resources, stick to the build dir
188+
buildDir = new File(
189+
curateOutcomeBuildItem.getApplicationModel().getAppArtifact().getWorkspaceModule().getBuildDir(),
190+
"classes");
191+
}
192+
return buildDir;
193+
}
194+
195+
private Map<String, String> addDefaultSmallryePropertiesIfMissing(Set<String> userConfigs,
196+
Optional<GenerateEncryptedDevModeJwtKeysBuildItem> generateEncryptedDevModeJwtKeysBuildItem) {
114197
HashMap<String, String> devServiceConfigs = new HashMap<>();
115198
if (!userConfigs.contains(SMALLRYE_JWT_SIGN_KEY)) {
116199
devServiceConfigs.put(SMALLRYE_JWT_SIGN_KEY, NONE);
@@ -120,6 +203,16 @@ private Map<String, String> addDefaultSmallryePropertiesIfMissing(Set<String> us
120203
devServiceConfigs.put(MP_JWT_VERIFY_PUBLIC_KEY, NONE);
121204
}
122205

206+
if (generateEncryptedDevModeJwtKeysBuildItem.isPresent()) {
207+
if (!userConfigs.contains(SMALLRYE_JWT_ENCRYPT_KEY) && !userConfigs.contains(SMALLRYE_JWT_ENCRYPT_KEY_LOCATION)) {
208+
devServiceConfigs.put(SMALLRYE_JWT_ENCRYPT_KEY, NONE);
209+
}
210+
211+
if (!userConfigs.contains(SMALLRYE_JWT_DECRYPT_KEY) && !userConfigs.contains(MP_JWT_DECRYPT_KEY_LOCATION)) {
212+
devServiceConfigs.put(SMALLRYE_JWT_DECRYPT_KEY, NONE);
213+
}
214+
}
215+
123216
return devServiceConfigs;
124217
}
125218

@@ -133,10 +226,15 @@ private DevServicesResultBuildItem smallryeJwtDevServiceWith(Map<String, String>
133226
Feature.SMALLRYE_JWT.name(), null, properties);
134227
}
135228

136-
private static Map<String, String> generateDevServiceProperties(String publicKey, String privateKey) {
229+
private static Map<String, String> generateDevServiceProperties(String publicKey, String privateKey,
230+
Optional<GenerateEncryptedDevModeJwtKeysBuildItem> generateEncryptedDevModeJwtKeysBuildItem) {
137231
HashMap<String, String> properties = new HashMap<>();
138232
properties.put(MP_JWT_VERIFY_PUBLIC_KEY, publicKey);
139233
properties.put(SMALLRYE_JWT_SIGN_KEY, privateKey);
234+
if (generateEncryptedDevModeJwtKeysBuildItem.isPresent()) {
235+
properties.put(SMALLRYE_JWT_ENCRYPT_KEY, publicKey);
236+
properties.put(SMALLRYE_JWT_DECRYPT_KEY, privateKey);
237+
}
140238
return properties;
141239
}
142240

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.quarkus.jwt.test.dev;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import org.jboss.shrinkwrap.api.ShrinkWrap;
6+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.RegisterExtension;
9+
10+
import io.quarkus.builder.BuildChainBuilder;
11+
import io.quarkus.builder.BuildContext;
12+
import io.quarkus.builder.BuildStep;
13+
import io.quarkus.jwt.test.GreetingResource;
14+
import io.quarkus.smallrye.jwt.deployment.GenerateEncryptedDevModeJwtKeysBuildItem;
15+
import io.quarkus.test.QuarkusUnitTest;
16+
import io.restassured.RestAssured;
17+
import io.restassured.http.Header;
18+
import io.smallrye.jwt.build.Jwt;
19+
20+
public class SmallryeJwtPersistentColdStartupEncryptedTest {
21+
22+
@RegisterExtension
23+
static QuarkusUnitTest unitTest = new QuarkusUnitTest()
24+
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
25+
.addClasses(GreetingResource.class,
26+
SmallryeJwtPersistentColdStartupSignedTest.PersistentJwtColdStartupChainBuilder.class))
27+
.addBuildChainCustomizer(new SmallryeJwtPersistentColdStartupSignedTest.PersistentJwtColdStartupChainBuilder() {
28+
@Override
29+
public void accept(BuildChainBuilder chain) {
30+
super.accept(chain);
31+
chain.addBuildStep(new BuildStep() {
32+
@Override
33+
public void execute(BuildContext context) {
34+
context.produce(new GenerateEncryptedDevModeJwtKeysBuildItem());
35+
}
36+
})
37+
.produces(GenerateEncryptedDevModeJwtKeysBuildItem.class)
38+
.build();
39+
}
40+
});
41+
42+
@Test
43+
void canBeEncrypted() {
44+
// make sure we can sign JWT tokens recognised by the server, since they use the same config
45+
String token = Jwt.upn("[email protected]")
46+
.groups("User")
47+
.innerSign().encrypt();
48+
RestAssured.given()
49+
.header(new Header("Authorization", "Bearer " + token))
50+
.get("/only-user")
51+
.then().assertThat().statusCode(200);
52+
}
53+
}

0 commit comments

Comments
 (0)