diff --git a/data-node/src/main/java/org/graylog/datanode/initializers/JwtTokenValidator.java b/data-node/src/main/java/org/graylog/datanode/initializers/JwtTokenValidator.java index c5c55a3319c5..2d2e0b2cf676 100644 --- a/data-node/src/main/java/org/graylog/datanode/initializers/JwtTokenValidator.java +++ b/data-node/src/main/java/org/graylog/datanode/initializers/JwtTokenValidator.java @@ -16,17 +16,15 @@ */ package org.graylog.datanode.initializers; -import io.jsonwebtoken.Jwt; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; import jakarta.inject.Named; import jakarta.inject.Singleton; -import javax.crypto.spec.SecretKeySpec; +import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; -import java.security.Key; -import java.util.Optional; @Singleton public class JwtTokenValidator implements AuthTokenValidator { @@ -41,30 +39,18 @@ public JwtTokenValidator(@Named("password_secret") final String signingKey) { @Override public void verifyToken(String token) throws TokenVerificationException { - SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; - Key signingKey = new SecretKeySpec(this.signingKey.getBytes(StandardCharsets.UTF_8), signatureAlgorithm.getJcaName()); - final JwtParser parser = Jwts.parserBuilder() - .setSigningKey(signingKey) + final SecretKey key = Keys.hmacShaKeyFor(this.signingKey.getBytes(StandardCharsets.UTF_8)); + final JwtParser parser = Jwts.parser() + .verifyWith(key) .requireSubject(REQUIRED_SUBJECT) .requireIssuer(REQUIRED_ISSUER) .build(); try { - final Jwt parsed = parser.parse(token); - verifySignature(parsed, signatureAlgorithm); - } catch (Exception e) { + parser.parse(token); + } catch (UnsupportedJwtException e) { + throw new TokenVerificationException("Token format/configuration is not supported", e); + } catch (Throwable e) { throw new TokenVerificationException(e); } } - - private void verifySignature(Jwt token, SignatureAlgorithm expectedAlgorithm) { - final SignatureAlgorithm usedAlgorithm = Optional.of(token.getHeader()) - .map(h -> h.get("alg")) - .map(Object::toString) - .map(SignatureAlgorithm::forName) - .orElseThrow(() -> new IllegalArgumentException("Token doesn't provide valid signature algorithm")); - - if (expectedAlgorithm != usedAlgorithm) { - throw new IllegalArgumentException("Token is using unsupported signature algorithm :" + usedAlgorithm); - } - } } diff --git a/data-node/src/main/java/org/graylog/datanode/initializers/TokenVerificationException.java b/data-node/src/main/java/org/graylog/datanode/initializers/TokenVerificationException.java index c0c70bbca689..52c812dd0679 100644 --- a/data-node/src/main/java/org/graylog/datanode/initializers/TokenVerificationException.java +++ b/data-node/src/main/java/org/graylog/datanode/initializers/TokenVerificationException.java @@ -17,11 +17,15 @@ package org.graylog.datanode.initializers; public class TokenVerificationException extends Exception { - public TokenVerificationException(Exception cause) { + public TokenVerificationException(Throwable cause) { super(cause); } public TokenVerificationException(String message) { super(message); } + + public TokenVerificationException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/data-node/src/test/java/org/graylog/datanode/initializers/JwtTokenValidatorTest.java b/data-node/src/test/java/org/graylog/datanode/initializers/JwtTokenValidatorTest.java new file mode 100644 index 000000000000..b3656bbef1dd --- /dev/null +++ b/data-node/src/test/java/org/graylog/datanode/initializers/JwtTokenValidatorTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.datanode.initializers; + +import com.github.joschi.jadconfig.util.Duration; +import org.assertj.core.api.Assertions; +import org.graylog2.security.IndexerJwtAuthTokenProvider; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +class JwtTokenValidatorTest { + + @Test + void verifyValidToken() throws TokenVerificationException { + final String key = "gTVfiF6A0pB70A3UP1EahpoR6LId9DdNadIkYNygK5Z8lpeJIpw9vN0jZ6fdsfeuV9KIg9gVLkCHIPj6FHW5Q9AvpOoGZO3h"; + final JwtTokenValidator validator = new JwtTokenValidator(key); + validator.verifyToken(generateToken(key)); + } + + @Test + void verifyInvalidToken() { + final String generationKey = "gTVfiF6A0pB70A3UP1EahpoR6LId9DdNadIkYNygK5Z8lpeJIpw9vN0jZ6fdsfeuV9KIg9gVLkCHIPj6FHW5Q9AvpOoGZO3h"; + final String verificationKey = "n51wcO3jn8w3JNyGgKc7k1fTCr1FWvGg7ODfQOyBT2fizBrCVsRJg2GsbYGLNejfi3QsKaqJgo3zAWMuAZhJznuizHZpv92S"; + final JwtTokenValidator validator = new JwtTokenValidator(verificationKey); + Assertions.assertThatThrownBy(() -> validator.verifyToken(generateToken(generationKey))) + .isInstanceOf(TokenVerificationException.class) + .hasMessageContaining("JWT signature does not match locally computed signature"); + } + + @Test + void testNoneAlgorithm() { + final String key = "gTVfiF6A0pB70A3UP1EahpoR6LId9DdNadIkYNygK5Z8lpeJIpw9vN0jZ6fdsfeuV9KIg9gVLkCHIPj6FHW5Q9AvpOoGZO3h"; + final JwtTokenValidator validator = new JwtTokenValidator(key); + Assertions.assertThatThrownBy(() -> validator.verifyToken(removeSignature(generateToken(key)))) + .isInstanceOf(TokenVerificationException.class) + .hasMessageContaining("Token format/configuration is not supported"); + } + + private String removeSignature(String token) { + final String header = Base64.getEncoder() + .encodeToString("{\"alg\": \"none\"}" + .getBytes(StandardCharsets.UTF_8)); + + return header + token.substring(token.indexOf('.'), token.lastIndexOf('.') + 1); + } + + @NotNull + private static String generateToken(String signingKey) { + return IndexerJwtAuthTokenProvider.createToken(signingKey.getBytes(StandardCharsets.UTF_8), Duration.seconds(180)); + } +} diff --git a/data-node/src/test/java/org/graylog/datanode/initializers/JwtTokenVerifierTest.java b/data-node/src/test/java/org/graylog/datanode/initializers/JwtTokenVerifierTest.java deleted file mode 100644 index cc8c32b23a06..000000000000 --- a/data-node/src/test/java/org/graylog/datanode/initializers/JwtTokenVerifierTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2020 Graylog, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * . - */ -package org.graylog.datanode.initializers; - -import com.github.joschi.jadconfig.util.Duration; -import io.jsonwebtoken.Jwts; -import org.apache.commons.lang.RandomStringUtils; -import org.assertj.core.api.Assertions; -import org.graylog2.security.IndexerJwtAuthTokenProvider; -import org.junit.jupiter.api.Test; - -import java.util.Date; -import java.util.Map; - -class JwtTokenVerifierTest { - @Test - void testValidToken() { - final String signingKey = RandomStringUtils.random(64); - final AuthTokenValidator tokenVerifier = new JwtTokenValidator(signingKey); - final IndexerJwtAuthTokenProvider tokenProvider = new IndexerJwtAuthTokenProvider(signingKey, Duration.seconds(10), Duration.seconds(1)); - - try { - tokenVerifier.verifyToken(tokenProvider.get().replace("Bearer ", "")); - } catch (TokenVerificationException e) { - Assertions.fail("Verification of the token shouldn't fail"); - } - } - - @Test - void testNoneAlgorithmToken() { - final Date now = new Date(); - final String insecureToken = Jwts.builder() - .addClaims(Map.of("os_roles", "admin")) - .setIssuedAt(now) - .setSubject("admin") - .setIssuer("graylog") - .setNotBefore(now) - .setExpiration(new Date(now.getTime() + Duration.seconds(10).toMilliseconds())) - .compact(); - - final String signingKey = RandomStringUtils.random(64); - final AuthTokenValidator tokenVerifier = new JwtTokenValidator(signingKey); - - Assertions.assertThatThrownBy(() -> tokenVerifier.verifyToken(insecureToken)) - .isInstanceOf(TokenVerificationException.class) - .hasMessageContaining("Token is using unsupported signature algorithm :NONE"); - } -} diff --git a/graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthTokenProvider.java b/graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthTokenProvider.java index f6ac7753d9d3..ab2776eae09b 100644 --- a/graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthTokenProvider.java +++ b/graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthTokenProvider.java @@ -21,8 +21,10 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; +import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import io.jsonwebtoken.security.Keys; import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.inject.Provider; @@ -61,24 +63,21 @@ public IndexerJwtAuthTokenProvider(@Named("password_secret") String signingKey, } public static String createToken(final byte[] apiKeySecretBytes, final Duration tokenExpirationDuration) { - SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; - long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); + final SecretKey signingKey = Keys.hmacShaKeyFor(apiKeySecretBytes); - Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); - - JwtBuilder builder = Jwts.builder().setId("graylog datanode connect " + nowMillis) - .addClaims(Map.of("os_roles", "admin")) - .setIssuedAt(now) - .setSubject("admin") - .setIssuer("graylog") - .setNotBefore(now) - .setExpiration(new Date(nowMillis + tokenExpirationDuration.toMilliseconds())) - .signWith(signingKey, signatureAlgorithm); + JwtBuilder builder = Jwts.builder() + .id("graylog datanode connect " + nowMillis) + .claims(Map.of("os_roles", "admin")) + .issuedAt(now) + .subject("admin") + .issuer("graylog") + .notBefore(now) + .expiration(new Date(nowMillis + tokenExpirationDuration.toMilliseconds())) + .signWith(signingKey); - final var token = builder.compact(); - return token; + return builder.compact(); } @Override diff --git a/graylog2-server/src/test/java/org/graylog/testing/completebackend/ContainerizedGraylogBackend.java b/graylog2-server/src/test/java/org/graylog/testing/completebackend/ContainerizedGraylogBackend.java index dd1613d982d1..17098762f22f 100644 --- a/graylog2-server/src/test/java/org/graylog/testing/completebackend/ContainerizedGraylogBackend.java +++ b/graylog2-server/src/test/java/org/graylog/testing/completebackend/ContainerizedGraylogBackend.java @@ -108,7 +108,11 @@ private ContainerizedGraylogBackend create(Services services, } private void createLicenses(final MongoDBInstance mongoDBInstance, final String... licenseStrs) { - final List licenses = Arrays.stream(licenseStrs).map(System::getenv).filter(StringUtils::isNotBlank).collect(Collectors.toList()); + final List licenses = Arrays.stream(licenseStrs) + .map(System::getenv) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(Collectors.toList()); if (!licenses.isEmpty()) { ServiceLoader loader = ServiceLoader.load(TestLicenseImporter.class); loader.forEach(importer -> importer.importLicenses(mongoDBInstance, licenses)); diff --git a/pom.xml b/pom.xml index 26c97ef68d09..e98d998ef24d 100644 --- a/pom.xml +++ b/pom.xml @@ -180,7 +180,7 @@ 1.5.5-11 9.2.1 0.3.2 - 0.11.5 + 0.12.5 2.6.0