Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support jwt guarded identity via custom token claim #1277

Merged
merged 4 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class JwtOptionsConfig extends OptionsConfig
public final String audience;
public final List<JwtKeyConfig> keys;
public final Optional<Duration> challenge;
public final String identity;
public final Optional<String> keysURL;

public static JwtOptionsConfigBuilder<JwtOptionsConfig> builder()
Expand All @@ -47,12 +48,14 @@ public static <T> JwtOptionsConfigBuilder<T> builder(
String audience,
List<JwtKeyConfig> keys,
Duration challenge,
String identity,
String keysURL)
{
this.issuer = issuer;
this.audience = audience;
this.keys = keys;
this.challenge = ofNullable(challenge);
this.identity = identity;
this.keysURL = ofNullable(keysURL);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class JwtOptionsConfigBuilder<T> extends ConfigBuilder<T, JwtOptionsConfi
private String audience;
private List<JwtKeyConfig> keys;
private Duration challenge;
private String identity;
private String keysURL;

JwtOptionsConfigBuilder(
Expand Down Expand Up @@ -66,6 +67,13 @@ public JwtOptionsConfigBuilder<T> challenge(
return this;
}

public JwtOptionsConfigBuilder<T> identity(
String identity)
{
this.identity = identity;
return this;
}

public JwtOptionsConfigBuilder<T> keys(
List<JwtKeyConfig> keys)
{
Expand Down Expand Up @@ -99,6 +107,6 @@ public JwtOptionsConfigBuilder<T> keysURL(
@Override
public T build()
{
return mapper.apply(new JwtOptionsConfig(issuer, audience, keys, challenge, keysURL));
return mapper.apply(new JwtOptionsConfig(issuer, audience, keys, challenge, identity, keysURL));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class JwtGuardHandler implements GuardHandler
private final String issuer;
private final String audience;
private final Duration challenge;
private final String identity;
private final Map<String, JsonWebKey> keys;
private final Long2ObjectHashMap<JwtSession> sessionsById;
private final LongSupplier supplyAuthorizedId;
Expand All @@ -72,14 +73,14 @@ public JwtGuardHandler(
this.issuer = options.issuer;
this.audience = options.audience;
this.challenge = options.challenge.orElse(null);
this.identity = options.identity;

List<JwtKeyConfig> keysConfig = options.keys;
if ((keysConfig == null || keysConfig.isEmpty()) && options.keysURL.isPresent())
{
JsonbConfig config = new JsonbConfig()
.withAdapters(new JwtKeySetConfigAdapter());
Jsonb jsonb = JsonbBuilder.newBuilder()
.withConfig(config)
.withConfig(new JsonbConfig()
.withAdapters(new JwtKeySetConfigAdapter()))
.build();
Path keysPath = context.resolvePath(options.keysURL.get());
String keysText = readKeys(keysPath);
Expand Down Expand Up @@ -128,7 +129,7 @@ public long reauthorize(
String credentials)
{
JwtSession session = null;
String subject = null;
String identity = null;
String reason = "";

authorize:
Expand Down Expand Up @@ -158,7 +159,7 @@ public long reauthorize(

String payload = signature.getPayload();
JwtClaims claims = JwtClaims.parse(payload);
subject = claims.getSubject();
identity = this.identity != null ? claims.getStringClaimValue(this.identity) : claims.getSubject();
NumericDate notBefore = claims.getNotBefore();
NumericDate notAfter = claims.getExpirationTime();
String issuer = claims.getIssuer();
Expand All @@ -185,7 +186,7 @@ public long reauthorize(
.orElse(null);

JwtSessionStore sessionStore = supplySessionStore(contextId);
session = sessionStore.supplySession(subject, roles);
session = sessionStore.supplySession(identity, roles);

session.credentials = credentials;
session.roles = roles;
Expand All @@ -204,7 +205,7 @@ public long reauthorize(
}
if (session == null)
{
event.authorizationFailed(traceId, bindingId, subject, reason);
event.authorizationFailed(traceId, bindingId, identity, reason);
}
return session != null ? session.authorized : NOT_AUTHORIZED;
}
Expand All @@ -231,7 +232,7 @@ public String identity(
long sessionId)
{
JwtSession session = sessionsById.get(sessionId);
return session != null ? session.subject : null;
return session != null ? session.identity : null;
}

@Override
Expand Down Expand Up @@ -298,51 +299,51 @@ private JwtSessionStore supplySessionStore(
private final class JwtSessionStore
{
private final long contextId;
private final Map<String, JwtSession> sessionsBySubject;
private final Map<String, JwtSession> sessionsByIdentity;

private JwtSessionStore(
long contextId)
{
this.contextId = contextId;
this.sessionsBySubject = new IdentityHashMap<>();
this.sessionsByIdentity = new IdentityHashMap<>();
}

private JwtSession supplySession(
String subject,
String identity,
List<String> roles)
{
String subjectKey = subject != null ? subject.intern() : null;
JwtSession session = sessionsBySubject.get(subjectKey);
String identityKey = identity != null ? identity.intern() : null;
JwtSession session = sessionsByIdentity.get(identityKey);

if (subjectKey == null || session != null && roles != null && !supersetOf(session, roles))
if (identityKey == null || session != null && roles != null && !supersetOf(session, roles))
{
session = newSession(subjectKey);
session = newSession(identityKey);
}
else
{
session = sessionsBySubject.computeIfAbsent(subjectKey, this::newSharedSession);
session = sessionsByIdentity.computeIfAbsent(identityKey, this::newSharedSession);
}

return session;
}

private JwtSession newSharedSession(
String subject)
String identity)
{
return new JwtSession(supplyAuthorizedId.getAsLong(), subject, this::onUnshared);
return new JwtSession(supplyAuthorizedId.getAsLong(), identity, this::onUnshared);
}

private JwtSession newSession(
String subject)
String identity)
{
return new JwtSession(supplyAuthorizedId.getAsLong(), subject);
return new JwtSession(supplyAuthorizedId.getAsLong(), identity);
}

private void onUnshared(
JwtSession session)
{
sessionsBySubject.remove(session.subject);
if (sessionsBySubject.isEmpty())
sessionsByIdentity.remove(session.identity);
if (sessionsByIdentity.isEmpty())
{
sessionStoresByContextId.remove(contextId);
}
Expand All @@ -352,7 +353,7 @@ private void onUnshared(
private final class JwtSession
{
private final long authorized;
private final String subject;
private final String identity;
private final Consumer<JwtSession> unshare;

private String credentials;
Expand All @@ -366,26 +367,26 @@ private final class JwtSession

private JwtSession(
long authorized,
String subject)
String identity)
{
this(authorized, subject, null);
this(authorized, identity, null);
}

private JwtSession(
long authorized,
String subject,
String identity,
Consumer<JwtSession> unshare)
{
this.authorized = authorized;
this.subject = subject;
this.identity = identity;
this.unshare = unshare;
}

boolean challenge(
long now)
{
final boolean challenge =
subject != null &&
identity != null &&
challengeAt <= now && now < expiresAt &&
challengedAt < challengeAt;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public final class JwtOptionsConfigAdapter implements OptionsConfigAdapterSpi, J
private static final String AUDIENCE_NAME = "audience";
private static final String KEYS_NAME = "keys";
private static final String CHALLENGE_NAME = "challenge";
private static final String IDENTITY_NAME = "identity";

private static final List<JwtKeyConfig> KEYS_DEFAULT = emptyList();

Expand Down Expand Up @@ -92,6 +93,11 @@ public JsonObject adaptToJson(
object.add(CHALLENGE_NAME, jwtOptions.challenge.get().getSeconds());
}

if (jwtOptions.identity != null)
{
object.add(IDENTITY_NAME, jwtOptions.identity);
}

return object.build();
}

Expand Down Expand Up @@ -149,6 +155,11 @@ public OptionsConfig adaptFromJson(
jwtOptions.challenge(Duration.ofSeconds(object.getInt(CHALLENGE_NAME)));
}

if (object.containsKey(IDENTITY_NAME))
{
jwtOptions.identity(object.getString(IDENTITY_NAME));
}

return jwtOptions.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,40 @@ public void shouldAuthorize() throws Exception
assertTrue(guard.verify(sessionId, asList("read:stream", "write:stream")));
}

@Test
public void shouldAuthorizeWithCustomIdentity() throws Exception
{
Duration challenge = ofSeconds(3L);
JwtOptionsConfig options = JwtOptionsConfig.builder()
.inject(identity())
.issuer("test issuer")
.audience("testAudience")
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.identity("username")
.build();
JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);

Instant now = Instant.now();

JwtClaims claims = new JwtClaims();
claims.setClaim("iss", "test issuer");
claims.setClaim("aud", "testAudience");
claims.setClaim("username", "johndoe");
claims.setClaim("exp", now.getEpochSecond() + 10L);
claims.setClaim("scope", "read:stream write:stream");

String token = sign(claims.toJson(), "test", RFC7515_RS256, "RS256");

long sessionId = guard.reauthorize(0L, 0L, 101L, token);

assertThat(sessionId, not(equalTo(0L)));
assertThat(guard.identity(sessionId), equalTo("johndoe"));
assertThat(guard.expiresAt(sessionId), equalTo(ofSeconds(now.getEpochSecond() + 10L).toMillis()));
assertThat(guard.expiringAt(sessionId), equalTo(ofSeconds(now.getEpochSecond() + 10L).minus(challenge).toMillis()));
assertTrue(guard.verify(sessionId, asList("read:stream", "write:stream")));
}

@Test
public void shouldChallengeDuringChallengeWindow() throws Exception
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@
{
"title": "Challenge",
"type": "integer"
},
"identity":
{
"title": "Identity",
"type": "string",
"default": "sub"
}
},
"additionalProperties": false
Expand Down