From ff86d4fd6408aa8c069caed94e24df98d40b38ed Mon Sep 17 00:00:00 2001
From: Oliver Wolff <23139298+cuioss@users.noreply.github.com>
Date: Fri, 24 Jan 2025 13:37:31 +0100
Subject: [PATCH] Adding MultiIssuer Handling
---
.../authentication/token/JwtTokenParser.java | 192 ++++++++++++++++++
.../token/MultiIssuerTokenParser.java | 106 ++++++++++
.../token/ParsedAccessToken.java | 10 +-
.../authentication/token/ParsedIdToken.java | 8 +-
.../authentication/token/ParsedToken.java | 44 +---
.../authentication/token/TokenFactory.java | 49 +++--
.../authentication/token/TokenType.java | 3 +
.../token/JwksAwareTokenParserTest.java | 54 +++--
.../token/JwtTokenParserTest.java | 131 ++++++++++++
.../token/MultiIssuerTokenParserTest.java | 82 ++++++++
.../token/ParsedAccessTokenTest.java | 77 ++++---
.../token/ParsedIdTokenTest.java | 12 +-
.../authentication/token/ParsedTokenTest.java | 28 ++-
.../token/TestTokenProducer.java | 5 +-
.../token/TokenFactoryTest.java | 42 ++--
.../authentication/token/TokenKeycloakIT.java | 15 +-
.../authentication/token/TokenTypeTest.java | 14 +-
.../src/test/resources/token/some-groups.json | 7 +
18 files changed, 718 insertions(+), 161 deletions(-)
create mode 100644 modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/JwtTokenParser.java
create mode 100644 modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/MultiIssuerTokenParser.java
create mode 100644 modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/JwtTokenParserTest.java
create mode 100644 modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/MultiIssuerTokenParserTest.java
create mode 100644 modules/authentication/portal-authentication-token/src/test/resources/token/some-groups.json
diff --git a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/JwtTokenParser.java b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/JwtTokenParser.java
new file mode 100644
index 00000000..3a6a0eb8
--- /dev/null
+++ b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/JwtTokenParser.java
@@ -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.
+ *
+ *
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 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 getClaimNames() {
+ return claims.keySet();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public 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 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 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 getGroups() {
+ return Set.of(); // Not needed for inspection
+ }
+ }
+}
diff --git a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/MultiIssuerTokenParser.java b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/MultiIssuerTokenParser.java
new file mode 100644
index 00000000..4fbf164d
--- /dev/null
+++ b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/MultiIssuerTokenParser.java
@@ -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 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 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 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 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 getParserForToken(@NonNull String token) {
+ return extractIssuer(token)
+ .flatMap(this::getParserForIssuer);
+ }
+
+ /**
+ * Builder for {@link MultiIssuerTokenParser}
+ */
+ public static class Builder {
+ private final Map 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);
+ }
+ }
+}
diff --git a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedAccessToken.java b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedAccessToken.java
index 2355f42c..ab84a07f 100644
--- a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedAccessToken.java
+++ b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedAccessToken.java
@@ -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 fromTokenString(String tokenString, @NonNull JWTParser tokenParser) {
return fromTokenString(tokenString, null, tokenParser);
}
@@ -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 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) {
diff --git a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedIdToken.java b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedIdToken.java
index 57652371..38e19e9a 100644
--- a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedIdToken.java
+++ b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedIdToken.java
@@ -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 fromTokenString(String tokenString, JWTParser tokenParser) {
+ Optional rawToken = jsonWebTokenFrom(tokenString, tokenParser, LOGGER);
+
+ return rawToken.map(ParsedIdToken::new);
}
/**
diff --git a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedToken.java b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedToken.java
index 3304c03e..371f4372 100644
--- a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedToken.java
+++ b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/ParsedToken.java
@@ -27,7 +27,7 @@
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
-import java.util.Set;
+import java.util.Optional;
import static de.cuioss.tools.string.MoreStrings.trimOrNull;
@@ -40,36 +40,6 @@
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public abstract class ParsedToken {
- protected static final String EMPTY_NAME = "EMPTY";
-
- /**
- * null
token.
- */
- public static final JsonWebToken EMPTY_WEB_TOKEN = new JsonWebToken() {
-
- @Override
- public String getName() {
- return EMPTY_NAME;
- }
-
- @Override
- public Set getClaimNames() {
- return Set.of();
- }
-
- @Override
- public T getClaim(String claimName) {
- return null;
- }
- };
-
- /**
- * @return true, if the token could not be parsed.
- */
- public boolean isEmpty() {
- return EMPTY_NAME.equals(jsonWebToken.getName());
- }
-
/**
* @return the token as encoded String.
*/
@@ -77,18 +47,18 @@ public String getTokenString() {
return jsonWebToken.getRawToken();
}
- protected static JsonWebToken jsonWebTokenFrom(String tokenString, JWTParser tokenParser, CuiLogger logger) {
+ protected static Optional jsonWebTokenFrom(String tokenString, JWTParser tokenParser, CuiLogger logger) {
logger.trace("Parsing token '%s'", tokenString);
if (MoreStrings.isEmpty(trimOrNull(tokenString))) {
logger.warn(LogMessages.TOKEN_IS_EMPTY.format());
- return EMPTY_WEB_TOKEN;
+ return Optional.empty();
}
try {
- return tokenParser.parse(tokenString);
+ return Optional.ofNullable(tokenParser.parse(tokenString));
} catch (ParseException e) {
logger.warn(e, LogMessages.COULD_NOT_PARSE_TOKEN.format());
logger.trace(() -> LogMessages.COULD_NOT_PARSE_TOKEN_TRACE.format(tokenString));
- return EMPTY_WEB_TOKEN;
+ return Optional.empty();
}
}
@@ -105,10 +75,6 @@ public boolean isExpired() {
return willExpireInSeconds(0);
}
- public boolean isValid() {
- return !(isEmpty() || isExpired());
- }
-
/**
* @param seconds maybe {@code 0}. Calling it with a negative number is not defined.
* @return boolean indicating whether the token will expired within the given number of seconds.
diff --git a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/TokenFactory.java b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/TokenFactory.java
index 910d52ad..fa48de9c 100644
--- a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/TokenFactory.java
+++ b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/TokenFactory.java
@@ -1,20 +1,20 @@
package de.cuioss.portal.authentication.token;
-import io.smallrye.jwt.auth.principal.JWTParser;
-import jakarta.enterprise.context.ApplicationScoped;
+import de.cuioss.tools.base.Preconditions;
import jakarta.inject.Inject;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
+import java.util.Optional;
+
/**
- * Factory for creating different types of tokens.
+ * Factory for creating different types of tokens with support for multiple issuers.
*/
-@ApplicationScoped
@RequiredArgsConstructor(access = AccessLevel.PRIVATE, onConstructor_ = @Inject)
public class TokenFactory {
- private final JWTParser tokenParser;
+ private final MultiIssuerTokenParser tokenParser;
/**
* Creates a new token factory using the given parser.
@@ -22,38 +22,53 @@ public class TokenFactory {
* @param tokenParser The parser to use for token validation, must not be null
* @return A new TokenFactory instance
*/
- public static TokenFactory of(@NonNull JWTParser tokenParser) {
- return new TokenFactory(tokenParser);
+ public static TokenFactory of(@NonNull JwksAwareTokenParser... tokenParser) {
+
+ Preconditions.checkArgument(tokenParser.length > 0, "tokenParser must be set");
+ var builder = MultiIssuerTokenParser.builder();
+ for (JwksAwareTokenParser jwksAwareTokenParser : tokenParser) {
+ builder.addParser(jwksAwareTokenParser);
+ }
+ return new TokenFactory(builder.build());
}
/**
* Creates an access token from the given token string.
*
* @param tokenString The token string to parse, must not be null
- * @return The parsed access token
+ * @return The parsed access token, which may be empty if the token is invalid or no parser is found
*/
- public ParsedAccessToken createAccessToken(@NonNull String tokenString) {
- return ParsedAccessToken.fromTokenString(tokenString, tokenParser);
+ public Optional createAccessToken(@NonNull String tokenString) {
+ var parser = tokenParser.getParserForToken(tokenString);
+ if (parser.isPresent()) {
+ return ParsedAccessToken.fromTokenString(tokenString, parser.get());
+ }
+ return Optional.empty();
}
/**
* Creates an ID token from the given token string.
*
* @param tokenString The token string to parse, must not be null
- * @return The parsed ID token
+ * @return The parsed ID token, which may be empty if the token is invalid or no parser is found
*/
- public ParsedIdToken createIdToken(@NonNull String tokenString) {
- return ParsedIdToken.fromTokenString(tokenString, tokenParser);
+ public Optional createIdToken(@NonNull String tokenString) {
+ var parser = tokenParser.getParserForToken(tokenString);
+ if (parser.isPresent()) {
+ return ParsedIdToken.fromTokenString(tokenString, parser.get());
+ }
+ return Optional.empty();
}
/**
* Creates a refresh token from the given token string.
*
* @param tokenString The token string to parse, must not be null
- * @return The parsed refresh token
+ * @return The parsed refresh token, which may be empty if the token is invalid or no parser is found
*/
- public ParsedRefreshToken createRefreshToken(@NonNull String tokenString) {
- return ParsedRefreshToken.fromTokenString(tokenString);
- }
+ public Optional createRefreshToken(@NonNull String tokenString) {
+ return tokenParser.getParserForToken(tokenString)
+ .map(parser -> ParsedRefreshToken.fromTokenString(tokenString));
+ }
}
diff --git a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/TokenType.java b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/TokenType.java
index 14867233..6811ec6f 100644
--- a/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/TokenType.java
+++ b/modules/authentication/portal-authentication-token/src/main/java/de/cuioss/portal/authentication/token/TokenType.java
@@ -32,6 +32,9 @@ public enum TokenType {
private final String typeClaimName;
public static TokenType fromTypClaim(String typeClaimName) {
+ if (typeClaimName == null) {
+ return UNKNOWN;
+ }
for (TokenType tokenType : TokenType.values()) {
if (tokenType.typeClaimName.equalsIgnoreCase(typeClaimName)) {
return tokenType;
diff --git a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/JwksAwareTokenParserTest.java b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/JwksAwareTokenParserTest.java
index c235e935..dedd65f2 100644
--- a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/JwksAwareTokenParserTest.java
+++ b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/JwksAwareTokenParserTest.java
@@ -23,7 +23,6 @@
import lombok.Getter;
import lombok.Setter;
import mockwebserver3.MockWebServer;
-import org.eclipse.microprofile.jwt.JsonWebToken;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -34,7 +33,7 @@
import static de.cuioss.portal.authentication.token.TestTokenProducer.validSignedJWTWithClaims;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@EnableMockWebServer
@@ -60,7 +59,7 @@ class JwksAwareTokenParserTest implements MockWebServerHolder {
void setupMockServer() {
mockserverPort = mockWebServer.getPort();
jwksEndpoint = "http://localhost:" + mockserverPort + jwksResolveDispatcher.getBaseUrl();
- tokenParser = JwksAwareTokenParser.builder().jwksEndpoint(jwksEndpoint).jwksRefreshIntervall(JWKS_REFRESH_INTERVALL).jwksIssuer(TestTokenProducer.ISSUER).build();
+ tokenParser = getValidJWKSParserWithRemoteJWKS();
jwksResolveDispatcher.setCallCounter(0);
}
@@ -69,25 +68,29 @@ void setupMockServer() {
void shouldResolveFromRemote() {
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
- JsonWebToken jsonWebToken = assertDoesNotThrow(() -> ParsedToken.jsonWebTokenFrom(initialToken, tokenParser, LOGGER));
+ var jsonWebToken = assertDoesNotThrow(() -> ParsedToken.jsonWebTokenFrom(initialToken, tokenParser, LOGGER));
- assertValidJsonWebToken(jsonWebToken, initialToken);
+ assertTrue(jsonWebToken.isPresent());
+ assertEquals(jsonWebToken.get().getRawToken(), initialToken);
}
@Test
void shouldFailFromRemoteWithInvalidIssuer() {
tokenParser = JwksAwareTokenParser.builder().jwksEndpoint(jwksEndpoint).jwksRefreshIntervall(JWKS_REFRESH_INTERVALL).jwksIssuer("Wrong Issuer").build();
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
- JsonWebToken jsonWebToken = assertDoesNotThrow(() -> ParsedToken.jsonWebTokenFrom(initialToken, tokenParser, LOGGER));
- assertEquals(ParsedToken.EMPTY_WEB_TOKEN, jsonWebToken);
+ var jsonWebToken = assertDoesNotThrow(() -> ParsedToken.jsonWebTokenFrom(initialToken, tokenParser, LOGGER));
+
+ assertFalse(jsonWebToken.isPresent());
+
}
@Test
void shouldFailFromRemoteWithInvalidJWKS() {
jwksResolveDispatcher.switchToOtherPublicKey();
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
- JsonWebToken jsonWebToken = assertDoesNotThrow(() -> ParsedToken.jsonWebTokenFrom(initialToken, tokenParser, LOGGER));
- assertEquals(ParsedToken.EMPTY_WEB_TOKEN, jsonWebToken);
+ var jsonWebToken = assertDoesNotThrow(() -> ParsedToken.jsonWebTokenFrom(initialToken, tokenParser, LOGGER));
+
+ assertFalse(jsonWebToken.isPresent());
}
@Test
@@ -95,16 +98,16 @@ void shouldCacheMultipleCalls() {
jwksResolveDispatcher.assertCallsAnswered(0);
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
for (int i = 0; i < 100; i++) {
- JsonWebToken jsonWebToken = ParsedToken.jsonWebTokenFrom(initialToken, tokenParser, LOGGER);
- assertValidJsonWebToken(jsonWebToken, initialToken);
+ var jsonWebToken = ParsedToken.jsonWebTokenFrom(initialToken, tokenParser, LOGGER);
+ assertTrue(jsonWebToken.isPresent());
}
// For some reason, there are always at least 2 calls, instead of expected one call. No
// problem because as shown within this test, the number stays at 2
assertTrue(jwksResolveDispatcher.getCallCounter() < 3);
for (int i = 0; i < 100; i++) {
- JsonWebToken jsonWebToken = assertDoesNotThrow(() -> ParsedToken.jsonWebTokenFrom(initialToken, tokenParser, LOGGER));
- assertValidJsonWebToken(jsonWebToken, initialToken);
+ var jsonWebToken = assertDoesNotThrow(() -> ParsedToken.jsonWebTokenFrom(initialToken, tokenParser, LOGGER));
+ assertTrue(jsonWebToken.isPresent());
}
assertTrue(jwksResolveDispatcher.getCallCounter() < 3);
}
@@ -112,15 +115,28 @@ void shouldCacheMultipleCalls() {
@Test
void shouldConsumeJWKSDirectly() throws IOException {
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
- tokenParser = JwksAwareTokenParser.builder().jwksKeyContent(IOStreams.toString(
+ var token = ParsedToken.jsonWebTokenFrom(initialToken, getValidJWKSParserWithLocalJWKS(), LOGGER);
+ assertTrue(token.isPresent());
+ assertEquals(token.get().getRawToken(), initialToken);
+ }
+
+ static JwksAwareTokenParser getValidJWKSParserWithLocalJWKS() throws IOException {
+ return JwksAwareTokenParser.builder().jwksKeyContent(IOStreams.toString(
new FileInputStream(JwksResolveDispatcher.PUBLIC_KEY_JWKS))).jwksIssuer(TestTokenProducer.ISSUER).build();
- var token = ParsedToken.jsonWebTokenFrom(initialToken, tokenParser, LOGGER);
- assertValidJsonWebToken(token, initialToken);
}
- private void assertValidJsonWebToken(JsonWebToken jsonWebToken, String initialTokenString) {
- assertNotEquals(ParsedToken.EMPTY_WEB_TOKEN, jsonWebToken);
- assertEquals(initialTokenString, jsonWebToken.getRawToken());
+ static JwksAwareTokenParser getInvalidJWKSParserWithWrongLocalJWKS() throws IOException {
+ return JwksAwareTokenParser.builder().jwksKeyContent(IOStreams.toString(
+ new FileInputStream(TestTokenProducer.PUBLIC_KEY_OTHER))).jwksIssuer(TestTokenProducer.ISSUER).build();
+ }
+
+ static JwksAwareTokenParser getInvalidValidJWKSParserWithLocalJWKSAndWrongIssuer() throws IOException {
+ return JwksAwareTokenParser.builder().jwksKeyContent(IOStreams.toString(
+ new FileInputStream(JwksResolveDispatcher.PUBLIC_KEY_JWKS))).jwksIssuer(TestTokenProducer.WRONG_ISSUER).build();
+ }
+
+ private JwksAwareTokenParser getValidJWKSParserWithRemoteJWKS() {
+ return JwksAwareTokenParser.builder().jwksEndpoint(jwksEndpoint).jwksRefreshIntervall(JWKS_REFRESH_INTERVALL).jwksIssuer(TestTokenProducer.ISSUER).build();
}
}
diff --git a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/JwtTokenParserTest.java b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/JwtTokenParserTest.java
new file mode 100644
index 00000000..8a793d7c
--- /dev/null
+++ b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/JwtTokenParserTest.java
@@ -0,0 +1,131 @@
+package de.cuioss.portal.authentication.token;
+
+import de.cuioss.test.juli.TestLogLevel;
+import de.cuioss.test.juli.junit5.EnableTestLogger;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static de.cuioss.portal.authentication.token.TestTokenProducer.ISSUER;
+import static de.cuioss.portal.authentication.token.TestTokenProducer.SOME_NAME;
+import static de.cuioss.portal.authentication.token.TestTokenProducer.SOME_SCOPES;
+import static de.cuioss.portal.authentication.token.TestTokenProducer.validSignedJWTWithClaims;
+import static de.cuioss.test.juli.LogAsserts.assertLogMessagePresentContaining;
+import static de.cuioss.test.juli.LogAsserts.assertNoLogMessagePresent;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@EnableTestLogger
+class JwtTokenParserTest {
+
+ private JwtTokenParser parser;
+
+ @BeforeEach
+ void setUp() {
+ parser = new JwtTokenParser();
+ }
+
+ @Test
+ void shouldParseValidToken() {
+ var token = validSignedJWTWithClaims(SOME_SCOPES);
+ var result = parser.unsecured(token);
+
+ assertTrue(result.isPresent());
+ var jwt = result.get();
+ assertEquals(ISSUER, jwt.getIssuer());
+ assertNotNull(jwt.getSubject());
+ assertTrue(jwt.getExpirationTime() > 0);
+ assertTrue(jwt.getIssuedAtTime() > 0);
+ assertTrue(jwt.getGroups().isEmpty());
+ assertTrue(jwt.getAudience().isEmpty());
+ assertNoLogMessagePresent(TestLogLevel.WARN, JwtTokenParser.class);
+ assertNoLogMessagePresent(TestLogLevel.ERROR, JwtTokenParser.class);
+ }
+
+
+ @Test
+ void shouldParseTokenWithName() {
+ var token = validSignedJWTWithClaims(SOME_NAME);
+ var result = parser.unsecured(token);
+
+ assertTrue(result.isPresent());
+ var jwt = result.get();
+ assertNotNull(jwt.getName());
+ assertNoLogMessagePresent(TestLogLevel.WARN, JwtTokenParser.class);
+ assertNoLogMessagePresent(TestLogLevel.ERROR, JwtTokenParser.class);
+ }
+
+ @ParameterizedTest(name = "Should handle invalid token format: {0}")
+ @CsvSource({
+ "not.a.jwt, Failed to parse token",
+ "'', Token is empty or null",
+ "before.after, Invalid JWT token format: expected 3 parts but got 2",
+ "before.after.that.else, Invalid JWT token format: expected 3 parts but got 4",
+ "invalid, Invalid JWT token format: expected 3 parts but got 1"
+ })
+ void shouldHandleInvalidTokenFormat(String invalidToken, String expectedMessage) {
+ var result = parser.unsecured(invalidToken);
+ assertTrue(result.isEmpty());
+ assertLogMessagePresentContaining(TestLogLevel.INFO, expectedMessage);
+ }
+
+ @Test
+ void shouldHandleNullToken() {
+ var result = parser.unsecured(null);
+ assertTrue(result.isEmpty());
+ assertLogMessagePresentContaining(TestLogLevel.INFO, "Token is empty or null");
+ }
+
+ @ParameterizedTest(name = "Should handle oversized token of size {0}KB")
+ @ValueSource(ints = {9, 10, 12, 16})
+ void shouldRejectOversizedToken(int sizeInKb) {
+ String largeToken = createLargeToken(sizeInKb);
+ var result = parser.unsecured(largeToken);
+ assertTrue(result.isEmpty());
+ assertLogMessagePresentContaining(TestLogLevel.WARN, "Token exceeds maximum size limit");
+ }
+
+ @ParameterizedTest(name = "Should handle oversized payload of size {0}KB")
+ @ValueSource(ints = {17, 20, 24, 32})
+ void shouldRejectOversizedPayload(int sizeInKb) {
+ String tokenWithLargePayload = createTokenWithLargePayload(sizeInKb);
+ var result = parser.unsecured(tokenWithLargePayload);
+ assertTrue(result.isEmpty());
+ assertLogMessagePresentContaining(TestLogLevel.WARN, "Token exceeds maximum size limit");
+ }
+
+ private String createLargeToken(int sizeInKb) {
+ String repeatedChar = IntStream.range(0, sizeInKb * 1024)
+ .mapToObj(i -> "a")
+ .collect(Collectors.joining());
+ return repeatedChar + "." + repeatedChar + "." + repeatedChar;
+ }
+
+ private String createTokenWithLargePayload(int sizeInKb) {
+ // Create a valid header
+ String header = Base64.getUrlEncoder().encodeToString(
+ "{\"alg\":\"none\"}".getBytes(StandardCharsets.UTF_8));
+
+ // Create a large payload
+ String largeJson = "{\"data\":\"" +
+ IntStream.range(0, sizeInKb * 1024)
+ .mapToObj(i -> "a")
+ .collect(Collectors.joining()) +
+ "\"}";
+ String payload = Base64.getUrlEncoder().encodeToString(
+ largeJson.getBytes(StandardCharsets.UTF_8));
+
+ // Add a dummy signature
+ String signature = Base64.getUrlEncoder().encodeToString("signature".getBytes(StandardCharsets.UTF_8));
+
+ return String.join(".", header, payload, signature);
+ }
+}
diff --git a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/MultiIssuerTokenParserTest.java b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/MultiIssuerTokenParserTest.java
new file mode 100644
index 00000000..af0fe3b8
--- /dev/null
+++ b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/MultiIssuerTokenParserTest.java
@@ -0,0 +1,82 @@
+package de.cuioss.portal.authentication.token;
+
+import de.cuioss.test.juli.junit5.EnableTestLogger;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static de.cuioss.portal.authentication.token.TestTokenProducer.ISSUER;
+import static de.cuioss.portal.authentication.token.TestTokenProducer.SOME_SCOPES;
+import static de.cuioss.portal.authentication.token.TestTokenProducer.validSignedJWTWithClaims;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@EnableTestLogger
+class MultiIssuerTokenParserTest {
+
+ private MultiIssuerTokenParser multiIssuerParser;
+ private JwksAwareTokenParser defaultParser;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ defaultParser = JwksAwareTokenParserTest.getValidJWKSParserWithLocalJWKS();
+ JwksAwareTokenParser otherParser = JwksAwareTokenParserTest.getInvalidValidJWKSParserWithLocalJWKSAndWrongIssuer();
+
+ multiIssuerParser = MultiIssuerTokenParser.builder()
+ .addParser(defaultParser)
+ .addParser(otherParser)
+ .build();
+ }
+
+ @Test
+ void shouldExtractIssuerFromValidToken() {
+ var token = validSignedJWTWithClaims(SOME_SCOPES);
+ var extractedIssuer = multiIssuerParser.extractIssuer(token);
+
+ assertTrue(extractedIssuer.isPresent());
+ assertEquals(ISSUER, extractedIssuer.get());
+ }
+
+ @Test
+ void shouldHandleInvalidTokenForIssuerExtraction() {
+ var extractedIssuer = multiIssuerParser.extractIssuer("invalid-token");
+ assertFalse(extractedIssuer.isPresent());
+ }
+
+ @Test
+ void shouldGetParserForKnownIssuer() {
+ var parser = multiIssuerParser.getParserForIssuer(ISSUER);
+
+ assertTrue(parser.isPresent());
+ assertEquals(defaultParser, parser.get());
+ }
+
+ @Test
+ void shouldReturnEmptyForUnknownIssuer() {
+ var parser = multiIssuerParser.getParserForIssuer("unknown-issuer");
+ assertFalse(parser.isPresent());
+ }
+
+ @Test
+ void shouldGetParserForValidToken() {
+ var token = validSignedJWTWithClaims(SOME_SCOPES);
+ var parser = multiIssuerParser.getParserForToken(token);
+
+ assertTrue(parser.isPresent());
+ assertEquals(defaultParser, parser.get());
+ }
+
+ @Test
+ void shouldHandleInvalidTokenForParserRetrieval() {
+ var parser = multiIssuerParser.getParserForToken("invalid-token");
+ assertFalse(parser.isPresent());
+ }
+
+ @Test
+ void shouldHandleUnknownIssuerInToken() {
+ var parser = multiIssuerParser.getParserForIssuer("unknown-issuer");
+ assertFalse(parser.isPresent());
+ }
+}
diff --git a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedAccessTokenTest.java b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedAccessTokenTest.java
index 3f5f6208..c4160c7a 100644
--- a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedAccessTokenTest.java
+++ b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedAccessTokenTest.java
@@ -23,8 +23,15 @@
import java.util.Set;
-import static de.cuioss.portal.authentication.token.TestTokenProducer.*;
-import static org.junit.jupiter.api.Assertions.*;
+import static de.cuioss.portal.authentication.token.TestTokenProducer.DEFAULT_TOKEN_PARSER;
+import static de.cuioss.portal.authentication.token.TestTokenProducer.SOME_NAME;
+import static de.cuioss.portal.authentication.token.TestTokenProducer.SOME_ROLES;
+import static de.cuioss.portal.authentication.token.TestTokenProducer.SOME_SCOPES;
+import static de.cuioss.portal.authentication.token.TestTokenProducer.validSignedEmptyJWT;
+import static de.cuioss.portal.authentication.token.TestTokenProducer.validSignedJWTWithClaims;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
@EnableTestLogger
@@ -39,9 +46,11 @@ class ParsedAccessTokenTest {
@Test
void shouldParseValidToken() {
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+ var retrievedToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+
+ assertTrue(retrievedToken.isPresent());
+ var parsedAccessToken = retrievedToken.get();
- assertTrue(parsedAccessToken.isValid());
assertEquals(initialToken, parsedAccessToken.getTokenString());
assertEquals(3, parsedAccessToken.getScopes().size());
assertTrue(parsedAccessToken.getScopes().contains(EXISTING_SCOPE));
@@ -69,31 +78,34 @@ void shouldParseValidToken() {
@Test
void shouldHandleMissingScopes() {
String initialToken = validSignedEmptyJWT();
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
- assertEquals(0, parsedAccessToken.getScopes().size());
+ var parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedAccessToken.isPresent());
+ assertTrue(parsedAccessToken.get().getScopes().isEmpty());
}
-
@Test
void shouldHandleGivenRoles() {
String initialToken = validSignedJWTWithClaims(SOME_ROLES);
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
- assertTrue(parsedAccessToken.hasRole("reader"));
+ var parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedAccessToken.isPresent());
+ assertTrue(parsedAccessToken.get().hasRole("reader"));
}
@Test
void shouldHandleMissingRoles() {
String initialToken = validSignedJWTWithClaims(SOME_ROLES);
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
- assertFalse(parsedAccessToken.hasRole(DEFINITELY_NO_SCOPE));
+ var parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedAccessToken.isPresent());
+ assertFalse(parsedAccessToken.get().hasRole(DEFINITELY_NO_SCOPE));
}
@Test
void shouldHandleNoRoles() {
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
- assertTrue(parsedAccessToken.getRoles().isEmpty());
+ var parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedAccessToken.isPresent());
+ assertTrue(parsedAccessToken.get().getRoles().isEmpty());
}
@Test
@@ -101,8 +113,9 @@ void shouldHandleSubjectId() {
String expectedSubjectId = Generators.letterStrings(4, 9).next();
String initialToken = validSignedJWTWithClaims(SOME_SCOPES, expectedSubjectId);
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
- assertEquals(expectedSubjectId, parsedAccessToken.getSubjectId());
+ var parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedAccessToken.isPresent());
+ assertEquals(expectedSubjectId, parsedAccessToken.get().getSubjectId());
}
@Test
@@ -110,33 +123,38 @@ void shouldHandleGivenEmail() {
String expectedEmail = new EmailGenerator().next();
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, expectedEmail, DEFAULT_TOKEN_PARSER);
-
- assertEquals(expectedEmail, parsedAccessToken.getEmail().get());
+ var parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, expectedEmail, DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedAccessToken.isPresent());
+ assertEquals(expectedEmail, parsedAccessToken.get().getEmail().get());
}
@Test
void shouldHandleMissingEmail() {
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
- assertFalse(parsedAccessToken.getEmail().isPresent());
+ var parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedAccessToken.isPresent());
+
+ assertFalse(parsedAccessToken.get().getEmail().isPresent());
}
@Test
void shouldHandleGivenName() {
String initialToken = validSignedJWTWithClaims(SOME_NAME);
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
- assertEquals("hello", parsedAccessToken.getName().get());
+ var parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedAccessToken.isPresent());
+
+ assertEquals("hello", parsedAccessToken.get().getName().get());
}
@Test
void shouldHandleMissingName() {
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
- assertFalse(parsedAccessToken.getName().isPresent());
+ var parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedAccessToken.isPresent());
+ assertFalse(parsedAccessToken.get().getName().isPresent());
}
@@ -144,16 +162,19 @@ void shouldHandleMissingName() {
void shouldHandlePreferredName() {
String initialToken = validSignedJWTWithClaims(SOME_NAME);
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
- assertEquals("world", parsedAccessToken.getPreferredUsername().get());
+ var parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedAccessToken.isPresent());
+ assertEquals("world", parsedAccessToken.get().getPreferredUsername().get());
}
@Test
void shouldHandleMissingPreferredName() {
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
- ParsedAccessToken parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
- assertFalse(parsedAccessToken.getPreferredUsername().isPresent());
+ var parsedAccessToken = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedAccessToken.isPresent());
+
+ assertFalse(parsedAccessToken.get().getPreferredUsername().isPresent());
}
}
diff --git a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedIdTokenTest.java b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedIdTokenTest.java
index 314b9a52..31991083 100644
--- a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedIdTokenTest.java
+++ b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedIdTokenTest.java
@@ -18,6 +18,7 @@
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
class ParsedIdTokenTest {
@@ -25,16 +26,19 @@ class ParsedIdTokenTest {
void shouldHandleValidToken() {
String initialTokenString = TestTokenProducer.validSignedJWTWithClaims(TestTokenProducer.SOME_ID_TOKEN);
- ParsedIdToken parsedIdToken = ParsedIdToken.fromTokenString(initialTokenString, TestTokenProducer.DEFAULT_TOKEN_PARSER);
- assertEquals(parsedIdToken.getTokenString(), initialTokenString);
+ var parsedIdToken = ParsedIdToken.fromTokenString(initialTokenString, TestTokenProducer.DEFAULT_TOKEN_PARSER);
+
+ assertTrue(parsedIdToken.isPresent());
+ assertEquals(parsedIdToken.get().getTokenString(), initialTokenString);
}
@Test
void shouldHandleEmail() {
String initialTokenString = TestTokenProducer.validSignedJWTWithClaims(TestTokenProducer.SOME_ID_TOKEN);
- ParsedIdToken parsedIdToken = ParsedIdToken.fromTokenString(initialTokenString, TestTokenProducer.DEFAULT_TOKEN_PARSER);
- assertEquals("hello@world.com", parsedIdToken.getEmail().get());
+ var parsedIdToken = ParsedIdToken.fromTokenString(initialTokenString, TestTokenProducer.DEFAULT_TOKEN_PARSER);
+ assertTrue(parsedIdToken.isPresent());
+ assertEquals("hello@world.com", parsedIdToken.get().getEmail().get());
}
}
diff --git a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedTokenTest.java b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedTokenTest.java
index faebc4db..ae783c1b 100644
--- a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedTokenTest.java
+++ b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/ParsedTokenTest.java
@@ -20,8 +20,6 @@
import de.cuioss.test.juli.TestLogLevel;
import de.cuioss.test.juli.junit5.EnableTestLogger;
import de.cuioss.tools.logging.CuiLogger;
-import org.eclipse.microprofile.jwt.JsonWebToken;
-import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
@@ -42,10 +40,9 @@ class ParsedTokenTest {
@NullAndEmptySource
@ValueSource(strings = " ")
void shouldProvideEmptyFallbackOnEmptyInput(String initialTokenString) {
- JsonWebToken jsonWebToken = ParsedToken.jsonWebTokenFrom(initialTokenString,
+ var jsonWebToken = ParsedToken.jsonWebTokenFrom(initialTokenString,
TestTokenProducer.DEFAULT_TOKEN_PARSER, LOGGER);
-
- Assertions.assertEquals(ParsedToken.EMPTY_WEB_TOKEN, jsonWebToken);
+ assertFalse(jsonWebToken.isPresent());
LogAsserts.assertSingleLogMessagePresentContaining(TestLogLevel.WARN,
LogMessages.TOKEN_IS_EMPTY.resolveIdentifierString());
}
@@ -54,10 +51,10 @@ void shouldProvideEmptyFallbackOnEmptyInput(String initialTokenString) {
void shouldProvideEmptyFallbackOnParseError() {
String initialTokenString = Generators.letterStrings(10, 20).next();
- JsonWebToken jsonWebToken = ParsedToken.jsonWebTokenFrom(initialTokenString,
+ var jsonWebToken = ParsedToken.jsonWebTokenFrom(initialTokenString,
TestTokenProducer.DEFAULT_TOKEN_PARSER, LOGGER);
- Assertions.assertEquals(ParsedToken.EMPTY_WEB_TOKEN, jsonWebToken);
+ assertFalse(jsonWebToken.isPresent());
LogAsserts.assertSingleLogMessagePresentContaining(TestLogLevel.WARN,
LogMessages.COULD_NOT_PARSE_TOKEN.resolveIdentifierString());
}
@@ -66,10 +63,11 @@ void shouldProvideEmptyFallbackOnParseError() {
void shouldProvideEmptyFallbackOnInvalidIssuer() {
String initialTokenString = TestTokenProducer.validSignedJWTWithClaims(TestTokenProducer.SOME_SCOPES);
- JsonWebToken jsonWebToken = ParsedToken
+ var jsonWebToken = ParsedToken
.jsonWebTokenFrom(initialTokenString, TestTokenProducer.WRONG_ISSUER_TOKEN_PARSER, LOGGER);
- Assertions.assertEquals(ParsedToken.EMPTY_WEB_TOKEN, jsonWebToken);
+ assertFalse(jsonWebToken.isPresent());
+
LogAsserts.assertSingleLogMessagePresentContaining(TestLogLevel.WARN,
LogMessages.COULD_NOT_PARSE_TOKEN.resolveIdentifierString());
}
@@ -78,10 +76,10 @@ void shouldProvideEmptyFallbackOnInvalidIssuer() {
void shouldProvideEmptyFallbackOnWrongPublicKey() {
String initialTokenString = TestTokenProducer.validSignedJWTWithClaims(TestTokenProducer.SOME_SCOPES);
- JsonWebToken jsonWebToken = ParsedToken
+ var jsonWebToken = ParsedToken
.jsonWebTokenFrom(initialTokenString,
TestTokenProducer.WRONG_SIGNATURE_TOKEN_PARSER, LOGGER);
- Assertions.assertEquals(ParsedToken.EMPTY_WEB_TOKEN, jsonWebToken);
+ assertFalse(jsonWebToken.isPresent());
LogAsserts.assertSingleLogMessagePresentContaining(TestLogLevel.WARN,
LogMessages.COULD_NOT_PARSE_TOKEN.resolveIdentifierString());
}
@@ -91,9 +89,9 @@ void shouldHandleNotExpiredToken() {
String initialToken = validSignedJWTWithClaims(SOME_SCOPES);
var token = ParsedAccessToken.fromTokenString(initialToken, DEFAULT_TOKEN_PARSER);
-
- assertFalse(token.isExpired());
- assertFalse(token.willExpireInSeconds(5));
- assertTrue(token.willExpireInSeconds(500));
+ assertTrue(token.isPresent());
+ assertFalse(token.get().isExpired());
+ assertFalse(token.get().willExpireInSeconds(5));
+ assertTrue(token.get().willExpireInSeconds(500));
}
}
diff --git a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TestTokenProducer.java b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TestTokenProducer.java
index 13f29648..04757483 100644
--- a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TestTokenProducer.java
+++ b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TestTokenProducer.java
@@ -52,18 +52,21 @@ public class TestTokenProducer {
public static final String SOME_ID_TOKEN = BASE_PATH + "some-id-token.json";
+ public static final String WRONG_ISSUER = Generators.nonBlankStrings().next();
+
public static final JWTAuthContextInfo TEST_AUTH_CONTEXT_INFO = new JWTAuthContextInfo(PUBLIC_KEY, ISSUER);
public static final JWTAuthContextInfo TEST_AUTH_CONTEXT_INFO_WRONG_PUBLIC_KEY = new JWTAuthContextInfo(
PUBLIC_KEY_OTHER, ISSUER);
public static final JWTAuthContextInfo TEST_AUTH_CONTEXT_INFO_WRONG_ISSUER = new JWTAuthContextInfo(PUBLIC_KEY,
- new StringBuilder(ISSUER).reverse().toString());
+ WRONG_ISSUER);
public static final JWTParser DEFAULT_TOKEN_PARSER = new DefaultJWTParser(TEST_AUTH_CONTEXT_INFO);
public static final JWTParser WRONG_ISSUER_TOKEN_PARSER = new DefaultJWTParser(TEST_AUTH_CONTEXT_INFO_WRONG_ISSUER);
+
public static final JWTParser WRONG_SIGNATURE_TOKEN_PARSER = new DefaultJWTParser(
TEST_AUTH_CONTEXT_INFO_WRONG_PUBLIC_KEY);
diff --git a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenFactoryTest.java b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenFactoryTest.java
index a5a2f8f8..88a1784d 100644
--- a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenFactoryTest.java
+++ b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenFactoryTest.java
@@ -3,9 +3,11 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -14,9 +16,10 @@ class TokenFactoryTest {
private TokenFactory tokenFactory;
+
@BeforeEach
- void setUp() {
- tokenFactory = TokenFactory.of(TestTokenProducer.DEFAULT_TOKEN_PARSER);
+ void setUp() throws IOException {
+ tokenFactory = TokenFactory.of(JwksAwareTokenParserTest.getValidJWKSParserWithLocalJWKS());
}
@Test
@@ -24,10 +27,10 @@ void shouldCreateAccessToken() {
var token = TestTokenProducer.validSignedJWTWithClaims(TestTokenProducer.SOME_SCOPES);
var parsedToken = tokenFactory.createAccessToken(token);
- assertNotNull(parsedToken);
- assertFalse(parsedToken.getScopes().isEmpty());
- assertNotNull(parsedToken.getSubject());
- assertNotNull(parsedToken.getIssuer());
+ assertTrue(parsedToken.isPresent());
+ assertFalse(parsedToken.get().getScopes().isEmpty());
+ assertNotNull(parsedToken.get().getSubject());
+ assertEquals(TestTokenProducer.ISSUER, parsedToken.get().getIssuer());
}
@Test
@@ -35,9 +38,9 @@ void shouldCreateIdToken() {
var token = TestTokenProducer.validSignedJWTWithClaims(TestTokenProducer.SOME_ID_TOKEN);
var parsedToken = tokenFactory.createIdToken(token);
- assertNotNull(parsedToken);
- assertNotNull(parsedToken.getSubject());
- assertNotNull(parsedToken.getIssuer());
+ assertTrue(parsedToken.isPresent());
+ assertNotNull(parsedToken.get().getSubject());
+ assertEquals(TestTokenProducer.ISSUER, parsedToken.get().getIssuer());
}
@Test
@@ -45,34 +48,33 @@ void shouldCreateRefreshToken() {
var token = TestTokenProducer.validSignedJWTWithClaims(TestTokenProducer.REFRESH_TOKEN);
var parsedToken = tokenFactory.createRefreshToken(token);
- assertNotNull(parsedToken);
+ assertTrue(parsedToken.isPresent());
+ assertNotNull(parsedToken.get().getTokenString());
}
@Test
void shouldHandleExpiredToken() {
var expiredToken = TestTokenProducer.validSignedJWTExpireAt(
Instant.now().minus(1, ChronoUnit.HOURS));
+
var token = tokenFactory.createAccessToken(expiredToken);
- assertNotNull(token);
- assertTrue(token.isEmpty());
+ assertFalse(token.isPresent());
}
@Test
- void shouldHandleInvalidIssuer() {
- var wrongIssuerTokenFactory = TokenFactory.of(TestTokenProducer.WRONG_ISSUER_TOKEN_PARSER);
+ void shouldHandleInvalidIssuer() throws IOException {
+ var wrongIssuerTokenFactory = TokenFactory.of(JwksAwareTokenParserTest.getInvalidValidJWKSParserWithLocalJWKSAndWrongIssuer());
var token = TestTokenProducer.validSignedJWTWithClaims(TestTokenProducer.SOME_SCOPES);
var parsedToken = wrongIssuerTokenFactory.createAccessToken(token);
- assertNotNull(parsedToken);
- assertTrue(parsedToken.isEmpty());
+ assertFalse(parsedToken.isPresent());
}
@Test
- void shouldHandleInvalidSignature() {
- var wrongSignatureTokenFactory = TokenFactory.of(TestTokenProducer.WRONG_SIGNATURE_TOKEN_PARSER);
+ void shouldHandleInvalidSignature() throws IOException {
+ var wrongSignatureTokenFactory = TokenFactory.of(JwksAwareTokenParserTest.getInvalidJWKSParserWithWrongLocalJWKS());
var token = TestTokenProducer.validSignedJWTWithClaims(TestTokenProducer.SOME_SCOPES);
var parsedToken = wrongSignatureTokenFactory.createAccessToken(token);
- assertNotNull(parsedToken);
- assertTrue(parsedToken.isEmpty());
+ assertFalse(parsedToken.isPresent());
}
}
diff --git a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenKeycloakIT.java b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenKeycloakIT.java
index 5b012c69..f4652b1c 100644
--- a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenKeycloakIT.java
+++ b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenKeycloakIT.java
@@ -51,19 +51,24 @@ void shouldHandleValidKeycloakTokens() {
var tokenString = requestToken(parameterForScopedToken(SCOPES), TokenTypes.ACCESS);
var parser = JwksAwareTokenParser.builder().jwksEndpoint(getJWKSUrl()).jwksRefreshIntervall(100).jwksIssuer(getIssuer()).tTlsCertificatePath(TestRealm.providedKeyStore.PUBLIC_CERT).build();
- var accessToken = ParsedAccessToken.fromTokenString(tokenString, parser);
- assertFalse(accessToken.isEmpty());
- assertTrue(accessToken.isValid());
+ var retrievedAccessToken = ParsedAccessToken.fromTokenString(tokenString, parser);
+ assertTrue(retrievedAccessToken.isPresent());
+
+ var accessToken = retrievedAccessToken.get();
+ assertFalse(accessToken.isExpired());
assertTrue(accessToken.providesScopes(SCOPES_AS_LIST));
assertEquals(TestRealm.testUser.EMAIL.toLowerCase(), accessToken.getEmail().get());
assertEquals(TokenType.ACCESS_TOKEN, accessToken.getType());
tokenString = requestToken(parameterForScopedToken(SCOPES), TokenTypes.ID_TOKEN);
+
var idToken = ParsedIdToken.fromTokenString(tokenString, parser);
assertFalse(idToken.isEmpty());
- assertTrue(idToken.isValid());
+
+ assertFalse(idToken.get().isExpired());
assertEquals(TestRealm.testUser.EMAIL.toLowerCase(), accessToken.getEmail().get());
- assertEquals(TokenType.ID_TOKEN, idToken.getType());
+
+ assertEquals(TokenType.ID_TOKEN, idToken.get().getType());
tokenString = requestToken(parameterForScopedToken(SCOPES), TokenTypes.REFRESH);
var refreshToken = ParsedRefreshToken.fromTokenString(tokenString);
diff --git a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenTypeTest.java b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenTypeTest.java
index b985117e..6bd96dc5 100644
--- a/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenTypeTest.java
+++ b/modules/authentication/portal-authentication-token/src/test/java/de/cuioss/portal/authentication/token/TokenTypeTest.java
@@ -15,8 +15,10 @@
*/
package de.cuioss.portal.authentication.token;
-import de.cuioss.test.generator.Generators;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -29,10 +31,10 @@ void shouldHandleHappyCase() {
}
}
- @Test
- void shouldDefaultToUnknown() {
- assertEquals(TokenType.UNKNOWN, TokenType.fromTypClaim(""));
- assertEquals(TokenType.UNKNOWN, TokenType.fromTypClaim(null));
- assertEquals(TokenType.UNKNOWN, TokenType.fromTypClaim(Generators.letterStrings().next()));
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = {"invalid", "unknown", "not_a_token_type"})
+ void shouldDefaultToUnknown(String invalidType) {
+ assertEquals(TokenType.UNKNOWN, TokenType.fromTypClaim(invalidType));
}
}
\ No newline at end of file
diff --git a/modules/authentication/portal-authentication-token/src/test/resources/token/some-groups.json b/modules/authentication/portal-authentication-token/src/test/resources/token/some-groups.json
new file mode 100644
index 00000000..28256d69
--- /dev/null
+++ b/modules/authentication/portal-authentication-token/src/test/resources/token/some-groups.json
@@ -0,0 +1,7 @@
+{
+ "groups": [
+ "reader",
+ "writer",
+ "gambler"
+ ]
+}