Skip to content

Commit

Permalink
Jwt now support multiple issuers and multi issuer validation
Browse files Browse the repository at this point in the history
Signed-off-by: David Kral <[email protected]>
  • Loading branch information
Verdent committed Dec 16, 2022
1 parent 23b8e59 commit f8db1b4
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 9 deletions.
84 changes: 75 additions & 9 deletions security/jwt/src/main/java/io/helidon/security/jwt/Jwt.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -68,7 +69,7 @@ public class Jwt {

// iss
// "iss":"accounts.google.com",
private final Optional<String> issuer;
private final Optional<List<String>> issuers;
// exp
// "exp":1495734457,
/*
Expand Down Expand Up @@ -226,7 +227,11 @@ public class Jwt {
this.payloadClaims = getClaims(payloadJson);

// known payload
this.issuer = JwtUtil.getString(payloadJson, "iss");
if (payloadJson.get("iss") instanceof JsonString) {
this.issuers = JwtUtil.getString(payloadJson, "iss").map(List::of);
} else {
this.issuers = JwtUtil.getStrings(payloadJson, "iss");
}
this.expirationTime = JwtUtil.toInstant(payloadJson, "exp");
this.issueTime = JwtUtil.toInstant(payloadJson, "iat");
this.notBefore = JwtUtil.toInstant(payloadJson, "nbf");
Expand Down Expand Up @@ -284,12 +289,12 @@ private Jwt(Builder builder) {
this.headers = builder.headerBuilder.build();

// known payload
this.issuer = builder.issuer;
this.issuers = Optional.ofNullable(builder.issuers).map(List::copyOf);
this.expirationTime = builder.expirationTime;
this.issueTime = builder.issueTime;
this.notBefore = builder.notBefore;
this.subject = builder.subject.or(() -> toOptionalString(builder.payloadClaims, "sub"));
this.audience = Optional.ofNullable(builder.audience);
this.audience = Optional.ofNullable(builder.audience).map(List::copyOf);
this.jwtId = builder.jwtId;
this.email = builder.email.or(() -> toOptionalString(builder.payloadClaims, "email"));
this.emailVerified = builder.emailVerified.or(() -> getClaim(builder.payloadClaims, "email_verified"));
Expand Down Expand Up @@ -388,7 +393,20 @@ public static List<Validator<Jwt>> defaultTimeValidators(Instant now,
* @param mandatory whether issuer field is mandatory in the token (true - mandatory, false - optional)
*/
public static void addIssuerValidator(Collection<Validator<Jwt>> validators, String issuer, boolean mandatory) {
validators.add(FieldValidator.create(Jwt::issuer, "Issuer", issuer, mandatory));
validators.add((jwt, collector) -> {
Optional<List<String>> maybeJwtIssuers = jwt.issuers();
if (maybeJwtIssuers.isPresent()) {
List<String> jwtIssuers = maybeJwtIssuers.get();
if (jwtIssuers.contains(issuer)) {
return;
}
collector.fatal(jwt, "Issuer must contain issuer \"" + issuer + "\", yet it contains: " + jwtIssuers);
} else {
if (mandatory) {
collector.fatal(jwt, "Issuer is expected to contain: " + issuer + ", yet no issuer is in JWT");
}
}
});
}

/**
Expand Down Expand Up @@ -545,7 +563,16 @@ public Optional<String> contentType() {
* @return Issuer or empty if claim is not defined
*/
public Optional<String> issuer() {
return issuer;
return issuers.filter(it -> !it.isEmpty()).map(it -> it.get(0));
}

/**
* All the issuer claim values.
*
* @return Issuer values or empty if claim is not defined
*/
public Optional<List<String>> issuers() {
return issuers;
}

/**
Expand Down Expand Up @@ -837,7 +864,15 @@ public JsonObject payloadJson() {
payloadClaims.forEach(objectBuilder::add);

// known payload
this.issuer.ifPresent(it -> objectBuilder.add("iss", it));
issuers.ifPresent(values -> {
if (values.size() == 1) {
objectBuilder.add("iss", values.get(0));
} else if (values.size() > 1) {
JsonArrayBuilder jab = JSON.createArrayBuilder();
values.forEach(jab::add);
objectBuilder.add("iss", jab);
}
});
this.expirationTime.ifPresent(it -> objectBuilder.add("exp", it.getEpochSecond()));
this.issueTime.ifPresent(it -> objectBuilder.add("iat", it.getEpochSecond()));
this.notBefore.ifPresent(it -> objectBuilder.add("nbf", it.getEpochSecond()));
Expand Down Expand Up @@ -1326,14 +1361,14 @@ public void validate(Jwt token, Errors.Collector collector) {
public static final class Builder implements io.helidon.common.Builder<Builder, Jwt> {
private final JwtHeaders.Builder headerBuilder = JwtHeaders.builder();
private final Map<String, Object> payloadClaims = new HashMap<>();
private Optional<String> issuer = Optional.empty();
private Optional<Instant> expirationTime = Optional.empty();
private Optional<Instant> issueTime = Optional.empty();
private Optional<Instant> notBefore = Optional.empty();
private Optional<String> subject = Optional.empty();
private Optional<String> userPrincipal = Optional.empty();
private Optional<List<String>> userGroups = Optional.empty();
private List<String> audience;
private List<String> issuers;
private Optional<String> jwtId = Optional.empty();
private Optional<String> email = Optional.empty();
private Optional<Boolean> emailVerified = Optional.empty();
Expand Down Expand Up @@ -1481,9 +1516,40 @@ public Builder algorithm(String algorithm) {
*
* @param issuer issuer name or URL
* @return updated builder instance
* @deprecated use {@link #addIssuer(String)} instead
*/
@Deprecated(since = "3.0.3", forRemoval = true)
public Builder issuer(String issuer) {
this.issuer = Optional.ofNullable(issuer);
return addIssuer(issuer);
}

/**
* The issuer claim identifies the principal that issued the JWT.
*
* See <a href="https://tools.ietf.org/html/rfc7519#section-4.1.1">RFC 7519, section 4.1.1</a>.
*
* @param issuer issuer of this JWT
* @return updated builder instance
*/
public Builder addIssuer(String issuer) {
if (this.issuers == null) {
this.issuers = new ArrayList<>();
}
this.issuers.add(issuer);
return this;
}

/**
* The issuer claim identifies the principal that issued the JWT.
* Replaces existing configured issuers.
*
* See <a href="https://tools.ietf.org/html/rfc7519#section-4.1.1">RFC 7519, section 4.1.1</a>.
*
* @param issuers issuers of this JWT
* @return updated builder instance
*/
public Builder issuers(List<String> issuers) {
this.issuers = new ArrayList<>(issuers);
return this;
}

Expand Down
53 changes: 53 additions & 0 deletions security/jwt/src/test/java/io/helidon/security/jwt/JwtTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Logger;

Expand All @@ -29,7 +31,9 @@
import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

/**
* Unit test for {@link Jwt}.
Expand Down Expand Up @@ -101,4 +105,53 @@ public void testOidcJwt() {
errors.log(LOGGER);
errors.checkValid();
}

@Test
public void testMultiIssuers() {
String audience = "id_of_audience";
String subject = "54564645646465";
String username = "[email protected]";
String issuer = "I am issuer";
String secondIssuer = "I am second issuer";
String invalidIssuer = "I am invalid issuer";
Instant now = Instant.now();

Jwt jwt = Jwt.builder()
.jwtId(UUID.randomUUID().toString())
.subject(subject)
.preferredUsername(username)
.algorithm(JwkRSA.ALG_RS256)
.addAudience(audience)
.addIssuer(issuer)
.addIssuer(secondIssuer)
// time info
.issueTime(now)
.build();

//and this one should be valid
List<Validator<Jwt>> vals = new ArrayList<>();
Jwt.addIssuerValidator(vals, issuer, true);
Jwt.addIssuerValidator(vals, secondIssuer, true);

Errors errors = jwt.validate(vals);

errors.log(LOGGER);
errors.checkValid();

//another try with defaults
errors = jwt.validate(issuer, audience);
errors.log(LOGGER);
errors.checkValid();

Errors.ErrorMessagesException exception = assertThrows(Errors.ErrorMessagesException.class, () -> {
List<Validator<Jwt>> validators = new ArrayList<>();
Jwt.addIssuerValidator(validators, invalidIssuer, true);
Errors errors2 = jwt.validate(validators);
errors2.log(LOGGER);
errors2.checkValid();
});
assertThat(exception.getMessage(),
startsWith("FATAL: Issuer must contain issuer \"I am invalid issuer\", "
+ "yet it contains: [I am issuer, I am second issuer]"));
}
}

0 comments on commit f8db1b4

Please sign in to comment.