diff --git a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/LottoNumbers.java b/security-jwt-quickstart/src/main/java/org/acme/security/jwt/LottoNumbers.java deleted file mode 100644 index 9edf1fe1c4..0000000000 --- a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/LottoNumbers.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.acme.security.jwt; - -import java.util.List; - -public class LottoNumbers { - public List numbers; - - @Override - public String toString() { - return "LottoNumbers{" + - "numbers=" + numbers + - '}'; - } -} diff --git a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/LottoNumbersResource.java b/security-jwt-quickstart/src/main/java/org/acme/security/jwt/LottoNumbersResource.java deleted file mode 100644 index 143ffa6f76..0000000000 --- a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/LottoNumbersResource.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.acme.security.jwt; - -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Optional; - -import javax.annotation.security.RolesAllowed; -import javax.enterprise.context.Dependent; -import javax.inject.Inject; -import javax.json.JsonString; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; - -import org.eclipse.microprofile.jwt.Claim; -import org.eclipse.microprofile.jwt.Claims; - -@Dependent -public class LottoNumbersResource { - @Inject - @Claim(standard = Claims.birthdate) - Optional birthdate; - - @GET - @Path("winners") - @Produces(MediaType.APPLICATION_JSON) - @RolesAllowed("Subscriber") - public LottoNumbers winners() { - int remaining = 6; - ArrayList numbers = new ArrayList<>(); - - if (birthdate.isPresent()) { - JsonString bdayString = birthdate.get(); - LocalDate bday = LocalDate.parse(bdayString.getString()); - numbers.add(bday.getDayOfMonth()); - remaining--; - } - while (remaining > 0) { - int pick = (int) Math.rint(64 * Math.random()); - numbers.add(pick); - remaining--; - } - LottoNumbers winners = new LottoNumbers(); - winners.numbers = numbers; - return winners; - } -} diff --git a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResource.java b/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResource.java index d51b9cee17..882aa5792f 100644 --- a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResource.java +++ b/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResource.java @@ -1,20 +1,14 @@ package org.acme.security.jwt; -import java.security.Principal; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Optional; - import javax.annotation.security.DenyAll; import javax.annotation.security.PermitAll; import javax.annotation.security.RolesAllowed; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; -import javax.json.JsonString; import javax.ws.rs.GET; +import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.Path; import javax.ws.rs.Produces; -import javax.ws.rs.container.ResourceContext; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.SecurityContext; @@ -27,108 +21,62 @@ @RequestScoped public class TokenSecuredResource { - @Context - ResourceContext resourceContext; - @Inject JsonWebToken jwt; @Inject @Claim(standard = Claims.birthdate) - Optional birthdate; + String birthdate; + - @GET() + @GET @Path("permit-all") @PermitAll @Produces(MediaType.TEXT_PLAIN) public String hello(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - boolean hasJWT = jwt.getClaimNames() != null; - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s", name, ctx.isSecure(), - ctx.getAuthenticationScheme(), hasJWT); - return helloReply; + return getResponseString(ctx); } - @GET() + @GET @Path("roles-allowed") - @RolesAllowed({ "Echoer", "Subscriber" }) + @RolesAllowed({ "User", "Admin" }) @Produces(MediaType.TEXT_PLAIN) public String helloRolesAllowed(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - boolean hasJWT = jwt.getClaimNames() != null; - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s", name, ctx.isSecure(), - ctx.getAuthenticationScheme(), hasJWT); - return helloReply; - } - - @GET() - @Path("deny-all") - @DenyAll - @Produces(MediaType.TEXT_PLAIN) - public String helloShouldDeny(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - return "hello + " + name; + return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); } - + @GET - @Path("winners") + @Path("roles-allowed-admin") + @RolesAllowed("Admin") @Produces(MediaType.TEXT_PLAIN) - @RolesAllowed("Subscriber") - public String winners() { - int remaining = 6; - ArrayList numbers = new ArrayList<>(); - - // If the JWT contains a birthdate claim, use the day of the month as a pick - if (jwt.containsClaim(Claims.birthdate.name())) { - String bdayString = jwt.getClaim(Claims.birthdate.name()); - LocalDate bday = LocalDate.parse(bdayString); - numbers.add(bday.getDayOfMonth()); - remaining--; - } - // Fill remaining picks with random numbers - while (remaining > 0) { - int pick = (int) Math.rint(64 * Math.random() + 1); - numbers.add(pick); - remaining--; - } - return numbers.toString(); + public String helloRolesAllowedAdmin(@Context SecurityContext ctx) { + return getResponseString(ctx) + ", birthdate: " + birthdate; } @GET - @Path("winners2") + @Path("deny-all") + @DenyAll @Produces(MediaType.TEXT_PLAIN) - @RolesAllowed("Subscriber") - public String winners2() { - int remaining = 6; - ArrayList numbers = new ArrayList<>(); + public String helloShouldDeny(@Context SecurityContext ctx) { + throw new InternalServerErrorException("This method must not be invoked"); + } - // If the JWT contains a birthdate claim, use the day of the month as a pick - if (birthdate.isPresent()) { - String bdayString = birthdate.get().getString(); - LocalDate bday = LocalDate.parse(bdayString); - numbers.add(bday.getDayOfMonth()); - remaining--; + private String getResponseString(SecurityContext ctx) { + String name; + if (ctx.getUserPrincipal() == null) { + name = "anonymous"; + } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { + throw new InternalServerErrorException("Principal and JsonWebToken names do not match"); + } else { + name = ctx.getUserPrincipal().getName(); } - // Fill remaining picks with random numbers - while (remaining > 0) { - int pick = (int) Math.rint(64 * Math.random() + 1); - numbers.add(pick); - remaining--; - } - return numbers.toString(); + return String.format("hello + %s," + + " isHttps: %s," + + " authScheme: %s," + + " hasJWT: %s", + name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); } - /** - * Illustrate the same functionality as the winners endpoint using a sub-resource that has injected JsonWebToken and - * - * @Claim values using the {@linkplain CDI#current()} API. - * - * @return LottoNumbersResource - */ - @Path("/lotto") - public LottoNumbersResource lotto() { - return resourceContext.getResource(LottoNumbersResource.class); - } + private boolean hasJwt() { + return jwt.getClaimNames() != null; + } } diff --git a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV1.java b/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV1.java deleted file mode 100644 index e0cddb0433..0000000000 --- a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV1.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.acme.security.jwt; - -import java.security.Principal; - -import javax.annotation.security.PermitAll; -import javax.enterprise.context.RequestScoped; -import javax.inject.Inject; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.SecurityContext; - -import org.eclipse.microprofile.jwt.JsonWebToken; - -/** - * Version 1 of the TokenSecuredResource - */ -@Path("/secured-v1") -@RequestScoped -public class TokenSecuredResourceV1 { - - @Inject - JsonWebToken jwt; - - @GET() - @Path("permit-all") - @PermitAll - @Produces(MediaType.TEXT_PLAIN) - public String hello(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - boolean hasJWT = jwt.getClaimNames() != null; - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s", name, ctx.isSecure(), - ctx.getAuthenticationScheme(), hasJWT); - return helloReply; - } -} diff --git a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV2.java b/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV2.java deleted file mode 100644 index 182adf64c3..0000000000 --- a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV2.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.acme.security.jwt; - -import java.security.Principal; - -import javax.annotation.security.PermitAll; -import javax.annotation.security.RolesAllowed; -import javax.enterprise.context.RequestScoped; -import javax.inject.Inject; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.SecurityContext; - -import org.eclipse.microprofile.jwt.JsonWebToken; - -/** - * Version 2 of the TokenSecuredResource - */ -@Path("/secured-v2") -@RequestScoped -public class TokenSecuredResourceV2 { - - @Inject - JsonWebToken jwt; - - @GET() - @Path("permit-all") - @PermitAll - @Produces(MediaType.TEXT_PLAIN) - public String hello(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), - ctx.getAuthenticationScheme()); - return helloReply; - } - - @GET() - @Path("roles-allowed") - @RolesAllowed({ "Echoer", "Subscriber" }) - @Produces(MediaType.TEXT_PLAIN) - public String helloRolesAllowed(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - boolean hasJWT = jwt.getClaimNames() != null; - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s", name, ctx.isSecure(), - ctx.getAuthenticationScheme(), hasJWT); - return helloReply; - } -} diff --git a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV3.java b/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV3.java deleted file mode 100644 index 4e678f5afb..0000000000 --- a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV3.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.acme.security.jwt; - -import java.security.Principal; -import java.time.LocalDate; -import java.util.ArrayList; - -import javax.annotation.security.PermitAll; -import javax.annotation.security.RolesAllowed; -import javax.enterprise.context.RequestScoped; -import javax.inject.Inject; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.SecurityContext; - -import org.eclipse.microprofile.jwt.Claims; -import org.eclipse.microprofile.jwt.JsonWebToken; - -/** - * Version 3 of the TokenSecuredResource - */ -@Path("/secured-v3") -@RequestScoped -public class TokenSecuredResourceV3 { - - @Inject - JsonWebToken jwt; - - @GET() - @Path("permit-all") - @PermitAll - @Produces(MediaType.TEXT_PLAIN) - public String hello(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), - ctx.getAuthenticationScheme()); - return helloReply; - } - - @GET() - @Path("roles-allowed") - @RolesAllowed({ "Echoer", "Subscriber" }) - @Produces(MediaType.TEXT_PLAIN) - public String helloRolesAllowed(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - boolean hasJWT = jwt.getClaimNames() != null; - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s", name, ctx.isSecure(), - ctx.getAuthenticationScheme(), hasJWT); - return helloReply; - } - - @GET - @Path("winners") - @Produces(MediaType.TEXT_PLAIN) - @RolesAllowed("Subscriber") - public String winners() { - int remaining = 6; - ArrayList numbers = new ArrayList<>(); - - // If the JWT contains a birthdate claim, use the day of the month as a pick - if (jwt.containsClaim(Claims.birthdate.name())) { - String bdayString = jwt.getClaim(Claims.birthdate.name()); - LocalDate bday = LocalDate.parse(bdayString); - numbers.add(bday.getDayOfMonth()); - remaining--; - } - // Fill remaining picks with random numbers - while (remaining > 0) { - int pick = (int) Math.rint(64 * Math.random() + 1); - numbers.add(pick); - remaining--; - } - return numbers.toString(); - } -} diff --git a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV4.java b/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV4.java deleted file mode 100644 index 8099af6115..0000000000 --- a/security-jwt-quickstart/src/main/java/org/acme/security/jwt/TokenSecuredResourceV4.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.acme.security.jwt; - -import java.security.Principal; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Optional; - -import javax.annotation.security.PermitAll; -import javax.annotation.security.RolesAllowed; -import javax.enterprise.context.RequestScoped; -import javax.inject.Inject; -import javax.json.JsonString; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.SecurityContext; - -import org.eclipse.microprofile.jwt.Claim; -import org.eclipse.microprofile.jwt.Claims; -import org.eclipse.microprofile.jwt.JsonWebToken; - -/** - * Version 4 of the TokenSecuredResource - */ -@Path("/secured-v4") -@RequestScoped -public class TokenSecuredResourceV4 { - - @Inject - JsonWebToken jwt; - @Inject - @Claim(standard = Claims.birthdate) - Optional birthdate; - - @GET() - @Path("permit-all") - @PermitAll - @Produces(MediaType.TEXT_PLAIN) - public String hello(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), - ctx.getAuthenticationScheme()); - return helloReply; - } - - @GET() - @Path("roles-allowed") - @RolesAllowed({ "Echoer", "Subscriber" }) - @Produces(MediaType.TEXT_PLAIN) - public String helloRolesAllowed(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - boolean hasJWT = jwt.getClaimNames() != null; - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s", name, ctx.isSecure(), - ctx.getAuthenticationScheme(), hasJWT); - return helloReply; - } - - @GET - @Path("winners2") - @Produces(MediaType.TEXT_PLAIN) - @RolesAllowed("Subscriber") - public String winners2() { - int remaining = 6; - ArrayList numbers = new ArrayList<>(); - - // If the JWT contains a birthdate claim, use the day of the month as a pick - if (birthdate.isPresent()) { - String bdayString = birthdate.get().getString(); - LocalDate bday = LocalDate.parse(bdayString); - numbers.add(bday.getDayOfMonth()); - remaining--; - } - // Fill remaining picks with random numbers - while (remaining > 0) { - int pick = (int) Math.rint(64 * Math.random() + 1); - numbers.add(pick); - remaining--; - } - return numbers.toString(); - } -} diff --git a/security-jwt-quickstart/src/main/resources/application.properties b/security-jwt-quickstart/src/main/resources/application.properties index 02aa73dbc5..b75e82c0c3 100644 --- a/security-jwt-quickstart/src/main/resources/application.properties +++ b/security-jwt-quickstart/src/main/resources/application.properties @@ -1,20 +1,6 @@ -# Configuration file +# Public verification key mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem +# Required issuer mp.jwt.verify.issuer=https://quarkus.io/using-jwt-rbac - -quarkus.smallrye-jwt.enabled=true - - -# DEBUG console logging -quarkus.log.console.enable=true -#quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}]] (%t) %s%e%n -#quarkus.log.console.level=DEBUG - -# TRACE file logging -quarkus.log.file.enable=true -#quarkus.log.file.path=/tmp/trace.log -#quarkus.log.file.level=TRACE -#quarkus.log.file.format=%d{HH:mm:ss} %-5p [%c{2.}]] (%t) %s%e%n -#quarkus.log.category."io.quarkus.smallrye.jwt".level=TRACE -#quarkus.log.category."io.undertow.request.security".level=TRACE -#quarkus.log.category."io.smallrye.jwt".level=TRACE +# Private signing key +smallrye.jwt.sign.key-location=privateKey.pem \ No newline at end of file diff --git a/security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java b/security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java index e17849520d..4077c99f16 100644 --- a/security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java +++ b/security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java @@ -1,32 +1,25 @@ package org.acme.security.jwt; -import java.util.HashMap; +import java.util.Arrays; +import java.util.HashSet; import org.eclipse.microprofile.jwt.Claims; +import io.smallrye.jwt.build.Jwt; + /** - * A simple utility class to generate and print a JWT token string to stdout. Can be run with: - * mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test + * A simple utility class to generate and print a JWT token string to stdout. */ public class GenerateToken { /** - * - * @param args - [0]: optional name of classpath resource for json document of claims to add; defaults to "/JwtClaims.json" - * [1]: optional time in seconds for expiration of generated token; defaults to 300 - * @throws Exception + * Generate JWT token */ - public static void main(String[] args) throws Exception { - String claimsJson = "/JwtClaims.json"; - if (args.length > 0) { - claimsJson = args[0]; - } - HashMap timeClaims = new HashMap<>(); - if (args.length > 1) { - long duration = Long.parseLong(args[1]); - long exp = TokenUtils.currentTimeInSecs() + duration; - timeClaims.put(Claims.exp.name(), exp); - } - String token = TokenUtils.generateTokenString(claimsJson, timeClaims); + public static void main(String[] args) { + String token = Jwt.issuer("https://quarkus.io/using-jwt-rbac") + .upn("jdoe@quarkus.io") + .groups(new HashSet<>(Arrays.asList("User", "Admin"))) + .claim(Claims.birthdate.name(), "2001-07-13") + .sign(); System.out.println(token); } } diff --git a/security-jwt-quickstart/src/test/java/org/acme/security/jwt/TokenSecuredResourceTest.java b/security-jwt-quickstart/src/test/java/org/acme/security/jwt/TokenSecuredResourceTest.java index 6dd6df03a1..58736034fb 100644 --- a/security-jwt-quickstart/src/test/java/org/acme/security/jwt/TokenSecuredResourceTest.java +++ b/security-jwt-quickstart/src/test/java/org/acme/security/jwt/TokenSecuredResourceTest.java @@ -3,20 +3,16 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.containsString; -import java.io.StringReader; -import java.net.HttpURLConnection; -import java.util.HashMap; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashSet; -import javax.json.Json; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; +import org.eclipse.microprofile.jwt.Claims; import org.junit.jupiter.api.Test; -import io.quarkus.test.junit.DisabledOnNativeImage; import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; import io.restassured.response.Response; +import io.smallrye.jwt.build.Jwt; /** * Tests of the TokenSecuredResource REST endpoints @@ -24,17 +20,6 @@ @QuarkusTest public class TokenSecuredResourceTest { - /** - * The test generated JWT token string - */ - private String token; - - @BeforeEach - public void generateToken() throws Exception { - HashMap timeClaims = new HashMap<>(); - token = TokenUtils.generateTokenString("/JwtClaims.json", timeClaims); - } - @Test public void testHelloEndpoint() { Response response = given() @@ -44,64 +29,124 @@ public void testHelloEndpoint() { response.then() .statusCode(200) - .body(containsString("hello + anonymous, isSecure: false, authScheme: null, hasJWT: false")); + .body(containsString("hello + anonymous, isHttps: false, authScheme: null, hasJWT: false")); } @Test - public void testHelloRolesAllowed() { + public void testHelloRolesAllowedUser() { Response response = given().auth() - .oauth2(token) + .oauth2(generateValidUserToken()) .when() .get("/secured/roles-allowed").andReturn(); response.then() .statusCode(200) - .body(containsString("hello + jdoe@quarkus.io, isSecure: false, authScheme: Bearer, hasJWT: true")); + .body(containsString("hello + jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13")); } + + @Test + public void testHelloRolesAllowedAdminOnlyWithUserRole() { + Response response = given().auth() + .oauth2(generateValidUserToken()) + .when() + .get("/secured/roles-allowed-admin").andReturn(); + response.then().statusCode(403); + } + @Test - public void testHelloDenyAll() { + public void testHelloRolesAllowedAdmin() { Response response = given().auth() - .oauth2(token) + .oauth2(generateValidAdminToken()) .when() - .get("/secured/deny-all").andReturn(); + .get("/secured/roles-allowed").andReturn(); - Assertions.assertEquals(HttpURLConnection.HTTP_FORBIDDEN, response.getStatusCode()); + response.then() + .statusCode(200) + .body(containsString("hello + jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13")); } + + @Test + public void testHelloRolesAllowedAdminOnlyWithAdminRole() { + Response response = given().auth() + .oauth2(generateValidAdminToken()) + .when() + .get("/secured/roles-allowed-admin").andReturn(); + response.then() + .statusCode(200) + .body(containsString("hello + jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13")); + } + @Test - public void testWinners() { - Response response = RestAssured.given().auth() - .oauth2(token) + public void testHelloRolesAllowedExpiredToken() { + Response response = given().auth() + .oauth2(generateExpiredToken()) .when() - .get("/secured/winners").andReturn(); + .get("/secured/roles-allowed").andReturn(); - Assertions.assertFalse(response.body().asString().isEmpty()); + response.then().statusCode(401); } + + @Test + public void testHelloRolesAllowedModifiedToken() { + Response response = given().auth() + .oauth2(generateValidUserToken() + "1") + .when() + .get("/secured/roles-allowed").andReturn(); + response.then().statusCode(401); + } + @Test - public void testWinnersWithBirthdate() { - Response response = RestAssured.given().auth() - .oauth2(token) + public void testHelloRolesAllowedWrongIssuer() { + Response response = given().auth() + .oauth2(generateWrongIssuerToken()) .when() - .get("/secured/winners2").andReturn(); + .get("/secured/roles-allowed").andReturn(); - Assertions.assertEquals(HttpURLConnection.HTTP_OK, response.getStatusCode()); - Assertions.assertFalse(response.body().asString().isEmpty()); + response.then().statusCode(401); } @Test - @DisabledOnNativeImage("Doesn't work in the native mode due to a subresource issue") - public void testLottoWinners() { - Response response = RestAssured.given().auth() - .oauth2(token) + public void testHelloDenyAll() { + Response response = given().auth() + .oauth2(generateValidUserToken()) .when() - .get("/secured/lotto/winners").andReturn(); + .get("/secured/deny-all").andReturn(); - Assertions.assertEquals(HttpURLConnection.HTTP_OK, response.getStatusCode()); - String replyString = response.body().asString(); - Json.createReader(new StringReader(replyString)).readObject(); - LottoNumbers numbers = response.as(LottoNumbers.class); - Assertions.assertFalse(numbers.numbers.isEmpty()); + response.then().statusCode(403); + } + + static String generateValidUserToken() { + return Jwt.upn("jdoe@quarkus.io") + .issuer("https://quarkus.io/using-jwt-rbac") + .groups("User") + .claim(Claims.birthdate.name(), "2001-07-13") + .sign(); + } + + static String generateValidAdminToken() { + return Jwt.upn("jdoe@quarkus.io") + .issuer("https://quarkus.io/using-jwt-rbac") + .groups("Admin") + .claim(Claims.birthdate.name(), "2001-07-13") + .sign(); + } + + static String generateExpiredToken() { + return Jwt.upn("jdoe@quarkus.io") + .issuer("https://quarkus.io/using-jwt-rbac") + .groups(new HashSet<>(Arrays.asList("User", "Admin"))) + .expiresAt(Instant.now().minusSeconds(10)) + .sign(); + } + + static String generateWrongIssuerToken() { + return Jwt.upn("jdoe@quarkus.io") + .issuer("https://wrong-issuer") + .groups(new HashSet<>(Arrays.asList("User", "Admin"))) + .expiresAt(Instant.now().minusSeconds(10)) + .sign(); } } diff --git a/security-jwt-quickstart/src/test/java/org/acme/security/jwt/TokenUtils.java b/security-jwt-quickstart/src/test/java/org/acme/security/jwt/TokenUtils.java deleted file mode 100644 index a03227f6f9..0000000000 --- a/security-jwt-quickstart/src/test/java/org/acme/security/jwt/TokenUtils.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.acme.security.jwt; - -import java.io.InputStream; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.Base64; -import java.util.Map; - -import org.eclipse.microprofile.jwt.Claims; - -import io.smallrye.jwt.build.Jwt; -import io.smallrye.jwt.build.JwtClaimsBuilder; - -/** - * Utilities for generating a JWT for testing - */ -public class TokenUtils { - - private TokenUtils() { - // no-op: utility class - } - - /** - * Utility method to generate a JWT string from a JSON resource file that is signed by the privateKey.pem - * test resource key, possibly with invalid fields. - * - * @param jsonResName - name of test resources file - * @param timeClaims - used to return the exp, iat, auth_time claims - * @return the JWT string - * @throws Exception on parse failure - */ - public static String generateTokenString(String jsonResName, Map timeClaims) - throws Exception { - // Use the test private key associated with the test public key for a valid signature - PrivateKey pk = readPrivateKey("/privateKey.pem"); - return generateTokenString(pk, "/privateKey.pem", jsonResName, timeClaims); - } - - public static String generateTokenString(PrivateKey privateKey, String kid, - String jsonResName, Map timeClaims) throws Exception { - - JwtClaimsBuilder claims = Jwt.claims(jsonResName); - long currentTimeInSecs = currentTimeInSecs(); - long exp = timeClaims != null && timeClaims.containsKey(Claims.exp.name()) - ? timeClaims.get(Claims.exp.name()) - : currentTimeInSecs + 300; - - claims.issuedAt(currentTimeInSecs); - claims.claim(Claims.auth_time.name(), currentTimeInSecs); - claims.expiresAt(exp); - - return claims.jws().signatureKeyId(kid).sign(privateKey); - } - - /** - * Read a PEM encoded private key from the classpath - * - * @param pemResName - key file resource name - * @return PrivateKey - * @throws Exception on decode failure - */ - public static PrivateKey readPrivateKey(final String pemResName) throws Exception { - try (InputStream contentIS = TokenUtils.class.getResourceAsStream(pemResName)) { - byte[] tmp = new byte[4096]; - int length = contentIS.read(tmp); - return decodePrivateKey(new String(tmp, 0, length, "UTF-8")); - } - } - - /** - * Generate a new RSA keypair. - * - * @param keySize - the size of the key - * @return KeyPair - * @throws NoSuchAlgorithmException on failure to load RSA key generator - */ - public static KeyPair generateKeyPair(final int keySize) throws NoSuchAlgorithmException { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(keySize); - return keyPairGenerator.genKeyPair(); - } - - /** - * Decode a PEM encoded private key string to an RSA PrivateKey - * - * @param pemEncoded - PEM string for private key - * @return PrivateKey - * @throws Exception on decode failure - */ - public static PrivateKey decodePrivateKey(final String pemEncoded) throws Exception { - byte[] encodedBytes = toEncodedBytes(pemEncoded); - - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedBytes); - KeyFactory kf = KeyFactory.getInstance("RSA"); - return kf.generatePrivate(keySpec); - } - - /** - * Decode a PEM encoded public key string to an RSA PublicKey - * - * @param pemEncoded - PEM string for private key - * @return PublicKey - * @throws Exception on decode failure - */ - public static PublicKey decodePublicKey(String pemEncoded) throws Exception { - byte[] encodedBytes = toEncodedBytes(pemEncoded); - - X509EncodedKeySpec spec = new X509EncodedKeySpec(encodedBytes); - KeyFactory kf = KeyFactory.getInstance("RSA"); - return kf.generatePublic(spec); - } - - private static byte[] toEncodedBytes(final String pemEncoded) { - final String normalizedPem = removeBeginEnd(pemEncoded); - return Base64.getDecoder().decode(normalizedPem); - } - - private static String removeBeginEnd(String pem) { - pem = pem.replaceAll("-----BEGIN (.*)-----", ""); - pem = pem.replaceAll("-----END (.*)----", ""); - pem = pem.replaceAll("\r\n", ""); - pem = pem.replaceAll("\n", ""); - return pem.trim(); - } - - /** - * @return the current time in seconds since epoch - */ - public static int currentTimeInSecs() { - long currentTimeMS = System.currentTimeMillis(); - return (int) (currentTimeMS / 1000); - } - -} diff --git a/security-jwt-quickstart/src/test/resources/JwtClaims.json b/security-jwt-quickstart/src/test/resources/JwtClaims.json deleted file mode 100644 index 893b34a8ee..0000000000 --- a/security-jwt-quickstart/src/test/resources/JwtClaims.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "iss": "https://quarkus.io/using-jwt-rbac", - "jti": "a-123", - "sub": "jdoe-using-jwt-rbac", - "upn": "jdoe@quarkus.io", - "preferred_username": "jdoe", - "aud": "using-jwt-rbac", - "birthdate": "2001-07-13", - "roleMappings": { - "group1": "Group1MappedRole", - "group2": "Group2MappedRole" - }, - "groups": [ - "Echoer", - "Tester", - "Subscriber", - "group2" - ] -}