Skip to content

Commit 09b88c8

Browse files
authored
Support custom role claim (#1492)
1 parent 52d970f commit 09b88c8

7 files changed

Lines changed: 113 additions & 6 deletions

File tree

runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/config/JwtOptionsConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class JwtOptionsConfig extends OptionsConfig
2727
{
2828
public final String issuer;
2929
public final String audience;
30+
public final String roles;
3031
public final List<JwtKeyConfig> keys;
3132
public final Optional<Duration> challenge;
3233
public final String identity;
@@ -46,13 +47,15 @@ public static <T> JwtOptionsConfigBuilder<T> builder(
4647
JwtOptionsConfig(
4748
String issuer,
4849
String audience,
50+
String roles,
4951
List<JwtKeyConfig> keys,
5052
Duration challenge,
5153
String identity,
5254
String keysURL)
5355
{
5456
this.issuer = issuer;
5557
this.audience = audience;
58+
this.roles = roles;
5659
this.keys = keys;
5760
this.challenge = ofNullable(challenge);
5861
this.identity = identity;

runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/config/JwtOptionsConfigBuilder.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@
2424

2525
public class JwtOptionsConfigBuilder<T> extends ConfigBuilder<T, JwtOptionsConfigBuilder<T>>
2626
{
27+
public static final String ROLES_DEFAULT = "scope";
28+
2729
private final Function<OptionsConfig, T> mapper;
2830

2931
private String issuer;
3032
private String audience;
33+
private String roles;
3134
private List<JwtKeyConfig> keys;
3235
private Duration challenge;
3336
private String identity;
@@ -60,6 +63,13 @@ public JwtOptionsConfigBuilder<T> audience(
6063
return this;
6164
}
6265

66+
public JwtOptionsConfigBuilder<T> roles(
67+
String roles)
68+
{
69+
this.roles = roles;
70+
return this;
71+
}
72+
6373
public JwtOptionsConfigBuilder<T> challenge(
6474
Duration challenge)
6575
{
@@ -107,6 +117,8 @@ public JwtOptionsConfigBuilder<T> keysURL(
107117
@Override
108118
public T build()
109119
{
110-
return mapper.apply(new JwtOptionsConfig(issuer, audience, keys, challenge, identity, keysURL));
120+
String roles = this.roles != null ? this.roles : ROLES_DEFAULT;
121+
122+
return mapper.apply(new JwtOptionsConfig(issuer, audience, roles, keys, challenge, identity, keysURL));
111123
}
112124
}

runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardHandler.java

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,14 @@
5353

5454
public class JwtGuardHandler implements GuardHandler
5555
{
56+
private static final String SPLIT_VALUE_PATTERN = "\\s+";
57+
private static final String SPLIT_PATH_PATTERN = "\\.";
58+
5659
private final JsonWebSignature signature = new JsonWebSignature();
5760

5861
private final String issuer;
5962
private final String audience;
63+
private final String roles;
6064
private final Duration challenge;
6165
private final String identity;
6266
private final Map<String, JsonWebKey> keys;
@@ -72,6 +76,7 @@ public JwtGuardHandler(
7276
{
7377
this.issuer = options.issuer;
7478
this.audience = options.audience;
79+
this.roles = options.roles;
7580
this.challenge = options.challenge.orElse(null);
7681
this.identity = options.identity;
7782

@@ -179,11 +184,15 @@ public long reauthorize(
179184
break authorize;
180185
}
181186

182-
List<String> roles = Optional.ofNullable(claims.getClaimValue("scope"))
183-
.map(s -> s.toString().intern())
184-
.map(s -> s.split("\\s+"))
185-
.map(Arrays::asList)
186-
.orElse(null);
187+
Object rolesValue = claimValue(claims, this.roles);
188+
@SuppressWarnings("unchecked")
189+
List<String> roles = (rolesValue instanceof List)
190+
? (List<String>) rolesValue
191+
: Optional.ofNullable(rolesValue)
192+
.map(Object::toString)
193+
.map(s -> s.split(SPLIT_VALUE_PATTERN))
194+
.map(Arrays::asList)
195+
.orElse(null);
187196

188197
JwtSessionStore sessionStore = supplySessionStore(contextId);
189198
session = sessionStore.supplySession(identity, roles);
@@ -407,6 +416,33 @@ private void unshareIfNecessary()
407416
}
408417
}
409418

419+
private static Object claimValue(
420+
Object node,
421+
String path)
422+
{
423+
Object current = node;
424+
for (String part : path.split(SPLIT_PATH_PATTERN))
425+
{
426+
if (current == null)
427+
{
428+
break;
429+
}
430+
if (current instanceof JwtClaims)
431+
{
432+
current = ((JwtClaims) current).getClaimValue(part);
433+
}
434+
else if (current instanceof Map)
435+
{
436+
current = ((Map<?, ?>) current).get(part);
437+
}
438+
else
439+
{
440+
current = null;
441+
}
442+
}
443+
return current;
444+
}
445+
410446
private static String readKeys(
411447
Path keysPath)
412448
{

runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/config/JwtOptionsConfigAdapter.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515
package io.aklivity.zilla.runtime.guard.jwt.internal.config;
1616

17+
import static io.aklivity.zilla.runtime.guard.jwt.config.JwtOptionsConfigBuilder.ROLES_DEFAULT;
1718
import static java.util.Collections.emptyList;
1819

1920
import java.time.Duration;
@@ -38,6 +39,7 @@ public final class JwtOptionsConfigAdapter implements OptionsConfigAdapterSpi, J
3839
{
3940
private static final String ISSUER_NAME = "issuer";
4041
private static final String AUDIENCE_NAME = "audience";
42+
private static final String ROLES = "roles";
4143
private static final String KEYS_NAME = "keys";
4244
private static final String CHALLENGE_NAME = "challenge";
4345
private static final String IDENTITY_NAME = "identity";
@@ -76,6 +78,11 @@ public JsonObject adaptToJson(
7678
object.add(AUDIENCE_NAME, jwtOptions.audience);
7779
}
7880

81+
if (jwtOptions.roles != null && !ROLES_DEFAULT.equals(jwtOptions.roles))
82+
{
83+
object.add(ROLES, jwtOptions.roles);
84+
}
85+
7986
if (jwtOptions.keys != null)
8087
{
8188
JsonArrayBuilder newKeys = Json.createArrayBuilder();
@@ -118,6 +125,11 @@ public OptionsConfig adaptFromJson(
118125
jwtOptions.audience(object.getString(AUDIENCE_NAME));
119126
}
120127

128+
if (object.containsKey(ROLES))
129+
{
130+
jwtOptions.roles(object.getString(ROLES));
131+
}
132+
121133
if (object.containsKey(KEYS_NAME))
122134
{
123135
JsonValue keysValue = object.getValue(String.format("/%s", KEYS_NAME));

runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardHandlerTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import java.time.Clock;
3232
import java.time.Duration;
3333
import java.time.Instant;
34+
import java.util.List;
35+
import java.util.Map;
3436

3537
import org.agrona.collections.MutableLong;
3638
import org.jose4j.jws.JsonWebSignature;
@@ -650,6 +652,41 @@ public void shouldDeauthorize() throws Exception
650652
guard.deauthorize(sessionId);
651653
}
652654

655+
@Test
656+
public void shouldAuthorizeWithCustomRole() throws Exception
657+
{
658+
Duration challenge = ofSeconds(3L);
659+
JwtOptionsConfig options = JwtOptionsConfig.builder()
660+
.inject(identity())
661+
.issuer("test issuer")
662+
.audience("testAudience")
663+
.roles("realm_access.roles")
664+
.key(RFC7515_RS256_CONFIG)
665+
.challenge(challenge)
666+
.build();
667+
JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
668+
669+
Instant now = Instant.now();
670+
671+
JwtClaims claims = new JwtClaims();
672+
claims.setClaim("iss", "test issuer");
673+
claims.setClaim("aud", "testAudience");
674+
claims.setClaim("sub", "testSubject");
675+
claims.setClaim("exp", now.getEpochSecond() + 10L);
676+
claims.setClaim("realm_access",
677+
Map.of("roles", List.of("default-roles-backend", "offline_access", "uma_authorization")));
678+
String token = sign(claims.toJson(), "test", RFC7515_RS256, "RS256");
679+
680+
long sessionId = guard.reauthorize(0L, 0L, 101L, token);
681+
682+
assertThat(sessionId, not(equalTo(0L)));
683+
assertThat(guard.identity(sessionId), equalTo("testSubject"));
684+
assertThat(guard.expiresAt(sessionId), equalTo(ofSeconds(now.getEpochSecond() + 10L).toMillis()));
685+
assertThat(guard.expiringAt(sessionId), equalTo(ofSeconds(now.getEpochSecond() + 10L).minus(challenge).toMillis()));
686+
assertTrue(guard.verify(sessionId, asList("default-roles-backend", "offline_access", "uma_authorization")));
687+
assertFalse(guard.verify(sessionId, asList("admin")));
688+
}
689+
653690
static String sign(
654691
String payload,
655692
String kid,

specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/guard.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ guards:
2121
options:
2222
issuer: https://auth.example.com
2323
audience: https://api.example.com
24+
roles: "realm_access.roles"
2425
keys:
2526
- kty: EC
2627
crv: P-256

specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/schema/jwt.schema.patch.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
"title": "Audience",
4242
"type": "string"
4343
},
44+
"roles":
45+
{
46+
"title": "Roles",
47+
"type": "string",
48+
"default": "scope"
49+
},
4450
"keys":
4551
{
4652
"title": "Keys",

0 commit comments

Comments
 (0)