-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor security/jwt in order to use JWT builder inside a Quarkus ap…
…plication
- Loading branch information
pablo gonzalez granados
committed
May 30, 2022
1 parent
de8158c
commit 147aac8
Showing
4 changed files
with
104 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
77 changes: 77 additions & 0 deletions
77
security/jwt/src/main/java/io/quarkus/ts/security/jwt/GenerateJwtResource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package io.quarkus.ts.security.jwt; | ||
|
||
import java.security.KeyPair; | ||
import java.security.KeyPairGenerator; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.PrivateKey; | ||
import java.security.interfaces.RSAPrivateKey; | ||
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; | ||
private static final int NINETY = 90; | ||
|
||
@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 = (RSAPrivateKey) 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", "[email protected]") | ||
.claim("roleMappings", Collections.singletonMap("admin", "superuser")); | ||
|
||
if (!Objects.isNull(privateKey)) { | ||
return jwtbuilder.jws().sign(privateKey); | ||
} | ||
|
||
return jwtbuilder.sign(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,30 @@ | ||
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.Objects; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.function.Supplier; | ||
|
||
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; | ||
|
||
@Test | ||
public void securedEveryoneNoGroup() throws Exception { | ||
givenWithToken(createToken()) | ||
givenWithToken(createToken("")) | ||
.get("/secured/everyone") | ||
.then() | ||
.statusCode(HttpStatus.SC_OK) | ||
|
@@ -70,31 +57,31 @@ public void securedEveryoneAdminGroup() throws Exception { | |
|
||
@Test | ||
public void securedEveryoneWrongIssuer() throws Exception { | ||
givenWithToken(createToken(Invalidity.WRONG_ISSUER)) | ||
givenWithToken(createToken(Invalidity.WRONG_ISSUER, "")) | ||
.get("/secured/everyone") | ||
.then() | ||
.statusCode(HttpStatus.SC_UNAUTHORIZED); | ||
} | ||
|
||
@Test | ||
public void securedEveryoneWrongDate() throws Exception { | ||
givenWithToken(createToken(Invalidity.WRONG_DATE)) | ||
givenWithToken(createToken(Invalidity.WRONG_DATE, "")) | ||
.get("/secured/everyone") | ||
.then() | ||
.statusCode(HttpStatus.SC_UNAUTHORIZED); | ||
} | ||
|
||
@Test | ||
public void securedEveryoneWrongKey() throws Exception { | ||
givenWithToken(createToken(Invalidity.WRONG_KEY)) | ||
givenWithToken(createToken(Invalidity.WRONG_KEY, "")) | ||
.get("/secured/everyone") | ||
.then() | ||
.statusCode(HttpStatus.SC_UNAUTHORIZED); | ||
} | ||
|
||
@Test | ||
public void securedAdminNoGroup() throws Exception { | ||
givenWithToken(createToken()) | ||
givenWithToken(createToken("")) | ||
.get("/secured/admin") | ||
.then() | ||
.statusCode(HttpStatus.SC_FORBIDDEN); | ||
|
@@ -119,7 +106,7 @@ public void securedAdminAdminGroup() throws Exception { | |
|
||
@Test | ||
public void securedNoOneNoGroup() throws Exception { | ||
givenWithToken(createToken()) | ||
givenWithToken(createToken("")) | ||
.get("/secured/noone") | ||
.then() | ||
.statusCode(HttpStatus.SC_FORBIDDEN); | ||
|
@@ -143,7 +130,7 @@ public void securedNoOneAdminGroup() throws Exception { | |
|
||
@Test | ||
public void permittedCorrectToken() throws Exception { | ||
givenWithToken(createToken()) | ||
givenWithToken(createToken("")) | ||
.get("/permitted") | ||
.then() | ||
.statusCode(HttpStatus.SC_OK) | ||
|
@@ -152,39 +139,39 @@ public void permittedCorrectToken() throws Exception { | |
|
||
@Test | ||
public void permittedWrongIssuer() throws Exception { | ||
givenWithToken(createToken(Invalidity.WRONG_ISSUER)) | ||
givenWithToken(createToken(Invalidity.WRONG_ISSUER, "")) | ||
.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)) | ||
givenWithToken(createToken(Invalidity.WRONG_DATE, "")) | ||
.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)) | ||
givenWithToken(createToken(Invalidity.WRONG_KEY, "")) | ||
.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()) | ||
givenWithToken(createToken("")) | ||
.get("/denied") | ||
.then() | ||
.statusCode(HttpStatus.SC_FORBIDDEN); | ||
} | ||
|
||
@Test | ||
public void mixedConstrained() throws Exception { | ||
givenWithToken(createToken()) | ||
givenWithToken(createToken("")) | ||
.get("/mixed/constrained") | ||
.then() | ||
.statusCode(HttpStatus.SC_OK) | ||
|
@@ -193,7 +180,7 @@ public void mixedConstrained() throws Exception { | |
|
||
@Test | ||
public void mixedUnconstrained() throws Exception { | ||
givenWithToken(createToken()) | ||
givenWithToken(createToken("")) | ||
.get("/mixed/unconstrained") | ||
.then() | ||
.statusCode(HttpStatus.SC_FORBIDDEN); // quarkus.security.deny-unannotated-members=true | ||
|
@@ -279,7 +266,7 @@ public void tokenExpirationGracePeriod() throws Exception { | |
return now; | ||
}; | ||
|
||
givenWithToken(createToken(clock, null, "admin")) | ||
givenWithToken(createToken("admin")) | ||
.get("/secured/admin") | ||
.then() | ||
.statusCode(HttpStatus.SC_OK) | ||
|
@@ -288,64 +275,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) throws Exception { | ||
return createToken(null, group); | ||
} | ||
|
||
private static String createToken(Supplier<Date> 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", "[email protected]") | ||
.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 | ||
|