Skip to content

Commit

Permalink
Adding MultiIssuer Handling
Browse files Browse the repository at this point in the history
  • Loading branch information
cuioss committed Jan 24, 2025
1 parent 8af6f07 commit ff86d4f
Show file tree
Hide file tree
Showing 18 changed files with 718 additions and 161 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package de.cuioss.portal.authentication.token;

import de.cuioss.tools.logging.CuiLogger;
import de.cuioss.tools.string.MoreStrings;
import de.cuioss.tools.string.Splitter;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.eclipse.microprofile.jwt.JsonWebToken;

import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

/**
* A simplified JWT parser that can extract claims from a token without validating
* its signature. This is useful for inspecting token content, like the issuer,
* before deciding which actual validator to use.
*
* <p>Security considerations:
* - Implements size checks to prevent overflow attacks
* - Uses standard Java Base64 decoder
* - Does not validate signatures, only for inspection
*
* @author Generated
*/
@ToString
@EqualsAndHashCode
public class JwtTokenParser {

private static final CuiLogger LOGGER = new CuiLogger(JwtTokenParser.class);

/**
* Maximum size of a JWT token in bytes to prevent overflow attacks.
* 8KB should be more than enough for any reasonable JWT token.
*/
private static final int MAX_TOKEN_SIZE = 8 * 1024;

/**
* Maximum size of decoded JSON payload in bytes.
* 16KB should be more than enough for any reasonable JWT claims.
*/
private static final int MAX_PAYLOAD_SIZE = 16 * 1024;

/**
* Parses a JWT token without validating its signature and returns a JsonWebToken.
*
* @param token the JWT token string to parse
* @return an Optional containing the JsonWebToken if parsing is successful,
* or empty if the token is invalid or cannot be parsed
*/
public Optional<JsonWebToken> unsecured(String token) {
if (MoreStrings.isEmpty(token)) {
LOGGER.info("Token is empty or null");
return Optional.empty();
}

if (token.getBytes(StandardCharsets.UTF_8).length > MAX_TOKEN_SIZE) {
LOGGER.warn("Token exceeds maximum size limit of %s bytes", MAX_TOKEN_SIZE);
return Optional.empty();
}
var parts = Splitter.on('.').splitToList(token);
if (parts.size() != 3) {
LOGGER.info("Invalid JWT token format: expected 3 parts but got %s", parts.size());
return Optional.empty();
}

try {
JsonObject claims = parsePayload(parts.get(1));
return Optional.of(new UnsecuredJsonWebToken(claims));
} catch (Exception e) {
LOGGER.info(e, "Failed to parse token: %s", e.getMessage());
LOGGER.debug(e, "Detailed parse error");
return Optional.empty();
}
}

private JsonObject parsePayload(String payload) {
byte[] decoded = Base64.getUrlDecoder().decode(payload);

if (decoded.length > MAX_PAYLOAD_SIZE) {
LOGGER.info("Decoded payload exceeds maximum size limit of %s bytes", MAX_PAYLOAD_SIZE);
throw new IllegalStateException("Decoded payload exceeds maximum size limit");
}

try (var reader = Json.createReader(new StringReader(new String(decoded, StandardCharsets.UTF_8)))) {
return reader.readObject();
}
}

/**
* Simple implementation of JsonWebToken that holds claims without validation.
*/
private static class UnsecuredJsonWebToken implements JsonWebToken {
private final JsonObject claims;

UnsecuredJsonWebToken(JsonObject claims) {
this.claims = claims;
}

@Override
public String getName() {
return getStringClaim("name");
}

@Override
public Set<String> getClaimNames() {
return claims.keySet();
}

@SuppressWarnings("unchecked")
@Override
public <T> T getClaim(String claimName) {
JsonValue value = claims.get(claimName);
if (value == null) {
return null;
}

return (T) switch (value.getValueType()) {
case STRING -> ((JsonString) value).getString();
case NUMBER -> claims.getJsonNumber(claimName).longValue();
case ARRAY -> {
Set<String> result = new HashSet<>();
claims.getJsonArray(claimName).forEach(item -> {
if (item instanceof JsonString) {
result.add(((JsonString) item).getString());
}
});
yield result;
}
default -> null;
};
}

private String getStringClaim(String name) {
if (!claims.containsKey(name)) {
return null;
}
JsonValue value = claims.get(name);
return value.getValueType() == JsonValue.ValueType.STRING
? ((JsonString) value).getString()
: null;
}

@Override
public String getRawToken() {
return null; // Not needed for inspection
}

@Override
public String getIssuer() {
return getStringClaim("iss");
}

@Override
public String getSubject() {
return getStringClaim("sub");
}

@Override
public Set<String> getAudience() {
return Collections.emptySet(); // Not needed for inspection
}

@Override
public long getExpirationTime() {
return claims.containsKey("exp") ? claims.getJsonNumber("exp").longValue() : 0;
}

@Override
public long getIssuedAtTime() {
return claims.containsKey("iat") ? claims.getJsonNumber("iat").longValue() : 0;
}

@Override
public String getTokenID() {
return getStringClaim("jti");
}

@Override
public Set<String> getGroups() {
return Set.of(); // Not needed for inspection
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package de.cuioss.portal.authentication.token;

import io.smallrye.jwt.auth.principal.JWTParser;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.ToString;
import org.eclipse.microprofile.jwt.JsonWebToken;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
* Manages multiple {@link JwksAwareTokenParser} instances for different issuers.
* Provides functionality to inspect JWT tokens and determine the appropriate parser
* based on the issuer.
*
* @author Generated
*/
@ToString
@EqualsAndHashCode
public class MultiIssuerTokenParser {

private final Map<String, JWTParser> issuerToParser;
private final JwtTokenParser inspectionParser;

/**
* Constructor taking a map of issuer URLs to their corresponding parsers.
*
* @param issuerToParser Map containing issuer URLs as keys and their corresponding
* {@link JwksAwareTokenParser} instances as values. Must not be null.
*/
public MultiIssuerTokenParser(@NonNull Map<String, JWTParser> issuerToParser) {
this.issuerToParser = new HashMap<>(issuerToParser);
this.inspectionParser = new JwtTokenParser();
}

/**
* Creates a new builder for {@link MultiIssuerTokenParser}
*
* @return a new {@link Builder} instance
*/
public static Builder builder() {
return new Builder();
}

/**
* Inspects a JWT token to determine its issuer without validating the signature.
*
* @param token the JWT token to inspect, must not be null
* @return the issuer of the token if present
*/
public Optional<String> extractIssuer(@NonNull String token) {
return inspectionParser.unsecured(token)
.map(JsonWebToken::getIssuer);
}

/**
* Retrieves the appropriate {@link JWTParser} for a given issuer.
*
* @param issuer the issuer URL to find the parser for
* @return an Optional containing the parser if found, empty otherwise
*/
public Optional<JWTParser> getParserForIssuer(@NonNull String issuer) {
return Optional.ofNullable(issuerToParser.get(issuer));
}

/**
* Retrieves the appropriate {@link JWTParser} for a given token by first extracting
* its issuer.
*
* @param token the JWT token to find the parser for
* @return an Optional containing the parser if found, empty otherwise
*/
public Optional<JWTParser> getParserForToken(@NonNull String token) {
return extractIssuer(token)
.flatMap(this::getParserForIssuer);
}

/**
* Builder for {@link MultiIssuerTokenParser}
*/
public static class Builder {
private final Map<String, JWTParser> issuerToParser = new HashMap<>();

/**
* Adds a parser for a specific issuer
*
* @param parser the parser for that issuer
* @return this builder instance
*/
public Builder addParser(@NonNull JwksAwareTokenParser parser) {
issuerToParser.put(parser.getJwksIssuer(), parser);
return this;
}

/**
* Builds the {@link MultiIssuerTokenParser}
*
* @return a new instance of {@link MultiIssuerTokenParser}
*/
public MultiIssuerTokenParser build() {
return new MultiIssuerTokenParser(issuerToParser);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ public class ParsedAccessToken extends ParsedToken {
* @param tokenString to be parsed
* @param tokenParser the actual parser to be used
* @return an {@link ParsedAccessToken} if given Token can be parsed correctly,
* otherwise {@link ParsedAccessToken#EMPTY_WEB_TOKEN}
* {@code Optional#empty()} otherwise.
*/
public static ParsedAccessToken fromTokenString(String tokenString, @NonNull JWTParser tokenParser) {
public static Optional<ParsedAccessToken> fromTokenString(String tokenString, @NonNull JWTParser tokenParser) {
return fromTokenString(tokenString, null, tokenParser);
}

Expand All @@ -70,8 +70,10 @@ public static ParsedAccessToken fromTokenString(String tokenString, @NonNull JWT
* @return an {@link ParsedAccessToken} if given Token can be parsed correctly,
* {@code Optional#empty()} otherwise.
*/
public static ParsedAccessToken fromTokenString(String tokenString, String email, JWTParser tokenParser) {
return new ParsedAccessToken(jsonWebTokenFrom(tokenString, tokenParser, LOGGER), email);
public static Optional<ParsedAccessToken> fromTokenString(String tokenString, String email, JWTParser tokenParser) {
var rawToken = jsonWebTokenFrom(tokenString, tokenParser, LOGGER);

return rawToken.map(webToken -> new ParsedAccessToken(webToken, email));
}

private ParsedAccessToken(JsonWebToken jsonWebToken, String email) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ private ParsedIdToken(JsonWebToken jsonWebToken) {
* @param tokenString to be passed
* @param tokenParser to be passed
* @return an {@link ParsedIdToken} if given Token can be parsed correctly,
* otherwise {@link ParsedAccessToken#EMPTY_WEB_TOKEN}}
* otherwise {@link Optional#empty()}
*/
public static ParsedIdToken fromTokenString(String tokenString, JWTParser tokenParser) {
return new ParsedIdToken(jsonWebTokenFrom(tokenString, tokenParser, LOGGER));
public static Optional<ParsedIdToken> fromTokenString(String tokenString, JWTParser tokenParser) {
Optional<JsonWebToken> rawToken = jsonWebTokenFrom(tokenString, tokenParser, LOGGER);

return rawToken.map(ParsedIdToken::new);
}

/**
Expand Down
Loading

0 comments on commit ff86d4f

Please sign in to comment.