diff --git a/security/jwt/pom.xml b/security/jwt/pom.xml index f4f75ed61..b98ba9ca7 100644 --- a/security/jwt/pom.xml +++ b/security/jwt/pom.xml @@ -22,7 +22,6 @@ io.quarkus quarkus-smallrye-jwt-build - test diff --git a/security/jwt/src/main/java/io/quarkus/ts/security/jwt/GenerateJwtResource.java b/security/jwt/src/main/java/io/quarkus/ts/security/jwt/GenerateJwtResource.java new file mode 100644 index 000000000..3850ee899 --- /dev/null +++ b/security/jwt/src/main/java/io/quarkus/ts/security/jwt/GenerateJwtResource.java @@ -0,0 +1,75 @@ +package io.quarkus.ts.security.jwt; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.util.Collections; +import java.util.Date; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import javax.annotation.security.PermitAll; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.build.JwtClaimsBuilder; + +@Path("/login") +public class GenerateJwtResource { + + public enum Invalidity { + WRONG_ISSUER, + WRONG_DATE, + WRONG_KEY + } + + private static final String DEFAULT_ISSUER = "https://my.auth.server/"; + private static final int TEN = 10; + + @POST + @Path("/jwt") + @PermitAll + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String login(@QueryParam("invalidity") String invalidity, String body) throws NoSuchAlgorithmException { + Date now = new Date(); + Date expiration = new Date(TimeUnit.SECONDS.toMillis(TEN) + now.getTime()); + String issuer = DEFAULT_ISSUER; + if (invalidity.equalsIgnoreCase(Invalidity.WRONG_ISSUER.name())) { + issuer = "https://wrong/"; + } + + if (invalidity.equalsIgnoreCase(Invalidity.WRONG_DATE.name())) { + now = new Date(now.getTime() - TimeUnit.DAYS.toMillis(TEN)); + expiration = new Date(now.getTime() - TimeUnit.DAYS.toMillis(TEN)); + } + + PrivateKey privateKey = null; + if (invalidity.equalsIgnoreCase(Invalidity.WRONG_KEY.name())) { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + privateKey = keyPair.getPrivate(); + } + + JwtClaimsBuilder jwtbuilder = Jwt.issuer(issuer) + .expiresAt(expiration.getTime()) + .issuedAt(now.getTime()) + .subject("test_subject_at_example_com") + .groups(Set.of(body)) + .claim("upn", "test-subject@example.com") + .claim("roleMappings", Collections.singletonMap("admin", "superuser")); + + if (!Objects.isNull(privateKey)) { + return jwtbuilder.jws().sign(privateKey); + } + + return jwtbuilder.sign(); + } +} diff --git a/security/jwt/src/main/resources/application.properties b/security/jwt/src/main/resources/application.properties index 1f51d0e46..622caede7 100644 --- a/security/jwt/src/main/resources/application.properties +++ b/security/jwt/src/main/resources/application.properties @@ -1,3 +1,4 @@ +smallrye.jwt.sign.key.location=private-key.pem mp.jwt.verify.publickey.location=public-key.pem mp.jwt.verify.issuer=https://my.auth.server/ smallrye.jwt.expiration.grace=120 diff --git a/security/jwt/src/test/resources/private-key.pem b/security/jwt/src/main/resources/private-key.pem similarity index 100% rename from security/jwt/src/test/resources/private-key.pem rename to security/jwt/src/main/resources/private-key.pem diff --git a/security/jwt/src/test/java/io/quarkus/ts/security/jwt/BaseJwtSecurityIT.java b/security/jwt/src/test/java/io/quarkus/ts/security/jwt/BaseJwtSecurityIT.java index 0dae1645b..373428a09 100644 --- a/security/jwt/src/test/java/io/quarkus/ts/security/jwt/BaseJwtSecurityIT.java +++ b/security/jwt/src/test/java/io/quarkus/ts/security/jwt/BaseJwtSecurityIT.java @@ -1,43 +1,27 @@ package io.quarkus.ts.security.jwt; +import static io.quarkus.ts.security.jwt.GenerateJwtResource.Invalidity; +import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.KeyFactory; -import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; -import java.security.interfaces.RSAPrivateKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Collections; -import java.util.Date; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; +import java.util.Objects; import org.apache.http.HttpStatus; -import org.jboss.logging.Logger; -import org.jose4j.base64url.internal.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.Test; import io.restassured.http.ContentType; import io.restassured.specification.RequestSpecification; -import io.smallrye.jwt.algorithm.SignatureAlgorithm; -import io.smallrye.jwt.build.Jwt; public abstract class BaseJwtSecurityIT { - private static final Logger LOG = Logger.getLogger(BaseJwtSecurityIT.class); - - private static final int TEN = 10; - private static final int NINETY = 90; + private static final String EMPTY_GROUP = ""; @Test - public void securedEveryoneNoGroup() throws Exception { - givenWithToken(createToken()) + public void securedEveryoneNoGroup() { + givenWithToken(createToken(EMPTY_GROUP)) .get("/secured/everyone") .then() .statusCode(HttpStatus.SC_OK) @@ -47,7 +31,7 @@ public void securedEveryoneNoGroup() throws Exception { } @Test - public void securedEveryoneViewGroup() throws Exception { + public void securedEveryoneViewGroup() { givenWithToken(createToken("view")) .get("/secured/everyone") .then() @@ -58,7 +42,7 @@ public void securedEveryoneViewGroup() throws Exception { } @Test - public void securedEveryoneAdminGroup() throws Exception { + public void securedEveryoneAdminGroup() { givenWithToken(createToken("admin")) .get("/secured/everyone") .then() @@ -69,39 +53,39 @@ public void securedEveryoneAdminGroup() throws Exception { } @Test - public void securedEveryoneWrongIssuer() throws Exception { - givenWithToken(createToken(Invalidity.WRONG_ISSUER)) + public void securedEveryoneWrongIssuer() { + givenWithToken(createToken(Invalidity.WRONG_ISSUER, EMPTY_GROUP)) .get("/secured/everyone") .then() .statusCode(HttpStatus.SC_UNAUTHORIZED); } @Test - public void securedEveryoneWrongDate() throws Exception { - givenWithToken(createToken(Invalidity.WRONG_DATE)) + public void securedEveryoneWrongDate() { + givenWithToken(createToken(Invalidity.WRONG_DATE, EMPTY_GROUP)) .get("/secured/everyone") .then() .statusCode(HttpStatus.SC_UNAUTHORIZED); } @Test - public void securedEveryoneWrongKey() throws Exception { - givenWithToken(createToken(Invalidity.WRONG_KEY)) + public void securedEveryoneWrongKey() { + givenWithToken(createToken(Invalidity.WRONG_KEY, EMPTY_GROUP)) .get("/secured/everyone") .then() .statusCode(HttpStatus.SC_UNAUTHORIZED); } @Test - public void securedAdminNoGroup() throws Exception { - givenWithToken(createToken()) + public void securedAdminNoGroup() { + givenWithToken(createToken(EMPTY_GROUP)) .get("/secured/admin") .then() .statusCode(HttpStatus.SC_FORBIDDEN); } @Test - public void securedAdminViewGroup() throws Exception { + public void securedAdminViewGroup() { givenWithToken(createToken("view")) .get("/secured/admin") .then() @@ -109,7 +93,7 @@ public void securedAdminViewGroup() throws Exception { } @Test - public void securedAdminAdminGroup() throws Exception { + public void securedAdminAdminGroup() { givenWithToken(createToken("admin")) .get("/secured/admin") .then() @@ -118,15 +102,15 @@ public void securedAdminAdminGroup() throws Exception { } @Test - public void securedNoOneNoGroup() throws Exception { - givenWithToken(createToken()) + public void securedNoOneNoGroup() { + givenWithToken(createToken(EMPTY_GROUP)) .get("/secured/noone") .then() .statusCode(HttpStatus.SC_FORBIDDEN); } @Test - public void securedNoOneViewGroup() throws Exception { + public void securedNoOneViewGroup() { givenWithToken(createToken("view")) .get("/secured/noone") .then() @@ -134,7 +118,7 @@ public void securedNoOneViewGroup() throws Exception { } @Test - public void securedNoOneAdminGroup() throws Exception { + public void securedNoOneAdminGroup() { givenWithToken(createToken("admin")) .get("/secured/noone") .then() @@ -142,8 +126,8 @@ public void securedNoOneAdminGroup() throws Exception { } @Test - public void permittedCorrectToken() throws Exception { - givenWithToken(createToken()) + public void permittedCorrectToken() { + givenWithToken(createToken(EMPTY_GROUP)) .get("/permitted") .then() .statusCode(HttpStatus.SC_OK) @@ -151,32 +135,32 @@ public void permittedCorrectToken() throws Exception { } @Test - public void permittedWrongIssuer() throws Exception { - givenWithToken(createToken(Invalidity.WRONG_ISSUER)) + public void permittedWrongIssuer() { + givenWithToken(createToken(Invalidity.WRONG_ISSUER, EMPTY_GROUP)) .get("/permitted") .then() .statusCode(HttpStatus.SC_UNAUTHORIZED); // in Thorntail, this is 200, but both approaches are likely valid } @Test - public void permittedWrongDate() throws Exception { - givenWithToken(createToken(Invalidity.WRONG_DATE)) + public void permittedWrongDate() { + givenWithToken(createToken(Invalidity.WRONG_DATE, EMPTY_GROUP)) .get("/permitted") .then() .statusCode(HttpStatus.SC_UNAUTHORIZED); // in Thorntail, this is 200, but both approaches are likely valid } @Test - public void permittedWrongKey() throws Exception { - givenWithToken(createToken(Invalidity.WRONG_KEY)) + public void permittedWrongKey() { + givenWithToken(createToken(Invalidity.WRONG_KEY, EMPTY_GROUP)) .get("/permitted") .then() .statusCode(HttpStatus.SC_UNAUTHORIZED); // in Thorntail, this is 200, but both approaches are likely valid } @Test - public void deniedCorrectToken() throws Exception { - givenWithToken(createToken()) + public void deniedCorrectToken() { + givenWithToken(createToken(EMPTY_GROUP)) .get("/denied") .then() .statusCode(HttpStatus.SC_FORBIDDEN); @@ -184,7 +168,7 @@ public void deniedCorrectToken() throws Exception { @Test public void mixedConstrained() throws Exception { - givenWithToken(createToken()) + givenWithToken(createToken(EMPTY_GROUP)) .get("/mixed/constrained") .then() .statusCode(HttpStatus.SC_OK) @@ -193,7 +177,7 @@ public void mixedConstrained() throws Exception { @Test public void mixedUnconstrained() throws Exception { - givenWithToken(createToken()) + givenWithToken(createToken(EMPTY_GROUP)) .get("/mixed/unconstrained") .then() .statusCode(HttpStatus.SC_FORBIDDEN); // quarkus.security.deny-unannotated-members=true @@ -273,13 +257,7 @@ public void parameterizedPathsViewAdminGroup() throws Exception { @Test public void tokenExpirationGracePeriod() throws Exception { - Supplier clock = () -> { - Date now = new Date(); - now = new Date(now.getTime() - TimeUnit.SECONDS.toMillis(NINETY)); - return now; - }; - - givenWithToken(createToken(clock, null, "admin")) + givenWithToken(createToken("admin")) .get("/secured/admin") .then() .statusCode(HttpStatus.SC_OK) @@ -288,64 +266,17 @@ public void tokenExpirationGracePeriod() throws Exception { protected abstract RequestSpecification givenWithToken(String token); - private static RSAPrivateKey loadPrivateKey() throws Exception { - String key = new String(Files.readAllBytes(Paths.get("target/test-classes/private-key.pem")), Charset.defaultCharset()); - - String privateKeyPEM = key - .replace("-----BEGIN PRIVATE KEY-----", "") - .replaceAll(System.lineSeparator(), "") - .replace("-----END PRIVATE KEY-----", ""); - - byte[] encoded = Base64.decodeBase64(privateKeyPEM); - - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); - return (RSAPrivateKey) keyFactory.generatePrivate(keySpec); - } - - private enum Invalidity { - WRONG_ISSUER, - WRONG_DATE, - WRONG_KEY - } - - private static String createToken(String... groups) throws Exception { - return createToken(Date::new, null, groups); - } - - private static String createToken(Invalidity invalidity, String... groups) throws Exception { - return createToken(Date::new, invalidity, groups); + private static String createToken(String group) { + return createToken(null, group); } - private static String createToken(Supplier clock, Invalidity invalidity, String... groups) - throws Exception { - String issuer = "https://my.auth.server/"; - if (invalidity == Invalidity.WRONG_ISSUER) { - issuer = "https://wrong/"; - } - - Date now = clock.get(); - Date expiration = new Date(TimeUnit.SECONDS.toMillis(TEN) + now.getTime()); - if (invalidity == Invalidity.WRONG_DATE) { - now = new Date(now.getTime() - TimeUnit.DAYS.toMillis(TEN)); - expiration = new Date(now.getTime() - TimeUnit.DAYS.toMillis(TEN)); - } - - RSAPrivateKey privateKey = loadPrivateKey(); - if (invalidity == Invalidity.WRONG_KEY) { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - KeyPair keyPair = keyPairGenerator.generateKeyPair(); - privateKey = (RSAPrivateKey) keyPair.getPrivate(); - } - - return Jwt.issuer(issuer) - .expiresAt(expiration.getTime()) - .issuedAt(now.getTime()) - .subject("test_subject_at_example_com") - .groups(Set.of(groups)) - .claim("upn", "test-subject@example.com") - .claim("roleMappings", Collections.singletonMap("admin", "superuser")) - .jws().algorithm(SignatureAlgorithm.RS256).sign(privateKey); + private static String createToken(Invalidity invalidity, String group) { + return given() + .body(group) + .when() + .post("/login/jwt?invalidity=" + (Objects.isNull(invalidity) ? "" : invalidity.name())).then() + .statusCode(200) + .extract().body().asString(); } @Test