diff --git a/core/src/main/java/tech/ydb/core/auth/JwtUtils.java b/core/src/main/java/tech/ydb/core/auth/JwtUtils.java index 73f70edf4..42464e6fc 100644 --- a/core/src/main/java/tech/ydb/core/auth/JwtUtils.java +++ b/core/src/main/java/tech/ydb/core/auth/JwtUtils.java @@ -60,13 +60,14 @@ public static Instant extractExpireAt(String jwt, Instant defaultValue) { } try { - String payload = new String(Base64.getDecoder().decode(parts[1]), StandardCharsets.UTF_8); + String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); JwtClaims claims = GSON.fromJson(payload, JwtClaims.class); if (claims != null && claims.getExpiredAt() != null) { return Instant.ofEpochSecond(claims.getExpiredAt()); } } catch (IllegalArgumentException | JsonSyntaxException ex) { - logger.error("can't get expire from jwt {}", jwt, ex); + // the JWT itself is a credential and must never be logged + logger.error("Can't get expire from JWT", ex); } return defaultValue; diff --git a/core/src/test/java/tech/ydb/core/auth/JwtBuilder.java b/core/src/test/java/tech/ydb/core/auth/JwtBuilder.java index 5ea14fefd..fd5c8c45b 100644 --- a/core/src/test/java/tech/ydb/core/auth/JwtBuilder.java +++ b/core/src/test/java/tech/ydb/core/auth/JwtBuilder.java @@ -20,7 +20,8 @@ public static String create(Instant expiredAt, Instant issuedAt) { sb.append("\"iss\":\"").append("MOCK").append("\""); sb.append("}"); - String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes(StandardCharsets.UTF_8)); + String base64 = Base64.getUrlEncoder().withoutPadding() + .encodeToString(sb.toString().getBytes(StandardCharsets.UTF_8)); return PREFIX + base64; } diff --git a/core/src/test/java/tech/ydb/core/auth/JwtUtilsTest.java b/core/src/test/java/tech/ydb/core/auth/JwtUtilsTest.java index 3bd777504..f874d860c 100644 --- a/core/src/test/java/tech/ydb/core/auth/JwtUtilsTest.java +++ b/core/src/test/java/tech/ydb/core/auth/JwtUtilsTest.java @@ -24,4 +24,21 @@ public void parseTest() { Assert.assertEquals(Instant.ofEpochSecond(1726544488), JwtUtils.extractExpireAt(jwt2, Instant.EPOCH)); Assert.assertEquals(Instant.ofEpochSecond(1726544488), JwtUtils.extractExpireAt(jwt3, Instant.EPOCH)); } + + @Test + public void parseBase64UrlPayloadTest() { + // JWT payloads use the base64url alphabet, so they may contain '-' and '_' instead of '+' and '/' + // { "alg": "HS256", "typ": "JWT" }.{ "sub": "a?b>c", "aud": "x?y?z", "exp": 1726544488 } + String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + + ".eyJzdWIiOiJhP2I-YyIsImF1ZCI6Ing_eT96IiwiZXhwIjoxNzI2NTQ0NDg4fQ"; + + Assert.assertEquals(Instant.ofEpochSecond(1726544488), JwtUtils.extractExpireAt(jwt, Instant.EPOCH)); + } + + @Test + public void unparsablePayloadReturnsDefaultTest() { + String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.!not-a-base64-payload!"; + + Assert.assertEquals(Instant.EPOCH, JwtUtils.extractExpireAt(jwt, Instant.EPOCH)); + } }