Skip to content

Commit

Permalink
Merge pull request quarkusio#34227 from sberyozkin/test_jwt_custom_claim
Browse files Browse the repository at this point in the history
Support custom claim types in quarkus-test-security-jwt and quarkus-test-security-oidc
  • Loading branch information
sberyozkin committed Jun 22, 2023
2 parents ae7a5cf + df3ddc0 commit 8e3bb8b
Show file tree
Hide file tree
Showing 11 changed files with 578 additions and 187 deletions.
6 changes: 6 additions & 0 deletions test-framework/security-jwt/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
<artifactId>junit-jupiter</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonp</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.jwt</groupId>
<artifactId>microprofile-jwt-auth-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,21 @@
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Claim {
/**
* Claim name
*/
String key();

/**
* Claim value
*/
String value();

/**
* Claim value type.
* If this type is set to {@link ClaimType#DEFAULT} then the value will be converted to String unless the claim
* is a standard claim such as `exp` (expiry), `iat` (issued at), `nbf` (not before), `auth_time` (authentication time)
* whose value will be converted to Long or `email_verified` whose value will be converted to Boolean.
*/
ClaimType type() default ClaimType.DEFAULT;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.quarkus.test.security.jwt;

import java.io.StringReader;

import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonReader;

public enum ClaimType {
LONG {
@Override
public Long convert(String value) {
return Long.parseLong(value);
}
},
INTEGER {
@Override
public Integer convert(String value) {
return Integer.parseInt(value);
}
},
BOOLEAN {
@Override
public Boolean convert(String value) {
return Boolean.parseBoolean(value);
}
},
STRING {
@Override
public String convert(String value) {
return value;
}
},
JSON_ARRAY {
@Override
public JsonArray convert(String value) {
try (JsonReader jsonReader = Json.createReader(new StringReader(value))) {
return jsonReader.readArray();
}
}
},
JSON_OBJECT {
@Override
public JsonObject convert(String value) {
try (JsonReader jsonReader = Json.createReader(new StringReader(value))) {
return jsonReader.readObject();
}
}
},
DEFAULT {
@Override
public String convert(String value) {
return value;
}
};

abstract Object convert(String value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package io.quarkus.test.security.jwt;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import jakarta.json.Json;
import jakarta.json.JsonValue;

import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.test.security.TestSecurityIdentityAugmentor;

public class JwtTestSecurityIdentityAugmentor implements TestSecurityIdentityAugmentor {
private static Map<String, ClaimType> standardClaimTypes = Map.of(
Claims.exp.name(), ClaimType.LONG,
Claims.iat.name(), ClaimType.LONG,
Claims.nbf.name(), ClaimType.LONG,
Claims.auth_time.name(), ClaimType.LONG,
Claims.email_verified.name(), ClaimType.BOOLEAN);

@Override
public SecurityIdentity augment(final SecurityIdentity identity, final Annotation[] annotations) {
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);

final JwtSecurity jwtSecurity = findJwtSecurity(annotations);
builder.setPrincipal(new JsonWebToken() {

@Override
public String getName() {
return identity.getPrincipal().getName();
}

@SuppressWarnings("unchecked")
@Override
public <T> T getClaim(String claimName) {
if (Claims.groups.name().equals(claimName)) {
return (T) identity.getRoles();
}
if (jwtSecurity != null && jwtSecurity.claims() != null) {
for (Claim claim : jwtSecurity.claims()) {
if (claim.key().equals(claimName)) {
return (T) wrapValue(claim, convertClaimValue(claim));
}
}
}
return null;
}

@Override
public Set<String> getClaimNames() {
if (jwtSecurity != null && jwtSecurity.claims() != null) {
return Arrays.stream(jwtSecurity.claims()).map(Claim::key).collect(Collectors.toSet());
}
return Collections.emptySet();
}

});

return builder.build();
}

private static JwtSecurity findJwtSecurity(Annotation[] annotations) {
for (Annotation ann : annotations) {
if (ann instanceof JwtSecurity) {
return (JwtSecurity) ann;
}
}
return null;
}

private Object wrapValue(Claim claim, Object convertedClaimValue) {
Claims claimType = getClaimType(claim.key());
if (Claims.UNKNOWN == claimType) {
if (convertedClaimValue instanceof Long) {
return Json.createValue((Long) convertedClaimValue);
} else if (convertedClaimValue instanceof Integer) {
return Json.createValue((Integer) convertedClaimValue);
} else if (convertedClaimValue instanceof Boolean) {
return (Boolean) convertedClaimValue ? JsonValue.TRUE : JsonValue.FALSE;
}
}
return convertedClaimValue;
}

protected Claims getClaimType(String claimName) {
Claims claimType;
try {
claimType = Claims.valueOf(claimName);
} catch (IllegalArgumentException e) {
claimType = Claims.UNKNOWN;
}
return claimType;
}

private Object convertClaimValue(Claim claim) {
ClaimType type = claim.type();
if (type == ClaimType.DEFAULT && standardClaimTypes.containsKey(claim.key())) {
type = standardClaimTypes.get(claim.key());
}
return type.convert(claim.value());
}

}
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
package io.quarkus.test.security.jwt;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;

import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.arc.Unremovable;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.test.security.TestSecurityIdentityAugmentor;

@ApplicationScoped
Expand All @@ -25,57 +14,4 @@ public class JwtTestSecurityIdentityAugmentorProducer {
public TestSecurityIdentityAugmentor produce() {
return new JwtTestSecurityIdentityAugmentor();
}

private static class JwtTestSecurityIdentityAugmentor implements TestSecurityIdentityAugmentor {

@Override
public SecurityIdentity augment(final SecurityIdentity identity, final Annotation[] annotations) {
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);

final JwtSecurity jwtSecurity = findJwtSecurity(annotations);
builder.setPrincipal(new JsonWebToken() {

@Override
public String getName() {
return identity.getPrincipal().getName();
}

@SuppressWarnings("unchecked")
@Override
public <T> T getClaim(String claimName) {
if (Claims.groups.name().equals(claimName)) {
return (T) identity.getRoles();
}
if (jwtSecurity != null && jwtSecurity.claims() != null) {
for (Claim claim : jwtSecurity.claims()) {
if (claim.key().equals(claimName)) {
return (T) claim.value();
}
}
}
return null;
}

@Override
public Set<String> getClaimNames() {
if (jwtSecurity != null && jwtSecurity.claims() != null) {
return Arrays.stream(jwtSecurity.claims()).map(Claim::key).collect(Collectors.toSet());
}
return Collections.emptySet();
}

});

return builder.build();
}

private JwtSecurity findJwtSecurity(Annotation[] annotations) {
for (Annotation ann : annotations) {
if (ann instanceof JwtSecurity) {
return (JwtSecurity) ann;
}
}
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.quarkus.test.security.jwt;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.lang.annotation.Annotation;
import java.security.Principal;
import java.util.Set;

import jakarta.json.JsonArray;
import jakarta.json.JsonNumber;
import jakarta.json.JsonObject;
import jakarta.json.JsonValue;

import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.junit.jupiter.api.Test;

import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;

public class JwtTestSecurityIdentityAugmentorTest {

@Test
@JwtSecurity(claims = {
@Claim(key = "exp", value = "123456789"),
@Claim(key = "iat", value = "123456788"),
@Claim(key = "nbf", value = "123456787"),
@Claim(key = "auth_time", value = "123456786"),
@Claim(key = "customlong", value = "123456785", type = ClaimType.LONG),
@Claim(key = "email", value = "user@gmail.com"),
@Claim(key = "email_verified", value = "true"),
@Claim(key = "email_checked", value = "false", type = ClaimType.BOOLEAN),
@Claim(key = "jsonarray_claim", value = "[\"1\", \"2\"]", type = ClaimType.JSON_ARRAY),
@Claim(key = "jsonobject_claim", value = "{\"a\":\"1\", \"b\":\"2\"}", type = ClaimType.JSON_OBJECT)
})
public void testClaimValues() throws Exception {
SecurityIdentity identity = QuarkusSecurityIdentity.builder()
.setPrincipal(new Principal() {
@Override
public String getName() {
return "alice";
}

})
.addRole("user")
.build();

JwtTestSecurityIdentityAugmentor augmentor = new JwtTestSecurityIdentityAugmentor();

Annotation[] annotations = JwtTestSecurityIdentityAugmentorTest.class.getMethod("testClaimValues").getAnnotations();
JsonWebToken jwt = (JsonWebToken) augmentor.augment(identity, annotations).getPrincipal();

assertEquals("alice", jwt.getName());
assertEquals(Set.of("user"), jwt.getGroups());

assertEquals(123456789, jwt.getExpirationTime());
assertEquals(123456788, jwt.getIssuedAtTime());
assertEquals(123456787, (Long) jwt.getClaim(Claims.nbf.name()));
assertEquals(123456786, (Long) jwt.getClaim(Claims.auth_time.name()));
assertEquals(123456785, ((JsonNumber) jwt.getClaim("customlong")).longValue());
assertEquals("user@gmail.com", jwt.getClaim(Claims.email));
assertTrue((Boolean) jwt.getClaim(Claims.email_verified.name()));
assertEquals(JsonValue.FALSE, jwt.getClaim("email_checked"));

JsonArray array = jwt.getClaim("jsonarray_claim");
assertEquals("1", array.getString(0));
assertEquals("2", array.getString(1));

JsonObject map = jwt.getClaim("jsonobject_claim");
assertEquals("1", map.getString("a"));
assertEquals("2", map.getString("b"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,21 @@
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Claim {
/**
* Claim name
*/
String key();

/**
* Claim value
*/
String value();

/**
* Claim value type.
* If this type is set to {@link ClaimType#DEFAULT} then the value will be converted to String unless the claim
* is a standard claim such as `exp` (expiry), `iat` (issued at), `nbf` (not before), `auth_time` (authentication time)
* whose value will be converted to Long or `email_verified` whose value will be converted to Boolean.
*/
ClaimType type() default ClaimType.DEFAULT;
}
Loading

0 comments on commit 8e3bb8b

Please sign in to comment.