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" + ] +}