diff --git a/adapter-base/src/main/java/org/eclipse/hono/adapter/auth/device/jwt/DefaultJwsValidator.java b/adapter-base/src/main/java/org/eclipse/hono/adapter/auth/device/jwt/DefaultJwsValidator.java index f93cb21826..96643bd6cd 100644 --- a/adapter-base/src/main/java/org/eclipse/hono/adapter/auth/device/jwt/DefaultJwsValidator.java +++ b/adapter-base/src/main/java/org/eclipse/hono/adapter/auth/device/jwt/DefaultJwsValidator.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -14,7 +14,6 @@ package org.eclipse.hono.adapter.auth.device.jwt; import java.net.HttpURLConnection; -import java.security.Key; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; @@ -27,27 +26,21 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.stream.Stream; import org.eclipse.hono.client.ClientErrorException; -import org.eclipse.hono.util.CredentialsConstants; +import org.eclipse.hono.client.ServiceInvocationException; import org.eclipse.hono.util.RegistryManagementConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SigningKeyResolverAdapter; import io.jsonwebtoken.UnsupportedJwtException; -import io.jsonwebtoken.security.SignatureException; import io.vertx.core.Context; import io.vertx.core.Future; -import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonObject; @@ -107,85 +100,50 @@ public static JsonObject getJwtHeader(final String jws) { private PublicKey convertPublicKeyByteArrayToPublicKey( final JsonObject rawPublicKeySecret) throws InvalidKeySpecException, NoSuchAlgorithmException { - final byte[] encodedPublicKey = rawPublicKeySecret.getBinary(RegistryManagementConstants.FIELD_SECRETS_KEY); - final String alg = rawPublicKeySecret.getString(RegistryManagementConstants.FIELD_SECRETS_ALGORITHM); - final X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(encodedPublicKey); + final var encodedPublicKey = rawPublicKeySecret.getBinary(RegistryManagementConstants.FIELD_SECRETS_KEY); + final var alg = rawPublicKeySecret.getString(RegistryManagementConstants.FIELD_SECRETS_ALGORITHM); + final var keySpecX509 = new X509EncodedKeySpec(encodedPublicKey); return KeyFactory.getInstance(alg).generatePublic(keySpecX509); } - private void doExpand( - final String jws, - final List candidateKeys, - final Duration allowedClockSkew, - final Promise> resultHandler) { - - final SignatureAlgorithm signatureAlgorithmFromToken; - try { - final var header = DefaultJwsValidator.getJwtHeader(jws); - signatureAlgorithmFromToken = Optional.ofNullable(header.getString("alg")) - .map(SignatureAlgorithm::forName) - .orElseThrow(() -> new SignatureException("Missing signature algorithm header")); - } catch (final JwtException e) { - resultHandler.fail(new ClientErrorException(HttpURLConnection.HTTP_UNAUTHORIZED, e)); - return; - } - + private Jws doExpand( + final String jws, + final List candidateKeys, + final Duration allowedClockSkew) { final var claims = candidateKeys.stream() - .filter(spec -> Optional.ofNullable(spec.getString(CredentialsConstants.FIELD_SECRETS_ALGORITHM)) - .map(alg -> signatureAlgorithmFromToken.getFamilyName().startsWith(alg)) - .orElse(false)) - .flatMap(spec -> { - try { - return Stream.of(convertPublicKeyByteArrayToPublicKey(spec)); - } catch (final InvalidKeySpecException | NoSuchAlgorithmException e) { - return Stream.empty(); - } - }) - .flatMap(publicKey -> { + .>mapMulti((spec, consumer) -> { try { - final var parsedClaims = Jwts.parserBuilder() - .setAllowedClockSkewSeconds(allowedClockSkew.toSeconds()) - .setSigningKeyResolver(new SigningKeyResolverAdapter() { - - @SuppressWarnings("rawtypes") - @Override - public Key resolveSigningKey(final JwsHeader header, final Claims claims) { - final var tokenType = Optional.ofNullable(header.getType()) - .orElseThrow(() -> new MalformedJwtException("JWT must contain typ header")); - if (!tokenType.equalsIgnoreCase(EXPECTED_TOKEN_TYPE)) { - throw new MalformedJwtException( - "invalid typ header value [expected: %s, found: %s]" - .formatted(EXPECTED_TOKEN_TYPE, tokenType)); - } - final var signatureAlgorithm = Optional.ofNullable(header.getAlgorithm()) - .map(SignatureAlgorithm::forName) - .orElseThrow(() -> new MalformedJwtException("JWT must contain alg header")); - if (signatureAlgorithm.getFamilyName().startsWith(publicKey.getAlgorithm())) { - return publicKey; - } else { - throw new JwtException("key algorithm does not match JWT header value"); - } - } - }) + final var publicKey = convertPublicKeyByteArrayToPublicKey(spec); + final var claimsJws = Jwts.parser() + .clockSkewSeconds(allowedClockSkew.toSeconds()) + .verifyWith(publicKey) .build() - .parseClaimsJws(jws); - return Stream.of(parsedClaims); + .parseSignedClaims(jws); + if (Objects.equals(claimsJws.getHeader().getType(), EXPECTED_TOKEN_TYPE)) { + consumer.accept(claimsJws); + } else { + LOG.debug("JWT must contain header [name: type, value: {}", EXPECTED_TOKEN_TYPE); + } + } catch (final InvalidKeySpecException | NoSuchAlgorithmException e) { + LOG.debug("failed to create candidate public key [auth-id: {}]", + spec.getString(RegistryManagementConstants.FIELD_AUTH_ID), e); } catch (final JwtException e) { - LOG.debug("failed to validate token using key [{}]", publicKey, e); - return Stream.empty(); + LOG.debug("failed to validate token using candidate key [auth-id: {}]", + spec.getString(RegistryManagementConstants.FIELD_AUTH_ID), e); } }) .findFirst(); if (claims.isEmpty()) { - resultHandler.fail(new ClientErrorException(HttpURLConnection.HTTP_UNAUTHORIZED)); + throw new ClientErrorException(HttpURLConnection.HTTP_UNAUTHORIZED); } else { try { assertAdditionalClaimsPolicy(claims.get(), allowedClockSkew); - resultHandler.complete(claims.get()); + return claims.get(); } catch (final JwtException e) { - resultHandler.fail(new ClientErrorException(HttpURLConnection.HTTP_UNAUTHORIZED, e)); + LOG.debug("failed to validate JWT's claims", e); + throw new ClientErrorException(HttpURLConnection.HTTP_UNAUTHORIZED, e); } } } @@ -200,29 +158,29 @@ public Future> expand( Objects.requireNonNull(candidateKeys); Objects.requireNonNull(allowedClockSkew); - final Promise> result = Promise.promise(); final Context currentContext = Vertx.currentContext(); if (currentContext == null) { - doExpand(token, candidateKeys, allowedClockSkew, result); + try { + return Future.succeededFuture(doExpand(token, candidateKeys, allowedClockSkew)); + } catch (final ServiceInvocationException e) { + return Future.failedFuture(e); + } } else { - currentContext.executeBlocking(codeHandler -> doExpand( + return currentContext.executeBlocking(() -> doExpand( token, candidateKeys, - allowedClockSkew, - codeHandler), - true, result); + allowedClockSkew), true); } - return result.future(); } // TODO think about moving these additional checks to the JwtAuthProvider because // the parameters behind these checks might better be defined at the tenant level private void assertAdditionalClaimsPolicy(final Jws claims, final Duration allowedClockSkew) { - final var iat = Optional.ofNullable(claims.getBody().getIssuedAt()) + final var iat = Optional.ofNullable(claims.getPayload().getIssuedAt()) .map(Date::toInstant) .orElseThrow(() -> new UnsupportedJwtException("JWT must contain iat claim")); - final var exp = Optional.ofNullable(claims.getBody().getExpiration()) + final var exp = Optional.ofNullable(claims.getPayload().getExpiration()) .map(Date::toInstant) .orElseThrow(() -> new UnsupportedJwtException("JWT must contain exp claim")); diff --git a/adapter-base/src/test/java/org/eclipse/hono/adapter/auth/device/jwt/DefaultJwsValidatorTest.java b/adapter-base/src/test/java/org/eclipse/hono/adapter/auth/device/jwt/DefaultJwsValidatorTest.java index 911d436803..0788cfac60 100644 --- a/adapter-base/src/test/java/org/eclipse/hono/adapter/auth/device/jwt/DefaultJwsValidatorTest.java +++ b/adapter-base/src/test/java/org/eclipse/hono/adapter/auth/device/jwt/DefaultJwsValidatorTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import java.net.HttpURLConnection; +import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -25,11 +26,7 @@ import java.time.Duration; import java.time.Instant; import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; - -import javax.crypto.KeyGenerator; import org.eclipse.hono.client.ClientErrorException; import org.eclipse.hono.client.ServiceInvocationException; @@ -43,11 +40,10 @@ import org.junit.jupiter.params.provider.CsvSource; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.SignatureAlgorithm; import io.vertx.core.json.JsonObject; import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; @@ -63,52 +59,61 @@ class DefaultJwsValidatorTest { private final String deviceId = "device-id"; private final String authId = "auth-id"; private DefaultJwsValidator authTokenValidator; - private Map jwtHeader; private Instant instantNow; private Instant instantPlus24Hours; @BeforeEach void setUp() { authTokenValidator = new DefaultJwsValidator(); - jwtHeader = new HashMap<>(); - jwtHeader.put(JwsHeader.TYPE, "JWT"); instantNow = Instant.now(); instantPlus24Hours = instantNow.plus(Duration.ofHours(24)); } + JwtBuilder jwtBuilder() { + return Jwts.builder().header().type("JWT").and(); + } + /** * Verifies that expand returns JWS Claims, when valid JWTs matching public keys are provided. */ @ParameterizedTest @CsvSource(value = { - "RS256,2048", - "RS256,4096", - "RS384,2048", - "RS512,2048", - "PS256,2048", - "PS384,2048", - "PS512,2048", - "ES256,256", - "ES384,384" + "RS256,RSA,2048", + "RS256,RSA,4096", + "RS384,RSA,2048", + "RS512,RSA,2048", + "PS256,RSA,2048", + "PS384,RSA,2048", + "PS512,RSA,2048", + "ES256,EC,256", + "ES384,EC,384" }) - void testExpandValidJwtWithValidPublicKey(final String algorithm, final int keySize, final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.forName(algorithm); - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - final KeyPair keyPair = generateKeyPair(alg, keySize); - final byte[] publicKey = keyPair.getPublic().getEncoded(); - final var creds = CredentialsObject.fromRawPublicKey(deviceId, authId, alg.getFamilyName(), publicKey, - instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); - - final String jwt = generateJws(jwtHeader, - generateJwtClaims(null, null, instantNow, instantPlus24Hours), alg, keyPair.getPrivate()); - authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) - .onComplete(ctx.succeeding(jws -> { - ctx.verify(() -> { - assertThat(jws.getHeader().getAlgorithm()).isEqualTo(alg.getValue()); - }); - ctx.completeNow(); - })); + void testExpandValidJwtWithValidPublicKey( + final String algorithm, + final String algType, + final int keySize, + final VertxTestContext ctx) throws GeneralSecurityException { + final var alg = Jwts.SIG.get().forKey(algorithm); + if (alg instanceof SignatureAlgorithm sigAlg) { + final var keyPair = generateKeyPair(algType, keySize); + final byte[] publicKey = keyPair.getPublic().getEncoded(); + final var creds = CredentialsObject.fromRawPublicKey(deviceId, authId, keyPair.getPublic().getAlgorithm(), publicKey, + instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); + final String jwt = jwtBuilder() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantPlus24Hours)).and() + .signWith(keyPair.getPrivate(), sigAlg) + .compact(); + authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) + .onComplete(ctx.succeeding(jws -> { + ctx.verify(() -> { + assertThat(jws.getHeader().getAlgorithm()).isEqualTo(alg.getId()); + }); + ctx.completeNow(); + })); + } else { + org.junit.Assert.fail("not a signature algorithm"); + } } @@ -118,47 +123,44 @@ void testExpandValidJwtWithValidPublicKey(final String algorithm, final int keyS */ @Test void testExpandValidJwtWithMultipleDifferentPublicKeysWithinTheirValidityPeriod(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.ES256; - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - final KeyPair keyPair1 = generateKeyPair(alg, 256); - final KeyPair keyPair2 = generateKeyPair(alg, 256); + final var alg = Jwts.SIG.ES256; + final KeyPair keyPair1 = alg.keyPair().build(); + final KeyPair keyPair2 = alg.keyPair().build(); final JsonObject secondSecret = CredentialsObject.emptySecret( instantNow.minusSeconds(1500), instantNow.plusSeconds(5000)) - .put(RegistryManagementConstants.FIELD_SECRETS_ALGORITHM, CredentialsConstants.EC_ALG) + .put(RegistryManagementConstants.FIELD_SECRETS_ALGORITHM, keyPair2.getPublic().getAlgorithm()) .put(CredentialsConstants.FIELD_SECRETS_KEY, keyPair2.getPublic().getEncoded()); final var creds = CredentialsObject.fromRawPublicKey( deviceId, authId, - CredentialsConstants.EC_ALG, + keyPair1.getPublic().getAlgorithm(), keyPair1.getPublic().getEncoded(), instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); creds.addSecret(secondSecret); - final String jwt1 = generateJws( - jwtHeader, - generateJwtClaims(null, null, instantNow, instantPlus24Hours), - alg, - keyPair1.getPrivate()); - final String jwt2 = generateJws( - jwtHeader, - generateJwtClaims(null, null, instantNow, instantPlus24Hours), - alg, - keyPair2.getPrivate()); + final String jwt1 = jwtBuilder() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantPlus24Hours)).and() + .signWith(keyPair1.getPrivate()) + .compact(); + final String jwt2 = jwtBuilder() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantPlus24Hours)).and() + .signWith(keyPair2.getPrivate()) + .compact(); authTokenValidator.expand(jwt1, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .compose(jws1 -> { ctx.verify(() -> { - assertThat(jws1.getBody().getExpiration().toInstant()).isAtMost(instantPlus24Hours); + assertThat(jws1.getPayload().getExpiration().toInstant()).isAtMost(instantPlus24Hours); }); return authTokenValidator.expand(jwt2, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW); }) .onComplete(ctx.succeeding(jws2 -> { ctx.verify(() -> { - assertThat(jws2.getBody().getExpiration().toInstant()).isAtMost(instantPlus24Hours); + assertThat(jws2.getPayload().getExpiration().toInstant()).isAtMost(instantPlus24Hours); }); ctx.completeNow(); })); @@ -169,18 +171,20 @@ void testExpandValidJwtWithMultipleDifferentPublicKeysWithinTheirValidityPeriod( */ @Test void testExpandFailsForJwsUsingHmacKey(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.HS256; - final Key key = generateHmacKey(); + final var alg = Jwts.SIG.HS256; + final Key key = alg.key().build(); final var creds = CredentialsObject.fromRawPublicKey( deviceId, authId, - alg.getFamilyName(), + key.getAlgorithm(), key.getEncoded(), instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); - final String jwt = generateJws(jwtHeader, - generateJwtClaims(null, null, instantNow, instantPlus24Hours), alg, key); + final String jwt = jwtBuilder() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantPlus24Hours)).and() + .signWith(key) + .compact(); authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { assertThat(t).isInstanceOf(ClientErrorException.class); @@ -193,20 +197,19 @@ void testExpandFailsForJwsUsingHmacKey(final VertxTestContext ctx) { /** * Verifies that expand fails when an invalid JWS is provided. */ - @Test void testExpandFailsForMalformedJws(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.ES256; - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - final KeyPair keyPair = generateKeyPair(alg, 256); + final var alg = Jwts.SIG.ES256; + final KeyPair keyPair = alg.keyPair().build(); final byte[] publicKey = keyPair.getPublic().getEncoded(); final var creds = CredentialsObject.fromRawPublicKey(deviceId, authId, CredentialsConstants.EC_ALG, publicKey, instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); - final String jwt = generateJws( - jwtHeader, - generateJwtClaims(null, null, instantNow, instantPlus24Hours), alg, keyPair.getPrivate()) - .replaceFirst("e", "a"); + final String jwt = jwtBuilder() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantPlus24Hours)).and() + .signWith(keyPair.getPrivate()) + .compact() + .replaceFirst("e", "a"); authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { @@ -224,14 +227,15 @@ void testExpandFailsForMalformedJws(final VertxTestContext ctx) { */ @Test void testExpandFailsForTokenWithoutIat(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.ES256; - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - final KeyPair keyPair = generateKeyPair(alg, 256); + final var alg = Jwts.SIG.ES256; + final KeyPair keyPair = alg.keyPair().build(); final var creds = CredentialsObject.fromRawPublicKey(deviceId, authId, CredentialsConstants.EC_ALG, keyPair.getPublic().getEncoded(), instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); - final String jwt = generateJws(jwtHeader, - generateJwtClaims(null, null, null, instantPlus24Hours), alg, keyPair.getPrivate()); + final String jwt = jwtBuilder() + .claims().expiration(Date.from(instantPlus24Hours)).and() + .signWith(keyPair.getPrivate()) + .compact(); authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { ctx.verify(() -> { @@ -247,15 +251,16 @@ void testExpandFailsForTokenWithoutIat(final VertxTestContext ctx) { * Verifies that expand fails for a token that does not contain an exp claim. */ @Test - void testExpandExpClaimMissing(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.ES256; - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - final KeyPair keyPair = generateKeyPair(alg, 256); + void testExpandFailsForTokenWithoutExpiration(final VertxTestContext ctx) { + final var alg = Jwts.SIG.ES256; + final KeyPair keyPair = alg.keyPair().build(); final var creds = CredentialsObject.fromRawPublicKey(deviceId, authId, CredentialsConstants.EC_ALG, keyPair.getPublic().getEncoded(), instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); - final String jwt = generateJws(jwtHeader, - generateJwtClaims(null, null, instantNow, null), alg, keyPair.getPrivate()); + final String jwt = jwtBuilder() + .claims().issuedAt(Date.from(instantNow)).and() + .signWith(keyPair.getPrivate()) + .compact(); authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { ctx.verify(() -> { @@ -272,10 +277,8 @@ void testExpandExpClaimMissing(final VertxTestContext ctx) { */ @Test void testExpandNotYetValidJwtWithValidEcPublicKey(final VertxTestContext ctx) { - - final SignatureAlgorithm alg = SignatureAlgorithm.ES256; - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - final KeyPair keyPair = generateKeyPair(alg, 256); + final var alg = Jwts.SIG.ES256; + final KeyPair keyPair = alg.keyPair().build(); final byte[] publicKey = keyPair.getPublic().getEncoded(); final var creds = CredentialsObject.fromRawPublicKey( deviceId, @@ -286,9 +289,10 @@ void testExpandNotYetValidJwtWithValidEcPublicKey(final VertxTestContext ctx) { instantNow.plusSeconds(3600)); final var tooFarInTheFuture = instantNow.plus(ALLOWED_CLOCK_SKEW).plusSeconds(5); - final String jwt = generateJws(jwtHeader, - generateJwtClaims(null, null, tooFarInTheFuture, instantPlus24Hours), - alg, keyPair.getPrivate()); + final String jwt = jwtBuilder() + .claims().issuedAt(Date.from(tooFarInTheFuture)).expiration(Date.from(instantPlus24Hours)).and() + .signWith(keyPair.getPrivate()) + .compact(); authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { ctx.verify(() -> { @@ -305,9 +309,8 @@ void testExpandNotYetValidJwtWithValidEcPublicKey(final VertxTestContext ctx) { */ @Test void testExpandFailsForTokenWithExpNotAfterIat(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.ES256; - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - final KeyPair keyPair = generateKeyPair(alg, 256); + final var alg = Jwts.SIG.ES256; + final KeyPair keyPair = alg.keyPair().build(); final var creds = CredentialsObject.fromRawPublicKey( deviceId, authId, @@ -316,11 +319,10 @@ void testExpandFailsForTokenWithExpNotAfterIat(final VertxTestContext ctx) { instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); - final String jwt = generateJws( - jwtHeader, - generateJwtClaims(null, null, instantNow, instantNow), - alg, - keyPair.getPrivate()); + final String jwt = jwtBuilder() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantNow)).and() + .signWith(keyPair.getPrivate()) + .compact(); authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { ctx.verify(() -> { @@ -337,9 +339,8 @@ void testExpandFailsForTokenWithExpNotAfterIat(final VertxTestContext ctx) { */ @Test void testExpandFailsForTokenExceedingMaxValidityPeriod(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.ES256; - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - final KeyPair keyPair = generateKeyPair(alg, 256); + final var alg = Jwts.SIG.ES256; + final KeyPair keyPair = alg.keyPair().build(); final var creds = CredentialsObject.fromRawPublicKey( deviceId, authId, @@ -348,11 +349,10 @@ void testExpandFailsForTokenExceedingMaxValidityPeriod(final VertxTestContext ct instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); - final String jwt = generateJws( - jwtHeader, - generateJwtClaims(null, null, instantNow, instantPlus24Hours.plus(ALLOWED_CLOCK_SKEW).plusSeconds(10)), - alg, - keyPair.getPrivate()); + final String jwt = jwtBuilder() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantPlus24Hours.plus(ALLOWED_CLOCK_SKEW).plusSeconds(10))).and() + .signWith(keyPair.getPrivate()) + .compact(); authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { ctx.verify(() -> { @@ -369,9 +369,8 @@ void testExpandFailsForTokenExceedingMaxValidityPeriod(final VertxTestContext ct */ @Test void testExpandFailsIfCandidateKeyCannotBeDeserialized(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.ES256; - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - final KeyPair keyPair = generateKeyPair(alg, 256); + final var alg = Jwts.SIG.ES256; + final KeyPair keyPair = alg.keyPair().build(); final byte[] publicKey = keyPair.getPublic().getEncoded(); publicKey[0] = publicKey[1]; final var creds = CredentialsObject.fromRawPublicKey( @@ -381,11 +380,10 @@ void testExpandFailsIfCandidateKeyCannotBeDeserialized(final VertxTestContext ct instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); - final String jwt = generateJws( - jwtHeader, - generateJwtClaims(null, null, instantNow, instantPlus24Hours), - alg, - keyPair.getPrivate()); + final String jwt = jwtBuilder() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantPlus24Hours)).and() + .signWith(keyPair.getPrivate()) + .compact(); authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { @@ -403,9 +401,8 @@ void testExpandFailsIfCandidateKeyCannotBeDeserialized(final VertxTestContext ct */ @Test void testExpandFailsForNonMatchingPublicKey(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.ES256; - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - KeyPair keyPair = generateKeyPair(alg, 256); + final var alg = Jwts.SIG.ES256; + KeyPair keyPair = alg.keyPair().build(); final byte[] publicKey = keyPair.getPublic().getEncoded(); final var creds = CredentialsObject.fromRawPublicKey( deviceId, @@ -415,9 +412,11 @@ void testExpandFailsForNonMatchingPublicKey(final VertxTestContext ctx) { instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); - keyPair = generateKeyPair(alg, 256); - final String jwt = generateJws(jwtHeader, - generateJwtClaims(null, null, instantNow, instantPlus24Hours), alg, keyPair.getPrivate()); + keyPair = alg.keyPair().build(); + final String jwt = jwtBuilder() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantPlus24Hours)).and() + .signWith(keyPair.getPrivate()) + .compact(); authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { ctx.verify(() -> { @@ -434,15 +433,13 @@ void testExpandFailsForNonMatchingPublicKey(final VertxTestContext ctx) { */ @Test void testExpandFailsIfNoCandidateKeyExist(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.RS256; - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - final KeyPair keyPair = generateKeyPair(alg, 2048); + final var alg = Jwts.SIG.RS256; + final KeyPair keyPair = alg.keyPair().build(); - final String jwt = generateJws( - jwtHeader, - generateJwtClaims(null, null, instantNow, instantPlus24Hours), - alg, - keyPair.getPrivate()); + final String jwt = jwtBuilder() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantPlus24Hours)).and() + .signWith(keyPair.getPrivate()) + .compact(); authTokenValidator.expand(jwt, List.of(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { @@ -460,22 +457,21 @@ void testExpandFailsIfNoCandidateKeyExist(final VertxTestContext ctx) { */ @Test void testExpandFailsForTokenWithoutTypeHeader(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.RS256; - final KeyPair keyPair = generateKeyPair(alg, 2048); + final var alg = Jwts.SIG.RS256; + final KeyPair keyPair = alg.keyPair().build(); final byte[] publicKey = keyPair.getPublic().getEncoded(); final var creds = CredentialsObject.fromRawPublicKey( deviceId, authId, - alg.getFamilyName(), + keyPair.getPrivate().getAlgorithm(), publicKey, instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); - final String jwt = generateJws( - Map.of(), - generateJwtClaims(null, null, instantNow, instantPlus24Hours), - alg, - keyPair.getPrivate()); + final String jwt = Jwts.builder() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantPlus24Hours)).and() + .signWith(keyPair.getPrivate()) + .compact(); authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { @@ -492,24 +488,23 @@ void testExpandFailsForTokenWithoutTypeHeader(final VertxTestContext ctx) { * Verifies that expand fails for a token that has a typ header with a value other than {@code JWT}. */ @Test - void testExpandTypFieldInJwtHeaderInvalid(final VertxTestContext ctx) { - final SignatureAlgorithm alg = SignatureAlgorithm.RS256; - final KeyPair keyPair = generateKeyPair(alg, 2048); + void testExpandFailsForTokenWithInvalidType(final VertxTestContext ctx) { + final var alg = Jwts.SIG.RS256; + final KeyPair keyPair = alg.keyPair().build(); final byte[] publicKey = keyPair.getPublic().getEncoded(); final var creds = CredentialsObject.fromRawPublicKey( deviceId, authId, - alg.getFamilyName(), + keyPair.getPrivate().getAlgorithm(), publicKey, instantNow.minusSeconds(3600), instantNow.plusSeconds(3600)); - jwtHeader.put(JwsHeader.TYPE, "invalid"); - final String jwt = generateJws( - jwtHeader, - generateJwtClaims(null, null, instantNow, instantPlus24Hours), - alg, - keyPair.getPrivate()); + final String jwt = Jwts.builder() + .header().type("invalid").and() + .claims().issuedAt(Date.from(instantNow)).expiration(Date.from(instantPlus24Hours)).and() + .signWith(keyPair.getPrivate()) + .compact(); authTokenValidator.expand(jwt, creds.getCandidateSecrets(), ALLOWED_CLOCK_SKEW) .onComplete(ctx.failing(t -> { @@ -528,12 +523,18 @@ void testExpandTypFieldInJwtHeaderInvalid(final VertxTestContext ctx) { @Test void testGetJwtClaimsValidJwt() { final String tenantId = "tenant-id"; - final SignatureAlgorithm alg = SignatureAlgorithm.RS256; - jwtHeader.put(JwsHeader.ALGORITHM, alg.getValue()); - final KeyPair keyPair = generateKeyPair(alg, 2048); - - final String jwt = generateJws(jwtHeader, - generateJwtClaims(tenantId, authId, instantNow, instantPlus24Hours), alg, keyPair.getPrivate()); + final var alg = Jwts.SIG.RS256; + final KeyPair keyPair = alg.keyPair().build(); + + final String jwt = jwtBuilder() + .claims() + .issuer(tenantId) + .subject(authId) + .issuedAt(Date.from(instantNow)) + .expiration(Date.from(instantPlus24Hours)) + .and() + .signWith(keyPair.getPrivate()) + .compact(); final JsonObject claims = DefaultJwsValidator.getJwtClaims(jwt); assertThat(claims.getString(Claims.ISSUER)).isEqualTo(tenantId); assertThat(claims.getString(Claims.SUBJECT)).isEqualTo(authId); @@ -548,50 +549,9 @@ void testGetJwtClaimsInvalidJwt() { assertThrows(MalformedJwtException.class, () -> DefaultJwsValidator.getJwtClaims(jwt)); } - private String generateJws(final Map header, final Map claims, - final SignatureAlgorithm alg, final Key key) { - final JwtBuilder jwtBuilder = Jwts.builder().setHeaderParams(header).setClaims(claims).signWith(key, alg); - return jwtBuilder.compact(); - } - - private KeyPair generateKeyPair(final SignatureAlgorithm alg, final int keySize) { - String algType = alg.getFamilyName(); - if (alg.isEllipticCurve()) { - algType = CredentialsConstants.EC_ALG; - } - try { - final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algType); - keyPairGenerator.initialize(keySize); - return keyPairGenerator.generateKeyPair(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - private Key generateHmacKey() { - try { - final KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacSHA256"); - return keyGenerator.generateKey(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - private Map generateJwtClaims( - final String iss, - final String sub, - final Instant iat, - final Instant exp) { - - final Map jwtClaims = new HashMap<>(); - jwtClaims.put(Claims.ISSUER, iss); - jwtClaims.put(Claims.SUBJECT, sub); - if (iat != null) { - jwtClaims.put(Claims.ISSUED_AT, Date.from(iat)); - } - if (exp != null) { - jwtClaims.put(Claims.EXPIRATION, Date.from(exp)); - } - return jwtClaims; + private KeyPair generateKeyPair(final String algType, final int keySize) throws NoSuchAlgorithmException { + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algType); + keyPairGenerator.initialize(keySize); + return keyPairGenerator.generateKeyPair(); } } diff --git a/adapters/mqtt-base/src/main/java/org/eclipse/hono/adapter/mqtt/JwtAuthHandler.java b/adapters/mqtt-base/src/main/java/org/eclipse/hono/adapter/mqtt/JwtAuthHandler.java index 2bcba6005c..0dce392be9 100644 --- a/adapters/mqtt-base/src/main/java/org/eclipse/hono/adapter/mqtt/JwtAuthHandler.java +++ b/adapters/mqtt-base/src/main/java/org/eclipse/hono/adapter/mqtt/JwtAuthHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -15,6 +15,7 @@ import java.net.HttpURLConnection; import java.util.Objects; +import java.util.Optional; import org.eclipse.hono.adapter.auth.device.DeviceCredentialsAuthProvider; import org.eclipse.hono.adapter.auth.device.ExecutionContextAuthHandler; @@ -30,6 +31,7 @@ import io.jsonwebtoken.MalformedJwtException; import io.vertx.core.Future; import io.vertx.core.Promise; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.mqtt.MqttAuth; @@ -112,14 +114,22 @@ public Future parseCredentials(final MqttConnectContext context) { try { final var claims = DefaultJwsValidator.getJwtClaims(auth.getPassword()); - final JsonObject credentials; - if (Objects.equals(claims.getString(Claims.AUDIENCE), CredentialsConstants.AUDIENCE_HONO_ADAPTER)) { + final JsonObject credentials = Optional.ofNullable(claims.getValue(Claims.AUDIENCE)) + .map(v -> { + // support both kinds of audience claim values: single string and array of strings + if (v instanceof String stringValue) { + return JsonArray.of(stringValue); + } else if (v instanceof JsonArray array) { + return array; + } else { + return JsonArray.of(); + } + }) + .filter(aud -> aud.contains(CredentialsConstants.AUDIENCE_HONO_ADAPTER)) // extract tenant, device ID and issuer from claims - credentials = parseCredentialsFromClaims(claims); - } else { + .map(aud -> parseCredentialsFromClaims(claims)) // extract tenant and device ID from MQTT client identifier - credentials = parseCredentialsFromString(context.deviceEndpoint().clientIdentifier()); - } + .orElseGet(() -> parseCredentialsFromString(context.deviceEndpoint().clientIdentifier())); credentials.put(CredentialsConstants.FIELD_PASSWORD, auth.getPassword()); result.complete(credentials); } catch (final MalformedJwtException e) { diff --git a/adapters/mqtt-base/src/test/java/org/eclipse/hono/adapter/mqtt/JwtAuthHandlerTest.java b/adapters/mqtt-base/src/test/java/org/eclipse/hono/adapter/mqtt/JwtAuthHandlerTest.java index af5ad91362..8949dae08e 100644 --- a/adapters/mqtt-base/src/test/java/org/eclipse/hono/adapter/mqtt/JwtAuthHandlerTest.java +++ b/adapters/mqtt-base/src/test/java/org/eclipse/hono/adapter/mqtt/JwtAuthHandlerTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -87,10 +87,10 @@ private String getJws(final String audience, final String tenant, final String s throw new RuntimeException(e); } final var builder = Jwts.builder().signWith(key); - Optional.ofNullable(audience).ifPresent(builder::setAudience); + Optional.ofNullable(audience).ifPresent(aud -> builder.audience().add(aud)); Optional.ofNullable(tenant).ifPresent(t -> builder.claim(CredentialsConstants.CLAIM_TENANT_ID, t)); - Optional.ofNullable(subject).ifPresent(s -> builder.setSubject(s).setIssuer(s)); - builder.setExpiration(Date.from(Instant.now().plus(Duration.ofMinutes(10)))); + Optional.ofNullable(subject).ifPresent(s -> builder.subject(s).issuer(s)); + builder.expiration(Date.from(Instant.now().plus(Duration.ofMinutes(10)))); return builder.compact(); } @@ -132,7 +132,7 @@ public void testParseCredentialsMqttClientIdIncludesIds( /** * Verifies that the handler extracts the tenant and auth ID from the JWT, - * if the JWT's audience claim does have value {@value CredentialsConstants#AUDIENCE_HONO_ADAPTER}. + * if the JWT's audience claim contains {@value CredentialsConstants#AUDIENCE_HONO_ADAPTER}. * * @param ctx The vert.x test context. */ diff --git a/bom/pom.xml b/bom/pom.xml index 6912b6961b..f1dcdde359 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -38,7 +38,7 @@ quay.io/infinispan/server-native:13.0.12.Final docker.io/jaegertracing/all-in-one:1.51 docker.io/library/eclipse-temurin:17-jre-jammy - 0.11.5 + 0.12.5 docker.io/confluentinc/cp-kafka:7.5.0 1.4.12 docker.io/library/mongo:6.0 diff --git a/core/src/test/java/org/eclipse/hono/auth/AuthoritiesImplTest.java b/core/src/test/java/org/eclipse/hono/auth/AuthoritiesImplTest.java index eebe873b3c..8dec5d9b0d 100644 --- a/core/src/test/java/org/eclipse/hono/auth/AuthoritiesImplTest.java +++ b/core/src/test/java/org/eclipse/hono/auth/AuthoritiesImplTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2020 Contributors to the Eclipse Foundation + * Copyright (c) 2016 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -34,10 +34,11 @@ public class AuthoritiesImplTest { @Test public void testFromClaims() { - final Claims claims = Jwts.claims(); - claims.put("r:telemetry/*", "W"); - claims.put("r:registration/DEFAULT_TENANT", "RW"); - claims.put("o:credentials/*:get", "E"); + final Claims claims = Jwts.claims() + .add("r:telemetry/*", "W") + .add("r:registration/DEFAULT_TENANT", "RW") + .add("o:credentials/*:get", "E") + .build(); final Authorities auth = AuthoritiesImpl.from(claims); assertThat(auth.isAuthorized(ResourceIdentifier.fromString("telemetry/tenantA"), Activity.WRITE)).isTrue(); assertThat(auth.isAuthorized(ResourceIdentifier.fromString("registration/DEFAULT_TENANT"), Activity.READ)).isTrue(); diff --git a/legal/src/main/resources/legal/DEPENDENCIES b/legal/src/main/resources/legal/DEPENDENCIES index 69d4c5d70e..c4db4b00e7 100644 --- a/legal/src/main/resources/legal/DEPENDENCIES +++ b/legal/src/main/resources/legal/DEPENDENCIES @@ -93,9 +93,9 @@ maven/mavencentral/io.grpc/grpc-protobuf-lite/1.56.0, Apache-2.0, approved, clea maven/mavencentral/io.grpc/grpc-services/1.56.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.grpc/grpc-stub/1.56.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.grpc/grpc-xds/1.56.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.jsonwebtoken/jjwt-api/0.11.5, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.jsonwebtoken/jjwt-impl/0.11.5, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.jsonwebtoken/jjwt-jackson/0.11.5, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.jsonwebtoken/jjwt-api/0.12.5, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.jsonwebtoken/jjwt-impl/0.12.5, Apache-2.0, approved, #14485 +maven/mavencentral/io.jsonwebtoken/jjwt-jackson/0.12.5, Apache-2.0, approved, clearlydefined maven/mavencentral/io.micrometer/micrometer-commons/1.11.1, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #9243 maven/mavencentral/io.micrometer/micrometer-core/1.11.1, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #9238 maven/mavencentral/io.micrometer/micrometer-observation/1.11.1, Apache-2.0, approved, #9242 diff --git a/legal/src/main/resources/legal/hono-maven.deps b/legal/src/main/resources/legal/hono-maven.deps index 9b7dff9b69..00e6419366 100644 --- a/legal/src/main/resources/legal/hono-maven.deps +++ b/legal/src/main/resources/legal/hono-maven.deps @@ -93,9 +93,9 @@ io.grpc:grpc-protobuf-lite:jar:1.56.0 io.grpc:grpc-services:jar:1.56.0 io.grpc:grpc-stub:jar:1.56.0 io.grpc:grpc-xds:jar:1.56.0 -io.jsonwebtoken:jjwt-api:jar:0.11.5 -io.jsonwebtoken:jjwt-impl:jar:0.11.5 -io.jsonwebtoken:jjwt-jackson:jar:0.11.5 +io.jsonwebtoken:jjwt-api:jar:0.12.5 +io.jsonwebtoken:jjwt-impl:jar:0.12.5 +io.jsonwebtoken:jjwt-jackson:jar:0.12.5 io.micrometer:micrometer-commons:jar:1.11.1 io.micrometer:micrometer-core:jar:1.11.1 io.micrometer:micrometer-observation:jar:1.11.1 diff --git a/service-base/src/main/java/org/eclipse/hono/service/auth/AuthTokenFactory.java b/service-base/src/main/java/org/eclipse/hono/service/auth/AuthTokenFactory.java index b2eb445df7..331a8ec8a0 100644 --- a/service-base/src/main/java/org/eclipse/hono/service/auth/AuthTokenFactory.java +++ b/service-base/src/main/java/org/eclipse/hono/service/auth/AuthTokenFactory.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2016 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -17,7 +17,7 @@ import org.eclipse.hono.auth.Authorities; -import io.vertx.core.json.JsonObject; +import io.jsonwebtoken.security.JwkSet; /** * A factory for creating JSON Web Tokens containing user identity and @@ -64,5 +64,5 @@ public interface AuthTokenFactory { * @return The JWK set. * @see RFC 7517 */ - JsonObject getValidatingJwkSet(); + JwkSet getValidatingJwkSet(); } diff --git a/service-base/src/main/java/org/eclipse/hono/service/auth/EventBusAuthenticationService.java b/service-base/src/main/java/org/eclipse/hono/service/auth/EventBusAuthenticationService.java index 2ca75fc378..0bb8ed8197 100644 --- a/service-base/src/main/java/org/eclipse/hono/service/auth/EventBusAuthenticationService.java +++ b/service-base/src/main/java/org/eclipse/hono/service/auth/EventBusAuthenticationService.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2016 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -153,17 +153,17 @@ public static final class HonoUserImpl implements HonoUser { private HonoUserImpl(final Jws expandedToken, final String token) { Objects.requireNonNull(expandedToken); Objects.requireNonNull(token); - if (expandedToken.getBody() == null) { + if (expandedToken.getPayload() == null) { throw new IllegalArgumentException("token has no claims"); } this.token = token; this.expandedToken = expandedToken; - this.authorities = AuthoritiesImpl.from(expandedToken.getBody()); + this.authorities = AuthoritiesImpl.from(expandedToken.getPayload()); } @Override public String getName() { - return expandedToken.getBody().getSubject(); + return expandedToken.getPayload().getSubject(); } @Override @@ -185,7 +185,7 @@ public boolean isExpired() { @Override public Instant getExpirationTime() { - return expandedToken.getBody().getExpiration().toInstant(); + return expandedToken.getPayload().getExpiration().toInstant(); } } } diff --git a/service-base/src/main/java/org/eclipse/hono/service/auth/ExternalJwtAuthTokenValidator.java b/service-base/src/main/java/org/eclipse/hono/service/auth/ExternalJwtAuthTokenValidator.java deleted file mode 100644 index 78886366e8..0000000000 --- a/service-base/src/main/java/org/eclipse/hono/service/auth/ExternalJwtAuthTokenValidator.java +++ /dev/null @@ -1,209 +0,0 @@ -/** - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * https://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ - -package org.eclipse.hono.service.auth; - -import java.security.Key; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; -import java.time.Instant; -import java.util.Base64; -import java.util.Date; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Optional; - -import org.eclipse.hono.util.CredentialsConstants; -import org.eclipse.hono.util.CredentialsObject; -import org.eclipse.hono.util.RegistryManagementConstants; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwsHeader; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SigningKeyResolverAdapter; -import io.jsonwebtoken.UnsupportedJwtException; -import io.jsonwebtoken.security.SignatureException; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.json.JsonObject; - -/** - * A class to validate a JSON Web Token (JWT) against a CredentialsObject containing a public key/certificate. - */ -public class ExternalJwtAuthTokenValidator implements AuthTokenValidator { - - static final int ALLOWED_CLOCK_SKEW = 600; - private static final String EXPECTED_TOKEN_TYPE = "JWT"; - private CredentialsObject credentialsObject; - - /** - * Adds the allowed clock skew to the provided expiration time. - * - * @param exp The expiration time without the allowed clock skew. - * @return The expiration time with the allowed clock skew. - * @throws NullPointerException If the input parameter is null. - */ - public static Instant getExpirationTime(final Instant exp) { - return exp.plusSeconds(ALLOWED_CLOCK_SKEW); - } - - public CredentialsObject getCredentialsObject() { - return credentialsObject; - } - - public void setCredentialsObject(final CredentialsObject credentialsObject) { - this.credentialsObject = credentialsObject; - } - - @Override - public Jws expand(final String token) { - Objects.requireNonNull(token); - Jws claims; - SignatureException signatureException = null; - final var builder = Jwts.parserBuilder() - .setAllowedClockSkewSeconds(ALLOWED_CLOCK_SKEW); - for (int i = 0; true; i++) { - try { - - final int index = i; - builder.setSigningKeyResolver(new SigningKeyResolverAdapter() { - - @Override - public Key resolveSigningKey( - @SuppressWarnings("rawtypes") final JwsHeader header, - final Claims claims) { - - final var tokenType = Optional.ofNullable(header.getType()) - .orElseThrow( - () -> new MalformedJwtException("token does not contain required typ header.")); - if (!tokenType.equalsIgnoreCase(EXPECTED_TOKEN_TYPE)) { - throw new MalformedJwtException(String.format( - "typ field in token header is invalid. Must be \"%s\".", EXPECTED_TOKEN_TYPE)); - } - - final var algorithm = Optional.ofNullable(header.getAlgorithm()) - .orElseThrow( - () -> new MalformedJwtException("token does not contain required alg header.")); - final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.forName(algorithm); - - checkValidityOfExpirationAndCreationTime(claims.getExpiration(), claims.getIssuedAt()); - claims.setNotBefore(claims.getIssuedAt()); - - final List secrets = getCredentialsObject().getCandidateSecrets(); - final List validSecretsList = secrets.stream() - .filter(secret -> signatureAlgorithm.getFamilyName() - .startsWith( - secret.getString(RegistryManagementConstants.FIELD_SECRETS_ALGORITHM))) - .toList(); - - final byte[] encodedPublicKey = Objects.requireNonNull(validSecretsList.get(index) - .getBinary(RegistryManagementConstants.FIELD_SECRETS_KEY)); - - return convertPublicKeyByteArrayToPublicKey(encodedPublicKey, signatureAlgorithm); - } - }); - claims = builder.build().parseClaimsJws(token); - break; - } catch (SignatureException e) { - signatureException = e; - } catch (IndexOutOfBoundsException e) { - if (signatureException != null) { - throw signatureException; - } else { - throw new NoSuchElementException( - "There is no valid raw public key (\"rpk\") saved with the same algorithm as the provided JWT."); - } - } - } - return claims; - } - - /** - * Extracts the claims from a JSON Web Token (JWT) embedded in a JSON Web - * Signature (JWS) structure. - * - * @param jws The JWS structure. - * @return The claims contained in the token. - * @throws NullPointerException if the JWS is {@code null}. - * @throws MalformedJwtException if the JWT's payload can not be parsed into a JSON object. - */ - public JsonObject getJwtClaims(final String jws) { - - Objects.requireNonNull(jws); - final String[] jwtSplit = jws.split("\\.", 3); - if (jwtSplit.length != 3) { - throw new MalformedJwtException("String is not a valid JWS structure"); - } - - try { - final Buffer p = Buffer.buffer(Base64.getUrlDecoder().decode(jwtSplit[1])); - return new JsonObject(p); - } catch (final RuntimeException e) { - throw new MalformedJwtException("Cannot parse JWS payload into JSON object", e); - } - } - - private void checkValidityOfExpirationAndCreationTime(final Date expDate, final Date iatDate) { - final Instant exp; - final Instant iat; - try { - exp = expDate.toInstant(); - iat = iatDate.toInstant(); - } catch (NullPointerException e) { - throw new UnsupportedJwtException("iat and exp claims must be provided in JWT payload."); - } - final Instant startOfValidity = Instant.now().minusSeconds(ALLOWED_CLOCK_SKEW); - final int validityPeriodHours = 24; - final Instant endOfValidity = iat.plusSeconds(validityPeriodHours * 3600 + ALLOWED_CLOCK_SKEW); - if (iat.isBefore(startOfValidity)) { - throw new UnsupportedJwtException( - String.format("Timestamp in iat claim must be at most %s seconds before the current timestamp.", - ALLOWED_CLOCK_SKEW)); - } - if (!exp.isAfter(iat)) { - throw new UnsupportedJwtException("Timestamp in exp claim must not be before timestamp in iat claim."); - } - if (exp.isAfter(endOfValidity)) { - throw new UnsupportedJwtException(String.format( - "Timestamp in exp claim must be at most %s hours after the iat claim with a skew of %s seconds.", - validityPeriodHours, ALLOWED_CLOCK_SKEW)); - } - } - - private PublicKey convertPublicKeyByteArrayToPublicKey(final byte[] encodedPublicKey, - final SignatureAlgorithm alg) { - final X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(encodedPublicKey); - final PublicKey publicKey; - try { - final KeyFactory keyFactory; - if (alg.isRsa()) { - keyFactory = KeyFactory.getInstance(CredentialsConstants.RSA_ALG); - } else if (alg.isEllipticCurve()) { - keyFactory = KeyFactory.getInstance(CredentialsConstants.EC_ALG); - } else { - throw new RuntimeException("Provided algorithm is not supported."); - } - publicKey = keyFactory.generatePublic(keySpecX509); - } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - return publicKey; - } - -} diff --git a/service-base/src/main/java/org/eclipse/hono/service/auth/JjwtBasedAuthTokenFactory.java b/service-base/src/main/java/org/eclipse/hono/service/auth/JjwtBasedAuthTokenFactory.java index e16a0d14ee..d0cc961e96 100644 --- a/service-base/src/main/java/org/eclipse/hono/service/auth/JjwtBasedAuthTokenFactory.java +++ b/service-base/src/main/java/org/eclipse/hono/service/auth/JjwtBasedAuthTokenFactory.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2016 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,65 +13,62 @@ package org.eclipse.hono.service.auth; -import java.security.AlgorithmParameters; -import java.security.GeneralSecurityException; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.ECGenParameterSpec; +import java.security.PublicKey; import java.time.Duration; import java.time.Instant; -import java.util.Base64; import java.util.Date; -import java.util.Map; import java.util.Objects; import java.util.Optional; import javax.crypto.SecretKey; import org.eclipse.hono.auth.Authorities; -import org.eclipse.hono.util.CredentialsConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.JwkSet; +import io.jsonwebtoken.security.Jwks; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SecurityException; import io.quarkus.runtime.annotations.RegisterForReflection; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; /** * A JJWT based factory for creating signed JSON Web Tokens containing user claims. * */ @RegisterForReflection(targets = { + io.jsonwebtoken.impl.DefaultClaimsBuilder.class, io.jsonwebtoken.impl.DefaultJwtBuilder.class, - io.jsonwebtoken.jackson.io.JacksonSerializer.class, - io.jsonwebtoken.impl.compression.GzipCompressionCodec.class + io.jsonwebtoken.impl.DefaultJwtHeaderBuilder.class, + io.jsonwebtoken.impl.DefaultJwtParserBuilder.class, + io.jsonwebtoken.impl.compression.DeflateCompressionAlgorithm.class, + io.jsonwebtoken.impl.compression.GzipCompressionAlgorithm.class, + io.jsonwebtoken.impl.io.StandardCompressionAlgorithms.class, + io.jsonwebtoken.impl.security.DefaultDynamicJwkBuilder.class, + io.jsonwebtoken.impl.security.DefaultJwkParserBuilder.class, + io.jsonwebtoken.impl.security.DefaultJwkSetBuilder.class, + io.jsonwebtoken.impl.security.DefaultJwkSetParserBuilder.class, + io.jsonwebtoken.impl.security.DefaultKeyOperationBuilder.class, + io.jsonwebtoken.impl.security.DefaultKeyOperationPolicyBuilder.class, + io.jsonwebtoken.impl.security.JwksBridge.class, + io.jsonwebtoken.impl.security.StandardCurves.class, + io.jsonwebtoken.impl.security.StandardEncryptionAlgorithms.class, + io.jsonwebtoken.impl.security.StandardHashAlgorithms.class, + io.jsonwebtoken.impl.security.StandardKeyAlgorithms.class, + io.jsonwebtoken.impl.security.StandardKeyOperations.class, + io.jsonwebtoken.impl.security.StandardSecureDigestAlgorithms.class, + io.jsonwebtoken.jackson.io.JacksonDeserializer.class, + io.jsonwebtoken.jackson.io.JacksonSerializer.class }) public final class JjwtBasedAuthTokenFactory extends JwtSupport implements AuthTokenFactory { private static final Logger LOG = LoggerFactory.getLogger(JjwtBasedAuthTokenFactory.class); - private static final String PROPERTY_JWK_ALG = "alg"; - private static final String PROPERTY_JWK_CRV = "crv"; - private static final String PROPERTY_JWK_E = "e"; - private static final String PROPERTY_JWK_KEY_OPS = "key_ops"; - private static final String PROPERTY_JWK_K = "k"; - private static final String PROPERTY_JWK_KID = "kid"; - private static final String PROPERTY_JWK_KTY = "kty"; - private static final String PROPERTY_JWK_N = "n"; - private static final String PROPERTY_JWK_USE = "use"; - private static final String PROPERTY_JWK_X = "x"; - private static final String PROPERTY_JWK_Y = "y"; - - private static final Map CURVE_OID_TO_NIST_NAME = Map.of( - "1.2.840.10045.3.1.7", "P-256", - "1.3.132.0.34", "P-384", - "1.3.132.0.35", "P-521"); - - private final JsonObject jwkSet = new JsonObject(); + + private final JwkSet jwkSet; private final SignatureSupportingConfigProperties config; private final Duration tokenLifetime; private final String signingKeyId; @@ -108,7 +105,7 @@ public JjwtBasedAuthTokenFactory(final Vertx vertx, final SignatureSupportingCon tokenLifetime = Duration.ofSeconds(config.getTokenExpiration()); LOG.info("using token lifetime of {} seconds", tokenLifetime.getSeconds()); this.config = config; - createJwk(); + this.jwkSet = createJwkSet(); } catch (final SecurityException e) { throw new IllegalArgumentException("failed to create factory for configured key material", e); } @@ -119,72 +116,24 @@ public Duration getTokenLifetime() { return tokenLifetime; } - private void createJwk() { - final JsonArray keyArray = getValidatingKeys().stream() - .map(entry -> { - final var jwk = new JsonObject(); - final var keyId = entry.getKey(); - final var keySpec = entry.getValue(); - jwk.put(PROPERTY_JWK_USE, "sig"); - jwk.put(PROPERTY_JWK_KEY_OPS, "verify"); - if (keySpec.key instanceof SecretKey secretKey) { - jwk.put(PROPERTY_JWK_KTY, "oct"); - jwk.put(PROPERTY_JWK_K, keySpec.key.getEncoded()); - } else { - addPublicKey(jwk, keySpec); + private JwkSet createJwkSet() { + final var jwkSetBuilder = Jwks.set(); + getValidatingKeys().stream() + .>mapMulti((entry, consumer) -> { + final var key = entry.getValue(); + final var builder = Jwks.builder() + .id(entry.getKey()) + .operations().add(Jwks.OP.VERIFY).and(); + if (key instanceof SecretKey secretKey) { + consumer.accept(builder.key(secretKey).build()); + } else if (key instanceof PublicKey publicKey) { + consumer.accept(builder.key(publicKey).build()); } - jwk.put(PROPERTY_JWK_KID, keyId); - jwk.put(PROPERTY_JWK_ALG, keySpec.algorithm.getValue()); - return jwk; }) - .collect(JsonArray::new, JsonArray::add, JsonArray::addAll); - jwkSet.put("keys", keyArray); - if (LOG.isInfoEnabled()) { - LOG.info("successfully created JWK set:{}{}", System.lineSeparator(), jwkSet.encodePrettily()); - } - } - - private void addPublicKey(final JsonObject jwk, final KeySpec keySpec) { - - if (keySpec.key instanceof RSAPublicKey rsaPublicKey) { - addRsaPublicKey(jwk, rsaPublicKey); - } else if (keySpec.key instanceof ECPublicKey ecPublicKey) { - addEcPublicKey(jwk, ecPublicKey); - } else { - throw new IllegalArgumentException( - "unsupported key type [%s], must be RSA or EC".formatted(keySpec.key.getAlgorithm())); - } - jwk.put(PROPERTY_JWK_KTY, keySpec.key.getAlgorithm()); - } - - private void addRsaPublicKey(final JsonObject jwk, final RSAPublicKey rsaPublicKey) { - // according to https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.1 - // the modulus and exponent need to be base64url encoded without padding - final var encoder = Base64.getUrlEncoder().withoutPadding(); - jwk.put(PROPERTY_JWK_N, encoder.encodeToString(rsaPublicKey.getModulus().toByteArray())); - jwk.put(PROPERTY_JWK_E, encoder.encodeToString(rsaPublicKey.getPublicExponent().toByteArray())); - } - - private String getNistCurveName(final ECPublicKey ecPublicKey) throws GeneralSecurityException { - final AlgorithmParameters parameters = AlgorithmParameters.getInstance(CredentialsConstants.EC_ALG); - parameters.init(ecPublicKey.getParams()); - final var spec = parameters.getParameterSpec(ECGenParameterSpec.class); - final var oid = spec.getName(); - return Optional.ofNullable(CURVE_OID_TO_NIST_NAME.get(oid)) - .orElseThrow(() -> new IllegalArgumentException("unsupported curve [%s]".formatted(oid))); - } - - private void addEcPublicKey(final JsonObject jwk, final ECPublicKey ecPublicKey) { - try { - jwk.put(PROPERTY_JWK_CRV, getNistCurveName(ecPublicKey)); - // according to https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2 - // the X and Y coordinates need to be base64url encoded without padding - final var encoder = Base64.getUrlEncoder().withoutPadding(); - jwk.put(PROPERTY_JWK_X, encoder.encodeToString(ecPublicKey.getW().getAffineX().toByteArray())); - jwk.put(PROPERTY_JWK_Y, encoder.encodeToString(ecPublicKey.getW().getAffineY().toByteArray())); - } catch (final GeneralSecurityException e) { - throw new IllegalArgumentException("cannot serialize EC based public key", e); - } + .forEach(jwkSetBuilder::add); + final var jwkSet = jwkSetBuilder.build(); + LOG.info("successfully created JWK set containing {} keys", jwkSet.size()); + return jwkSet; } @Override @@ -192,18 +141,18 @@ public String createToken(final String authorizationId, final Authorities author Objects.requireNonNull(authorizationId); - final var signingKeySpec = getSigningKey(signingKeyId); + final var signingKey = getSigningKey(signingKeyId); final JwtBuilder builder = Jwts.builder() - .signWith(signingKeySpec.key, signingKeySpec.algorithm) - .setHeaderParam(PROPERTY_JWK_KID, signingKeyId) - .setIssuer(config.getIssuer()) - .setSubject(authorizationId) - .setExpiration(Date.from(Instant.now().plus(tokenLifetime))); + .header().keyId(signingKeyId).and() + .issuer(config.getIssuer()) + .subject(authorizationId) + .expiration(Date.from(Instant.now().plus(tokenLifetime))) + .signWith(signingKey); Optional.ofNullable(authorities) .map(Authorities::asMap) - .ifPresent(authMap -> authMap.forEach(builder::claim)); + .ifPresent(builder::claims); Optional.ofNullable(config.getAudience()) - .ifPresent(builder::setAudience); + .ifPresent(aud -> builder.audience().add(aud)); return builder.compact(); } @@ -211,7 +160,7 @@ public String createToken(final String authorizationId, final Authorities author * {@inheritDoc} */ @Override - public JsonObject getValidatingJwkSet() { + public JwkSet getValidatingJwkSet() { return jwkSet; } } diff --git a/service-base/src/main/java/org/eclipse/hono/service/auth/JjwtBasedAuthTokenValidator.java b/service-base/src/main/java/org/eclipse/hono/service/auth/JjwtBasedAuthTokenValidator.java index 0a41116d58..72a1defeff 100644 --- a/service-base/src/main/java/org/eclipse/hono/service/auth/JjwtBasedAuthTokenValidator.java +++ b/service-base/src/main/java/org/eclipse/hono/service/auth/JjwtBasedAuthTokenValidator.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2016 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -29,12 +29,11 @@ import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SigningKeyResolverAdapter; -import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.LocatorAdapter; +import io.jsonwebtoken.security.JwkSet; +import io.jsonwebtoken.security.Jwks; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SecurityException; -import io.jsonwebtoken.security.SignatureException; -import io.quarkus.runtime.annotations.RegisterForReflection; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpClient; @@ -44,9 +43,6 @@ import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.RequestOptions; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.auth.impl.jose.JWK; /** * A parser that creates a token from a compact serialization of a JWS containing a JSON Web Token as payload. @@ -54,11 +50,6 @@ * Supports retrieving a JWT set that contains the key(s) from a web resource. * */ -@RegisterForReflection(targets = { - io.jsonwebtoken.impl.DefaultJwtParserBuilder.class, - io.jsonwebtoken.jackson.io.JacksonDeserializer.class, - io.jsonwebtoken.impl.compression.DeflateCompressionCodec.class -}) public final class JjwtBasedAuthTokenValidator extends JwtSupport implements AuthTokenValidator { private static final Logger LOG = LoggerFactory.getLogger(JjwtBasedAuthTokenValidator.class); @@ -170,32 +161,24 @@ private void requestJwkSet() { httpClient.request(requestOptions) .compose(HttpClientRequest::send) .compose(HttpClientResponse::body) - .map(Buffer::toJsonObject) - .map(json -> { + .map(Buffer::toString) + .map(jsonString -> { + final JwkSet jwkSet = Jwks.setParser().build().parse(jsonString); if (LOG.isDebugEnabled()) { - LOG.debug("server returned JWK set:{}{}", System.lineSeparator(), json.encodePrettily()); + LOG.debug("server returned JWK set:{}{}", System.lineSeparator(), jwkSet.toString()); } - final var keys = json.getJsonArray("keys", new JsonArray()); - if (keys.isEmpty()) { + if (jwkSet.isEmpty()) { LOG.warn("server returned empty key set, won't be able to validate tokens"); } - return keys; + return jwkSet; }) .onSuccess(jwkSet -> { - final Map keys = new HashMap<>(); - jwkSet.stream() - .filter(JsonObject.class::isInstance) - .map(JsonObject.class::cast) - .forEach(json -> { - if (isJwksSignatureAlgorithmRequired && !json.containsKey("alg")) { - LOG.warn("JSON Web Key does not contain required alg property, skipping key ..."); + final Map keys = new HashMap<>(); + jwkSet.forEach(jwk -> { + if (isJwksSignatureAlgorithmRequired && jwk.getAlgorithm() == null) { + LOG.warn("JSON Web Key [id: {}] does not contain required alg property, skipping key ...", jwk.getId()); } else { - try { - final var jwk = new JWK(json); - keys.put(jwk.getId(), new KeySpec(jwk.publicKey(), jwk.getAlgorithm())); - } catch (final Exception e) { - LOG.warn("failed to deserialize JSON Web Key retrieved from server", e.getCause()); - } + keys.put(jwk.getId(), jwk.toKey()); } }); setValidatingKeys(keys); @@ -209,55 +192,33 @@ private void requestJwkSet() { .onComplete(ar -> jwksPollingInProgress.set(false)); } - private Key getValidatingKey(final String keyId, final String algorithmName) { - if (keyId == null) { - LOG.debug("token has no kid header, will try to use default key for validating signature"); - final var keySpec = getValidatingKey(); - if (keySpec.supportsSignatureAlgorithm(algorithmName)) { - return keySpec.key; - } else { - throw new InvalidKeyException(""" - validating key on record does not support signature algorithm [%s] used in token\ - """.formatted(algorithmName)); - } - } else { - final var keySpec = getValidatingKey(keyId); - if (keySpec == null) { - LOG.debug("unknown validating key [id: {}]", keyId); - requestJwkSet(); - throw new InvalidKeyException("unknown validating key"); - } else if (keySpec.supportsSignatureAlgorithm(algorithmName)) { - LOG.debug("using key [id: {}] to validate signature (alg: {}]", keyId, algorithmName); - return keySpec.key; - } else { - throw new InvalidKeyException(""" - validating key on record [id: %s] does not support signature - algorithm [%s] used in token\ - """.formatted(keyId, algorithmName)); - } - } - } - @Override public Jws expand(final String token) { Objects.requireNonNull(token); - final var builder = Jwts.parserBuilder() + final var builder = Jwts.parser() .requireIssuer(config.getIssuer()) - .setSigningKeyResolver(new SigningKeyResolverAdapter() { + .keyLocator(new LocatorAdapter() { @Override - public Key resolveSigningKey( - @SuppressWarnings("rawtypes") final JwsHeader header, - final Claims claims) { - - final var algorithmName = Optional.ofNullable(header.getAlgorithm()) - .orElseThrow(() -> new SignatureException("token does not contain required alg header")); + public Key locate(final JwsHeader header) { final var keyId = header.getKeyId(); - return getValidatingKey(keyId, algorithmName); + if (keyId == null) { + LOG.debug("token has no kid header, will try to use default key for validating signature"); + return getValidatingKey(); + } else { + final var validatingKey = getValidatingKey(keyId); + if (validatingKey == null) { + LOG.debug("unknown validating key [id: {}], refreshing JWK set ...", keyId); + requestJwkSet(); + return null; + } else { + return validatingKey; + } + } } }); Optional.ofNullable(config.getAudience()).ifPresent(builder::requireAudience); - return builder.build().parseClaimsJws(token); + return builder.build().parseSignedClaims(token); } } diff --git a/service-base/src/main/java/org/eclipse/hono/service/auth/JwtSupport.java b/service-base/src/main/java/org/eclipse/hono/service/auth/JwtSupport.java index e29583e9fa..4d26159bbd 100644 --- a/service-base/src/main/java/org/eclipse/hono/service/auth/JwtSupport.java +++ b/service-base/src/main/java/org/eclipse/hono/service/auth/JwtSupport.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2016 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -20,7 +20,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import javax.crypto.SecretKey; @@ -29,7 +28,6 @@ import com.google.common.hash.Hashing; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.InvalidKeyException; import io.vertx.core.Vertx; @@ -44,8 +42,8 @@ abstract class JwtSupport { */ protected final Vertx vertx; - private final Map signingKeys = new HashMap<>(5); - private final Map validatingKeys = new HashMap<>(5); + private final Map signingKeys = new HashMap<>(5); + private final Map validatingKeys = new HashMap<>(5); /** * Creates a new helper for a vertx instance. @@ -81,9 +79,8 @@ private String createKeyId(final byte[] encodedKey) { protected final String addSecretKey(final SecretKey secretKey) { Objects.requireNonNull(secretKey); final var id = createKeyId(secretKey.getEncoded()); - final var keySpec = new KeySpec(secretKey, SignatureAlgorithm.forSigningKey(secretKey)); - this.signingKeys.put(id, keySpec); - this.validatingKeys.put(id, keySpec); + this.signingKeys.put(id, secretKey); + this.validatingKeys.put(id, secretKey); return id; } @@ -134,9 +131,8 @@ protected final void addPrivateKey(final String keyId, final PrivateKey privateK Objects.requireNonNull(keyId); Objects.requireNonNull(privateKey); Objects.requireNonNull(publicKey); - final var alg = SignatureAlgorithm.forSigningKey(privateKey); - this.signingKeys.put(keyId, new KeySpec(privateKey, alg)); - this.validatingKeys.put(keyId, new KeySpec(publicKey, alg)); + this.signingKeys.put(keyId, privateKey); + this.validatingKeys.put(keyId, publicKey); } /** @@ -152,8 +148,7 @@ protected final void setPublicKey(final String keyPath) { if (publicKey == null) { throw new IllegalArgumentException("cannot load public key: " + keyPath); } else { - final var keySpec = new KeySpec(publicKey); - setValidatingKeys(Map.of(createKeyId(publicKey.getEncoded()), keySpec)); + setValidatingKeys(Map.of(createKeyId(publicKey.getEncoded()), publicKey)); } } @@ -163,7 +158,7 @@ protected final void setPublicKey(final String keyPath) { * @param keys (key ID, key) tuples. * @throws NullPointerException if keys is {@code null}. */ - protected final void setValidatingKeys(final Map keys) { + protected final void setValidatingKeys(final Map keys) { Objects.requireNonNull(keys); this.validatingKeys.clear(); this.validatingKeys.putAll(keys); @@ -175,7 +170,7 @@ protected final void setValidatingKeys(final Map keys) { * @return The key. * @throws IllegalStateException if no key or more than one key is registered. */ - protected final KeySpec getValidatingKey() { + protected final Key getValidatingKey() { if (validatingKeys.size() != 1) { throw new IllegalStateException("more than one validating key is registered"); } @@ -189,7 +184,7 @@ protected final KeySpec getValidatingKey() { * @return The key or {@code null} if no key is registered for the given identifier. * @throws NullPointerException if key ID is {@code null}. */ - protected final KeySpec getValidatingKey(final String keyId) { + protected final Key getValidatingKey(final String keyId) { Objects.requireNonNull(keyId); return validatingKeys.get(keyId); } @@ -199,7 +194,7 @@ protected final KeySpec getValidatingKey(final String keyId) { * * @return An unmodifiable view on the set of (key ID, key) tuples. */ - protected final Set> getValidatingKeys() { + protected final Set> getValidatingKeys() { return Collections.unmodifiableSet(validatingKeys.entrySet()); } @@ -219,84 +214,8 @@ public final boolean hasValidatingKey() { * @return The key or {@code null} if no key is registered for the given identifier. * @throws NullPointerException if key ID is {@code null}. */ - protected final KeySpec getSigningKey(final String keyId) { + protected final Key getSigningKey(final String keyId) { Objects.requireNonNull(keyId); return signingKeys.get(keyId); } - - /** - * A container for a key and its meta data. - * - * The meta data includes a signature algorithm that is supposed to be used with the - * key when creating and/or validating digital signatures. - */ - static class KeySpec { - final SignatureAlgorithm algorithm; - final Key key; - - /** - * Creates a new spec for a key. - * - * @param key The key. - * @throws NullPointerException if key is {@code null}. - */ - KeySpec(final Key key) { - this(key, (SignatureAlgorithm) null); - } - - /** - * Creates a new spec for a key and a signature algorithm. - * - * @param key The key. - * @param algorithmName The JWA name of the signature algorithm to use or {@code null} if the key - * may be used with any algorithm that is compatible with the key's strength. - * @throws NullPointerException if key is {@code null}. - */ - KeySpec(final Key key, final String algorithmName) { - this(key, Optional.ofNullable(algorithmName) - .map(SignatureAlgorithm::forName) - .orElse(null)); - } - - /** - * Creates a new spec for a key and a signature algorithm. - * - * @param key The key. - * @param algorithm The signature algorithm to use or {@code null} if the key - * may be used with any algorithm that is compatible with the key's strength. - * @throws NullPointerException if key is {@code null}. - */ - KeySpec(final Key key, final SignatureAlgorithm algorithm) { - this.key = Objects.requireNonNull(key); - this.algorithm = algorithm; - } - - /** - * Checks if a given signature algorithm can be used with the key. - * - * @param algorithmName The JWA name of the algorithm. - * @return {@code true} if either - *
    - *
  • the key does not require any particular algorithm at all, i.e. the algorithm property is {@code null}, - * or
  • - *
  • the given algorithm name is equal to the name of the key's required algorithm.
  • - *
- */ - boolean supportsSignatureAlgorithm(final String algorithmName) { - - Objects.requireNonNull(algorithmName); - - if (algorithm == null) { - // check if the key can be used with the given algorithm - try { - SignatureAlgorithm.forName(algorithmName).assertValidVerificationKey(key); - return true; - } catch (final io.jsonwebtoken.security.SecurityException e) { - return false; - } - } else { - return algorithm.getValue().equals(algorithmName); - } - } - } } diff --git a/service-base/src/main/java/org/eclipse/hono/service/auth/delegating/AuthenticationServerClientConfigProperties.java b/service-base/src/main/java/org/eclipse/hono/service/auth/delegating/AuthenticationServerClientConfigProperties.java index e49aedbe4a..bd8a1bb97a 100644 --- a/service-base/src/main/java/org/eclipse/hono/service/auth/delegating/AuthenticationServerClientConfigProperties.java +++ b/service-base/src/main/java/org/eclipse/hono/service/auth/delegating/AuthenticationServerClientConfigProperties.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2016 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -37,7 +37,7 @@ public class AuthenticationServerClientConfigProperties extends ClientConfigProp private String jwksEndpointUri = AuthenticationServerClientOptions.DEFAULT_JWKS_ENDPOINT_URI; private boolean jwksEndpointTlsEnabled = false; private Duration jwksPollingInterval = Duration.ofMinutes(5); - private boolean jwksSignatureAlgorithmRequired = true; + private boolean jwksSignatureAlgorithmRequired = false; /** * Creates new properties using default values. @@ -195,7 +195,7 @@ public final void setJwksPollingInterval(final Duration interval) { * the signature algorithm to use with the key as described in * RFC 7517, Section 4.4. *

- * The default value of this property is {@code true}. + * The default value of this property is {@code false}. * * @return {@code true} if the property is required. */ diff --git a/service-base/src/main/java/org/eclipse/hono/service/auth/delegating/AuthenticationServerClientOptions.java b/service-base/src/main/java/org/eclipse/hono/service/auth/delegating/AuthenticationServerClientOptions.java index 0ae03118f1..574e080517 100644 --- a/service-base/src/main/java/org/eclipse/hono/service/auth/delegating/AuthenticationServerClientOptions.java +++ b/service-base/src/main/java/org/eclipse/hono/service/auth/delegating/AuthenticationServerClientOptions.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021, 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2021 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -98,6 +98,6 @@ public interface AuthenticationServerClientOptions { * * @return {@code true} if the property is required. */ - @WithDefault("true") + @WithDefault("false") boolean jwksSignatureAlgorithmRequired(); } diff --git a/service-base/src/main/resources/META-INF/native-image/org.eclipse.hono/hono-service-base/native-image.properties b/service-base/src/main/resources/META-INF/native-image/org.eclipse.hono/hono-service-base/native-image.properties index 2608cd4812..528c80ae8b 100644 --- a/service-base/src/main/resources/META-INF/native-image/org.eclipse.hono/hono-service-base/native-image.properties +++ b/service-base/src/main/resources/META-INF/native-image/org.eclipse.hono/hono-service-base/native-image.properties @@ -11,7 +11,7 @@ # use Base64 encoder/decoder that is compatible with vert.x 3 Args = -H:ResourceConfigurationResources=${.}/resources-config.json \ - --initialize-at-run-time=io.netty.internal.tcnative.SSL \ + --initialize-at-run-time=io.netty.internal.tcnative.SSL,io.jsonwebtoken \ -Dvertx.json.base64=legacy \ -Djdk.tls.ephemeralDHKeySize=matched \ -Djdk.tls.rejectClientInitiatedRenegotiation=true diff --git a/service-base/src/test/java/org/eclipse/hono/service/auth/AuthTokenFactoryTest.java b/service-base/src/test/java/org/eclipse/hono/service/auth/AuthTokenFactoryTest.java index 72e6dde60a..80ce52f41b 100644 --- a/service-base/src/test/java/org/eclipse/hono/service/auth/AuthTokenFactoryTest.java +++ b/service-base/src/test/java/org/eclipse/hono/service/auth/AuthTokenFactoryTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2016 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -32,8 +32,6 @@ import io.jsonwebtoken.IncorrectClaimException; import io.jsonwebtoken.Jws; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonArray; -import io.vertx.ext.auth.impl.jose.JWK; import io.vertx.junit5.VertxExtension; @@ -88,10 +86,10 @@ public void testCreateAndExpandToken() { final String token = factory.createToken("userA", authorities); final Jws parsedToken = validator.expand(token); - assertThat(parsedToken.getBody()).isNotNull(); - assertThat(parsedToken.getBody().getExpiration().toInstant()).isAtLeast(expirationMin); - assertThat(parsedToken.getBody().getExpiration().toInstant()).isAtMost(expirationMax); - assertThat(parsedToken.getBody().getSubject()).isEqualTo("userA"); + assertThat(parsedToken.getPayload()).isNotNull(); + assertThat(parsedToken.getPayload().getExpiration().toInstant()).isAtLeast(expirationMin); + assertThat(parsedToken.getPayload().getExpiration().toInstant()).isAtMost(expirationMax); + assertThat(parsedToken.getPayload().getSubject()).isEqualTo("userA"); } /** @@ -102,10 +100,10 @@ public void testCreateAndExpandToken() { public void testGetValidatingKeysReturnsProperKey() { final var jwks = factory.getValidatingJwkSet(); - final JsonArray keys = jwks.getJsonArray("keys"); + final var keys = jwks.getKeys(); assertThat(keys).hasSize(1); - final var jwk = new JWK(keys.getJsonObject(0)); - assertThat(jwk.publicKey()).isEqualTo(publicKey); + final var jwk = keys.iterator().next(); + assertThat(jwk.toKey()).isEqualTo(publicKey); } /** diff --git a/services/auth-base/pom.xml b/services/auth-base/pom.xml index a0fc4c5810..e801679d72 100644 --- a/services/auth-base/pom.xml +++ b/services/auth-base/pom.xml @@ -1,5 +1,5 @@