Skip to content

Commit 1f5edb2

Browse files
Claims (#36)
* feat: expose more request data * docs: fix javadocs
1 parent 577ab05 commit 1f5edb2

File tree

8 files changed

+135
-45
lines changed

8 files changed

+135
-45
lines changed

grpc-context-utils/build.gradle.kts

+3
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ dependencies {
2020

2121
testImplementation("org.junit.jupiter:junit-jupiter:5.8.2")
2222
testImplementation("org.mockito:mockito-core:4.4.0")
23+
testImplementation("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
24+
testAnnotationProcessor("org.projectlombok:lombok:1.18.24")
25+
testCompileOnly("org.projectlombok:lombok:1.18.24")
2326
}

grpc-context-utils/src/main/java/org/hypertrace/core/grpcutils/context/Jwt.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.hypertrace.core.grpcutils.context;
22

3-
import java.util.List;
43
import java.util.Optional;
54

65
interface Jwt {
@@ -12,5 +11,5 @@ interface Jwt {
1211

1312
Optional<String> getEmail();
1413

15-
List<String> getRoles(String rolesClaim);
14+
Optional<JwtClaim> getClaim(String claimName);
1615
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.hypertrace.core.grpcutils.context;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
6+
public interface JwtClaim {
7+
/**
8+
* Get this Claim as a List of type T
9+
*
10+
* @param <T> type
11+
* @param tClazz the type class
12+
* @return An Optional containing the converted list of values if conversion succeeds
13+
*/
14+
<T> Optional<List<T>> asList(Class<T> tClazz);
15+
16+
/**
17+
* Get this Claim as a custom type T.
18+
*
19+
* @param <T> type
20+
* @param tClazz the type class
21+
* @return An Optional containing the converted value if conversion succeeds
22+
*/
23+
<T> Optional<T> as(Class<T> tClazz);
24+
}

grpc-context-utils/src/main/java/org/hypertrace/core/grpcutils/context/JwtParser.java

+38-22
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package org.hypertrace.core.grpcutils.context;
22

33
import com.auth0.jwt.JWT;
4+
import com.auth0.jwt.exceptions.JWTDecodeException;
5+
import com.auth0.jwt.interfaces.Claim;
46
import com.auth0.jwt.interfaces.DecodedJWT;
57
import com.google.common.cache.Cache;
68
import com.google.common.cache.CacheBuilder;
7-
import java.util.Collections;
89
import java.util.List;
910
import java.util.Optional;
1011
import java.util.concurrent.ExecutionException;
1112
import java.util.concurrent.TimeUnit;
13+
import java.util.function.Predicate;
1214
import org.slf4j.Logger;
1315
import org.slf4j.LoggerFactory;
1416

@@ -58,46 +60,60 @@ private DefaultJwt(DecodedJWT jwt) {
5860

5961
@Override
6062
public Optional<String> getUserId() {
61-
return Optional.ofNullable(jwt.getClaim(SUBJECT_CLAIM).asString());
63+
return this.getClaim(SUBJECT_CLAIM).flatMap(claim -> claim.as(String.class));
6264
}
6365

6466
@Override
6567
public Optional<String> getName() {
66-
return Optional.ofNullable(jwt.getClaim(NAME_CLAIM).asString());
68+
return this.getClaim(NAME_CLAIM).flatMap(claim -> claim.as(String.class));
6769
}
6870

6971
@Override
7072
public Optional<String> getPictureUrl() {
71-
return Optional.ofNullable(jwt.getClaim(PICTURE_CLAIM).asString());
73+
return this.getClaim(PICTURE_CLAIM).flatMap(claim -> claim.as(String.class));
7274
}
7375

7476
@Override
7577
public Optional<String> getEmail() {
76-
return Optional.ofNullable(jwt.getClaim(EMAIL_CLAIM).asString());
78+
return this.getClaim(EMAIL_CLAIM).flatMap(claim -> claim.as(String.class));
7779
}
7880

7981
@Override
80-
public List<String> getRoles(String rolesClaim) {
81-
List<String> roles = jwt.getClaim(rolesClaim).asList(String.class);
82-
if (roles == null || roles.isEmpty()) {
83-
return Collections.emptyList();
84-
}
85-
return roles;
82+
public Optional<JwtClaim> getClaim(String claimName) {
83+
return Optional.of(jwt.getClaim(claimName))
84+
.filter(Predicate.not(Claim::isNull))
85+
.map(DefaultJwtClaim::new);
8686
}
8787

8888
@Override
8989
public String toString() {
90-
final String emptyValue = "{}";
91-
return "DefaultJwt{"
92-
+ "userId="
93-
+ getUserId().orElse(emptyValue)
94-
+ ", name="
95-
+ getName().orElse(emptyValue)
96-
+ ", pictureUrl="
97-
+ getPictureUrl().orElse(emptyValue)
98-
+ ", email="
99-
+ getEmail().orElse(emptyValue)
100-
+ '}';
90+
return jwt.getClaims().toString();
91+
}
92+
}
93+
94+
private static class DefaultJwtClaim implements JwtClaim {
95+
private final Claim claim;
96+
97+
private DefaultJwtClaim(Claim claim) {
98+
this.claim = claim;
99+
}
100+
101+
@Override
102+
public <T> Optional<List<T>> asList(Class<T> tClazz) {
103+
try {
104+
return Optional.ofNullable(claim.asList(tClazz)).map(List::copyOf);
105+
} catch (Exception e) {
106+
return Optional.empty();
107+
}
108+
}
109+
110+
@Override
111+
public <T> Optional<T> as(Class<T> tClazz) throws JWTDecodeException {
112+
try {
113+
return Optional.ofNullable(claim.as(tClazz));
114+
} catch (Exception e) {
115+
return Optional.empty();
116+
}
101117
}
102118
}
103119
}

grpc-context-utils/src/main/java/org/hypertrace/core/grpcutils/context/RequestContext.java

+15-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.hypertrace.core.grpcutils.context.RequestContextConstants.CACHE_MEANINGFUL_HEADERS;
44
import static org.hypertrace.core.grpcutils.context.RequestContextConstants.TENANT_ID_HEADER_KEY;
55

6+
import com.google.common.collect.Maps;
67
import io.grpc.Context;
78
import io.grpc.Metadata;
89
import java.nio.charset.StandardCharsets;
@@ -81,8 +82,15 @@ public Optional<String> getEmail() {
8182
return getJwt().flatMap(Jwt::getEmail);
8283
}
8384

85+
@Deprecated
8486
public List<String> getRoles(String rolesClaim) {
85-
return getJwt().map(jwt -> jwt.getRoles(rolesClaim)).orElse(Collections.emptyList());
87+
return getClaim(rolesClaim)
88+
.flatMap(claim -> claim.asList(String.class))
89+
.orElse(Collections.emptyList());
90+
}
91+
92+
public Optional<JwtClaim> getClaim(String claimName) {
93+
return getJwt().flatMap(jwt -> jwt.getClaim(claimName));
8694
}
8795

8896
public Optional<String> getRequestId() {
@@ -175,12 +183,17 @@ public <T> ContextualKey<T> buildInternalContextualKey(T data) {
175183
return new DefaultContextualKey<>(this, data, List.of(TENANT_ID_HEADER_KEY));
176184
}
177185

186+
private Map<String, String> getHeadersOtherThanAuth() {
187+
return Maps.filterKeys(
188+
headers, key -> !key.equals(RequestContextConstants.AUTHORIZATION_HEADER));
189+
}
190+
178191
@Override
179192
public String toString() {
180193
final String emptyValue = "{}";
181194
return "RequestContext{"
182195
+ "headers="
183-
+ headers
196+
+ getHeadersOtherThanAuth()
184197
+ ", jwt="
185198
+ getJwt().map(Jwt::toString).orElse(emptyValue)
186199
+ '}';

grpc-context-utils/src/test/java/org/hypertrace/core/grpcutils/context/JwtParserTest.java

+50-15
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,19 @@
88
import static org.mockito.Mockito.verify;
99
import static org.mockito.Mockito.when;
1010

11+
import com.fasterxml.jackson.annotation.JsonProperty;
1112
import com.google.common.collect.ImmutableList;
12-
import java.util.Collections;
1313
import java.util.List;
1414
import java.util.Optional;
15+
import lombok.AllArgsConstructor;
16+
import lombok.NoArgsConstructor;
17+
import lombok.Value;
1518
import org.junit.jupiter.api.Test;
1619
import org.mockito.ArgumentMatchers;
1720

1821
class JwtParserTest {
1922
private final String testJwt =
2023
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MjEzNjM1OTcsImV4cCI6MTY1Mjg5OTU5NywiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJuYW1lIjoiSm9obm55IFJvY2tldCIsImVtYWlsIjoianJvY2tldEBleGFtcGxlLmNvbSIsInBpY3R1cmUiOiJ3d3cuZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJzdXBlcl91c2VyIiwidXNlciIsImJpbGxpbmdfYWRtaW4iXX0.lEDjPPCjr-Epv6pNslq-HK9vmxfstp1sY85GstlbU1I";
21-
private final String emptyRolesJwt =
22-
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MjEzNjM1OTcsImV4cCI6MTY1Mjg5OTU5NywiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJuYW1lIjoiSm9obm55IFJvY2tldCIsImVtYWlsIjoianJvY2tldEBleGFtcGxlLmNvbSIsInBpY3R1cmUiOiJ3d3cuZXhhbXBsZS5jb20iLCJodHRwczovL3RyYWNlYWJsZS5haS9yb2xlcyI6W119.sFUMZNyypj379xy5P4kqTbBXBOR5XvX2nhpKx6YiiwU";
23-
private final String noRolesJwt =
24-
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MjEzNjM1OTcsImV4cCI6MTY1Mjg5OTU5NywiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJuYW1lIjoiSm9obm55IFJvY2tldCIsImVtYWlsIjoianJvY2tldEBleGFtcGxlLmNvbSIsInBpY3R1cmUiOiJ3d3cuZXhhbXBsZS5jb20ifQ.Ui1Z2RhiVe3tq6uJPgcyjsfDBdeOeINs_gXEHC6cdpU";
2524
private final String testJwtUserId = "[email protected]";
2625
private final String testJwtName = "Johnny Rocket";
2726
private final String testJwtPictureUrl = "www.example.com";
@@ -65,23 +64,59 @@ void testExtractBearerTokenReturnsEmptyOnMalformed() {
6564
}
6665

6766
@Test
68-
void testRolesCanBeParsedFromToken() {
67+
void testClaimCanBeParsedFromToken() {
6968
JwtParser parser = new JwtParser();
7069
Optional<Jwt> jwt = parser.fromJwt(testJwt);
71-
assertEquals(Optional.of(testRoles), jwt.map(j -> j.getRoles(testRolesClaim)));
70+
assertEquals(
71+
Optional.of(testRoles),
72+
jwt.flatMap(j -> j.getClaim(testRolesClaim)).flatMap(claim -> claim.asList(String.class)));
7273
}
7374

7475
@Test
75-
void testRolesAreEmptyIfRolesArrayIsEmptyInJwt() {
76+
void testCanParseObjectClaim() {
77+
String jwtWithObjectArrayClaim =
78+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MjEzNjM1OTcsImV4cCI6MTY1Mjg5OTU5NywiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJuYW1lIjoiSm9obm55IFJvY2tldCIsImVtYWlsIjoianJvY2tldEBleGFtcGxlLmNvbSIsInBpY3R1cmUiOiJ3d3cuZXhhbXBsZS5jb20iLCJyb2xlcyI6W3siaWQiOiJzdXBlcl91c2VyIiwidmFsdWVzIjpbXX0seyJpZCI6InVzZXIiLCJ2YWx1ZXMiOlsiZm9vIl19XX0.EJyZWwbfbCAS4NJdwURAsOewf8V6863D1ZqXGTVZigE";
79+
/*
80+
{
81+
"iss": "Online JWT Builder",
82+
"iat": 1621363597,
83+
"exp": 1652899597,
84+
"aud": "www.example.com",
85+
86+
"GivenName": "Johnny",
87+
"Surname": "Rocket",
88+
"name": "Johnny Rocket",
89+
"email": "[email protected]",
90+
"picture": "www.example.com",
91+
"roles": [
92+
{
93+
"id": "super_user",
94+
"values": []
95+
},
96+
{
97+
"id": "user",
98+
"values": [
99+
"foo"
100+
]
101+
}
102+
]
103+
}
104+
*/
76105
JwtParser parser = new JwtParser();
77-
Optional<Jwt> jwt = parser.fromJwt(emptyRolesJwt);
78-
assertEquals(Optional.of(Collections.emptyList()), jwt.map(j -> j.getRoles(testRolesClaim)));
106+
Optional<Jwt> jwt = parser.fromJwt(jwtWithObjectArrayClaim);
107+
108+
assertEquals(
109+
Optional.of(
110+
List.of(
111+
new TestObject("super_user", List.of()), new TestObject("user", List.of("foo")))),
112+
jwt.flatMap(j -> j.getClaim("roles")).flatMap(claim -> claim.asList(TestObject.class)));
79113
}
80114

81-
@Test
82-
void testRolesAreEmptyIfRolesIfNoRolesClaimInToken() {
83-
JwtParser parser = new JwtParser();
84-
Optional<Jwt> jwt = parser.fromJwt(noRolesJwt);
85-
assertEquals(Optional.of(Collections.emptyList()), jwt.map(j -> j.getRoles(testRolesClaim)));
115+
@Value
116+
@NoArgsConstructor(force = true)
117+
@AllArgsConstructor
118+
private static class TestObject {
119+
@JsonProperty String id;
120+
@JsonProperty List<String> values;
86121
}
87122
}

grpc-server-rx-utils/build.gradle.kts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ dependencies {
1010
api("io.reactivex.rxjava3:rxjava:3.1.4")
1111
api("io.grpc:grpc-stub")
1212

13-
annotationProcessor("org.projectlombok:lombok:1.18.22")
14-
compileOnly("org.projectlombok:lombok:1.18.22")
13+
annotationProcessor("org.projectlombok:lombok:1.18.24")
14+
compileOnly("org.projectlombok:lombok:1.18.24")
1515

1616
implementation("org.slf4j:slf4j-api:1.7.36")
1717

grpc-server-utils/build.gradle.kts

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ dependencies {
2121
implementation(project(":grpc-context-utils"))
2222
implementation("org.slf4j:slf4j-api:1.7.36")
2323

24-
annotationProcessor("org.projectlombok:lombok:1.18.22")
25-
compileOnly("org.projectlombok:lombok:1.18.22")
24+
annotationProcessor("org.projectlombok:lombok:1.18.24")
25+
compileOnly("org.projectlombok:lombok:1.18.24")
2626

2727
testImplementation("org.junit.jupiter:junit-jupiter:5.8.2")
2828
testImplementation("org.mockito:mockito-core:4.4.0")

0 commit comments

Comments
 (0)