From ef60ff2bc83aace5a84b9652b9a74fac14a0bda2 Mon Sep 17 00:00:00 2001 From: thjarvin Date: Fri, 6 Jun 2025 15:28:50 +0300 Subject: [PATCH 1/9] Authenticate with Microsoft Entra ID using Token Cache --- pom.xml | 8 +- .../hsl/common/pulsar/PulsarApplication.java | 60 +++++++++- .../java/fi/hsl/common/redis/RedisUtils.java | 110 ++++++++++++++++++ 3 files changed, 174 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index a2df6ace..3e5bf7f1 100644 --- a/pom.xml +++ b/pom.xml @@ -78,10 +78,16 @@ ${pulsar.version} + + com.azure + azure-identity + 1.11.2 + + redis.clients jedis - 4.4.3 + 5.1.0 jar compile diff --git a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java index 8213c515..bb9810af 100644 --- a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java +++ b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java @@ -1,7 +1,12 @@ package fi.hsl.common.pulsar; +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenRequestContext; +import com.azure.identity.DefaultAzureCredential; +import com.azure.identity.DefaultAzureCredentialBuilder; import com.typesafe.config.Config; import fi.hsl.common.health.HealthServer; +import fi.hsl.common.redis.RedisUtils; import org.apache.pulsar.client.admin.PulsarAdmin; import org.apache.pulsar.client.api.*; import org.jetbrains.annotations.NotNull; @@ -9,6 +14,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisException; import java.util.Arrays; import java.util.HashMap; @@ -19,6 +25,9 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import static fi.hsl.common.redis.RedisUtils.createJedisClient; +import static fi.hsl.common.redis.RedisUtils.extractUsernameFromToken; + public class PulsarApplication implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(PulsarApplication.class); @@ -152,9 +161,54 @@ public PulsarApplicationContext initialize(@NotNull Config config) throws Except @NotNull protected Jedis createRedisClient(@NotNull String redisHost, int port, int connTimeOutSecs) { log.info("Connecting to Redis at " + redisHost + ":" + port + " with connection timeout of (s): "+ connTimeOutSecs); - int timeOutMs = connTimeOutSecs * 1000; - Jedis jedis = new Jedis(redisHost, port, timeOutMs); - jedis.connect(); + + //Construct a Token Credential from Identity library, e.g. DefaultAzureCredential / ClientSecretCredential / Client CertificateCredential / ManagedIdentityCredential etc. + DefaultAzureCredential defaultAzureCredential = new DefaultAzureCredentialBuilder().build(); + + // Fetch a Microsoft Entra token to be used for authentication. This token will be used as the password. + TokenRequestContext trc = new TokenRequestContext().addScopes("https://redis.azure.com/.default"); + RedisUtils.TokenRefreshCache tokenRefreshCache = new RedisUtils.TokenRefreshCache(defaultAzureCredential, trc); + AccessToken accessToken = tokenRefreshCache.getAccessToken(); + + // SSL connection is required. + boolean useSsl = true; + String username = extractUsernameFromToken(accessToken.getToken()); + + // Create Jedis client and connect to the Azure Cache for Redis over the TLS/SSL port using the access token as password. + // Note: Cache Host Name, Port, Microsoft Entra access token and SSL connections are required below. + Jedis jedis = createJedisClient(redisHost, port, username, accessToken, useSsl); + + // Configure the jedis instance for proactive authentication before token expires. + tokenRefreshCache.setJedisInstanceToAuthenticate(jedis); + + int maxTries = 3; + int i = 0; + + while (i < maxTries) { + try { + // Set a value against your key in the Redis cache. + jedis.set("Az:key", "testValue"); + System.out.println(jedis.get("Az:key")); + break; + } catch (JedisException e) { + // Handle The Exception as required in your application. + e.printStackTrace(); + + // For Exceptions containing Invalid Username Password / Permissions not granted error messages, look at troubleshooting section at the end of document. + + // Check if the client is broken, if it is then close and recreate it to create a new healthy connection. + if (jedis.isBroken()) { + jedis.close(); + accessToken = tokenRefreshCache.getAccessToken(); + jedis = createJedisClient(redisHost, port, username, accessToken, useSsl); + + // Configure the jedis instance for proactive authentication before token expires. + tokenRefreshCache.setJedisInstanceToAuthenticate(jedis); + } + } + i++; + } + log.info("Redis connected: " + jedis.isConnected()); return jedis; } diff --git a/src/main/java/fi/hsl/common/redis/RedisUtils.java b/src/main/java/fi/hsl/common/redis/RedisUtils.java index 2a9571af..9870df96 100644 --- a/src/main/java/fi/hsl/common/redis/RedisUtils.java +++ b/src/main/java/fi/hsl/common/redis/RedisUtils.java @@ -1,5 +1,11 @@ package fi.hsl.common.redis; +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; +import com.azure.core.util.CoreUtils; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import fi.hsl.common.pulsar.PulsarApplicationContext; import fi.hsl.common.transitdata.TransitdataProperties; import org.jetbrains.annotations.NotNull; @@ -10,9 +16,12 @@ import redis.clients.jedis.params.ScanParams; import redis.clients.jedis.resps.ScanResult; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.concurrent.ThreadLocalRandom; public class RedisUtils { private static final Logger log = LoggerFactory.getLogger(RedisUtils.class); @@ -227,4 +236,105 @@ public boolean checkResponse(@Nullable final String response) { public boolean checkResponse(@Nullable final Long response) { return response != null && response == 1; } + + // Azure Cache for Redis helper code + public static Jedis createJedisClient(String cacheHostname, int port, String username, AccessToken accessToken, boolean useSsl) { + return new Jedis(cacheHostname, port, DefaultJedisClientConfig.builder() + .password(accessToken.getToken()) + .user(username) + .ssl(useSsl) + .build()); + } + + public static String extractUsernameFromToken(String token) { + String[] parts = token.split("\\."); + String base64 = parts[1]; + + switch (base64.length() % 4) { + case 2: + base64 += "=="; + break; + case 3: + base64 += "="; + break; + } + + byte[] jsonBytes = Base64.getDecoder().decode(base64); + String json = new String(jsonBytes, StandardCharsets.UTF_8); + JsonObject jwt = JsonParser.parseString(json).getAsJsonObject(); + + return jwt.get("oid").getAsString(); + } + + /** + * The token cache to store and proactively refresh the access token. + */ + public static class TokenRefreshCache { + private final TokenCredential tokenCredential; + private final TokenRequestContext tokenRequestContext; + private final Timer timer; + private volatile AccessToken accessToken; + private final Duration maxRefreshOffset = Duration.ofMinutes(5); + private final Duration baseRefreshOffset = Duration.ofMinutes(2); + private Jedis jedisInstanceToAuthenticate; + private String username; + + /** + * Creates an instance of TokenRefreshCache + * @param tokenCredential the token credential to be used for authentication. + * @param tokenRequestContext the token request context to be used for authentication. + */ + public TokenRefreshCache(TokenCredential tokenCredential, TokenRequestContext tokenRequestContext) { + this.tokenCredential = tokenCredential; + this.tokenRequestContext = tokenRequestContext; + this.timer = new Timer(); + } + + /** + * Gets the cached access token. + * @return the AccessToken + */ + public AccessToken getAccessToken() { + if (accessToken != null) { + return accessToken; + } else { + TokenRefreshTask tokenRefreshTask = new TokenRefreshTask(); + accessToken = tokenCredential.getToken(tokenRequestContext).block(); + timer.schedule(tokenRefreshTask, getTokenRefreshDelay()); + return accessToken; + } + } + + private class TokenRefreshTask extends TimerTask { + // Add your task here + public void run() { + accessToken = tokenCredential.getToken(tokenRequestContext).block(); + username = extractUsernameFromToken(accessToken.getToken()); + System.out.println("Refreshed Token with Expiry: " + accessToken.getExpiresAt().toEpochSecond()); + + if (jedisInstanceToAuthenticate != null && !CoreUtils.isNullOrEmpty(username)) { + jedisInstanceToAuthenticate.auth(username, accessToken.getToken()); + System.out.println("Refreshed Jedis Connection with fresh access token, token expires at : " + + accessToken.getExpiresAt().toEpochSecond()); + } + timer.schedule(new TokenRefreshTask(), getTokenRefreshDelay()); + } + } + + private long getTokenRefreshDelay() { + return ((accessToken.getExpiresAt() + .minusSeconds(ThreadLocalRandom.current().nextLong(baseRefreshOffset.getSeconds(), maxRefreshOffset.getSeconds())) + .toEpochSecond() - OffsetDateTime.now().toEpochSecond()) * 1000); + } + + /** + * Sets the Jedis to proactively authenticate before token expiry. + * @param jedisInstanceToAuthenticate the instance to authenticate + * @return the updated instance + */ + public TokenRefreshCache setJedisInstanceToAuthenticate(Jedis jedisInstanceToAuthenticate) { + this.jedisInstanceToAuthenticate = jedisInstanceToAuthenticate; + return this; + } + } } From 09710d080bfb7b66a0267db51f2868fc122ff5b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20J=C3=A4rvinen?= Date: Fri, 6 Jun 2025 15:34:36 +0300 Subject: [PATCH 2/9] Update version to 2.0.3-RC7 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3e5bf7f1..db0c04b0 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 fi.hsl transitdata-common - 2.0.3-RC6 + 2.0.3-RC7 jar Common utilities for Transitdata projects From 5f480b04939e75d04aed1eeb4ff56a19ad34bf8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20J=C3=A4rvinen?= Date: Fri, 6 Jun 2025 17:15:25 +0300 Subject: [PATCH 3/9] Cleaning up --- pom.xml | 2 +- .../hsl/common/pulsar/PulsarApplication.java | 11 +-- .../java/fi/hsl/common/redis/RedisUtils.java | 75 ------------------- 3 files changed, 4 insertions(+), 84 deletions(-) diff --git a/pom.xml b/pom.xml index db0c04b0..18a55c9b 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 fi.hsl transitdata-common - 2.0.3-RC7 + 2.0.3-RC8 jar Common utilities for Transitdata projects diff --git a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java index bb9810af..c6e44337 100644 --- a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java +++ b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java @@ -93,14 +93,9 @@ public PulsarApplicationContext initialize(@NotNull Config config) throws Except } if (config.getBoolean("redis.enabled")) { - int connTimeOutSecs = 2; - if (config.hasPath("redis.connTimeOutSecs")) { - connTimeOutSecs = config.getInt("redis.connTimeOutSecs"); - } jedis = createRedisClient( config.getString("redis.host"), - config.getInt("redis.port"), - connTimeOutSecs); + config.getInt("redis.port")); } if (config.getBoolean("health.enabled")) { @@ -159,8 +154,8 @@ public PulsarApplicationContext initialize(@NotNull Config config) throws Except } @NotNull - protected Jedis createRedisClient(@NotNull String redisHost, int port, int connTimeOutSecs) { - log.info("Connecting to Redis at " + redisHost + ":" + port + " with connection timeout of (s): "+ connTimeOutSecs); + protected Jedis createRedisClient(@NotNull String redisHost, int port) { + log.info("Connecting to Redis at " + redisHost + ":" + port); //Construct a Token Credential from Identity library, e.g. DefaultAzureCredential / ClientSecretCredential / Client CertificateCredential / ManagedIdentityCredential etc. DefaultAzureCredential defaultAzureCredential = new DefaultAzureCredentialBuilder().build(); diff --git a/src/main/java/fi/hsl/common/redis/RedisUtils.java b/src/main/java/fi/hsl/common/redis/RedisUtils.java index 9870df96..4a64a840 100644 --- a/src/main/java/fi/hsl/common/redis/RedisUtils.java +++ b/src/main/java/fi/hsl/common/redis/RedisUtils.java @@ -7,9 +7,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import fi.hsl.common.pulsar.PulsarApplicationContext; -import fi.hsl.common.transitdata.TransitdataProperties; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.*; @@ -19,7 +17,6 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ThreadLocalRandom; @@ -164,78 +161,6 @@ public List getKeys(@NotNull final String prefix, @NotNull final String return new ArrayList<>(keys); } } - - /** - * Fetches hash values for keys - * @param keys - * @return HashMap of keys and their hash values if they exist - */ - @NotNull - public Map<@NotNull String, Optional>> getValuesByKeys(@NotNull final List<@NotNull String> keys) { - synchronized (jedis) { - final Transaction transaction = jedis.multi(); - final Map>> responses = new HashMap<>(); - keys.forEach(key -> responses.put(key, transaction.hgetAll(key))); - transaction.exec(); - - final Map>> values = new HashMap<>(responses.size()); - responses.forEach((k, v) -> { - final Map value = v.get(); - if (value == null || value.isEmpty()) { - values.put(k, Optional.empty()); - } else { - values.put(k, Optional.of(value)); - } - }); - - return values; - } - } - - /** - * Fetches string values for keys - * @param keys - * @return HashMap of keys and their values if they exist - */ - @NotNull - public Map<@NotNull String, Optional> getValueBykeys(@NotNull final List<@NotNull String> keys) { - synchronized (jedis) { - final Transaction transaction = jedis.multi(); - final Map> responses = new HashMap<>(); - keys.forEach(key -> responses.put(key, transaction.get(key))); - transaction.exec(); - - final Map> values = new HashMap<>(responses.size()); - responses.forEach((k, v) -> { - final String value = v.get(); - if (value == null || value.isEmpty()) { - values.put(k, Optional.empty()); - } else { - values.put(k, Optional.of(value)); - } - }); - - return values; - } - } - - @NotNull - public String updateTimestamp() { - synchronized (jedis) { - final OffsetDateTime now = OffsetDateTime.now(); - final String ts = DateTimeFormatter.ISO_INSTANT.format(now); - log.info("Updating Redis timestamp to {}", ts); - return jedis.set(TransitdataProperties.KEY_LAST_CACHE_UPDATE_TIMESTAMP, ts); - } - } - - public boolean checkResponse(@Nullable final String response) { - return response != null && response.trim().equalsIgnoreCase("OK"); - } - - public boolean checkResponse(@Nullable final Long response) { - return response != null && response == 1; - } // Azure Cache for Redis helper code public static Jedis createJedisClient(String cacheHostname, int port, String username, AccessToken accessToken, boolean useSsl) { From 0f89d8476c453bcc90b81316db09437038ae72b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20J=C3=A4rvinen?= Date: Fri, 6 Jun 2025 18:14:21 +0300 Subject: [PATCH 4/9] Revert "Cleaning up" This reverts commit 5f480b04939e75d04aed1eeb4ff56a19ad34bf8d. --- pom.xml | 2 +- .../hsl/common/pulsar/PulsarApplication.java | 11 ++- .../java/fi/hsl/common/redis/RedisUtils.java | 75 +++++++++++++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 18a55c9b..db0c04b0 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 fi.hsl transitdata-common - 2.0.3-RC8 + 2.0.3-RC7 jar Common utilities for Transitdata projects diff --git a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java index c6e44337..bb9810af 100644 --- a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java +++ b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java @@ -93,9 +93,14 @@ public PulsarApplicationContext initialize(@NotNull Config config) throws Except } if (config.getBoolean("redis.enabled")) { + int connTimeOutSecs = 2; + if (config.hasPath("redis.connTimeOutSecs")) { + connTimeOutSecs = config.getInt("redis.connTimeOutSecs"); + } jedis = createRedisClient( config.getString("redis.host"), - config.getInt("redis.port")); + config.getInt("redis.port"), + connTimeOutSecs); } if (config.getBoolean("health.enabled")) { @@ -154,8 +159,8 @@ public PulsarApplicationContext initialize(@NotNull Config config) throws Except } @NotNull - protected Jedis createRedisClient(@NotNull String redisHost, int port) { - log.info("Connecting to Redis at " + redisHost + ":" + port); + protected Jedis createRedisClient(@NotNull String redisHost, int port, int connTimeOutSecs) { + log.info("Connecting to Redis at " + redisHost + ":" + port + " with connection timeout of (s): "+ connTimeOutSecs); //Construct a Token Credential from Identity library, e.g. DefaultAzureCredential / ClientSecretCredential / Client CertificateCredential / ManagedIdentityCredential etc. DefaultAzureCredential defaultAzureCredential = new DefaultAzureCredentialBuilder().build(); diff --git a/src/main/java/fi/hsl/common/redis/RedisUtils.java b/src/main/java/fi/hsl/common/redis/RedisUtils.java index 4a64a840..9870df96 100644 --- a/src/main/java/fi/hsl/common/redis/RedisUtils.java +++ b/src/main/java/fi/hsl/common/redis/RedisUtils.java @@ -7,7 +7,9 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import fi.hsl.common.pulsar.PulsarApplicationContext; +import fi.hsl.common.transitdata.TransitdataProperties; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.*; @@ -17,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ThreadLocalRandom; @@ -161,6 +164,78 @@ public List getKeys(@NotNull final String prefix, @NotNull final String return new ArrayList<>(keys); } } + + /** + * Fetches hash values for keys + * @param keys + * @return HashMap of keys and their hash values if they exist + */ + @NotNull + public Map<@NotNull String, Optional>> getValuesByKeys(@NotNull final List<@NotNull String> keys) { + synchronized (jedis) { + final Transaction transaction = jedis.multi(); + final Map>> responses = new HashMap<>(); + keys.forEach(key -> responses.put(key, transaction.hgetAll(key))); + transaction.exec(); + + final Map>> values = new HashMap<>(responses.size()); + responses.forEach((k, v) -> { + final Map value = v.get(); + if (value == null || value.isEmpty()) { + values.put(k, Optional.empty()); + } else { + values.put(k, Optional.of(value)); + } + }); + + return values; + } + } + + /** + * Fetches string values for keys + * @param keys + * @return HashMap of keys and their values if they exist + */ + @NotNull + public Map<@NotNull String, Optional> getValueBykeys(@NotNull final List<@NotNull String> keys) { + synchronized (jedis) { + final Transaction transaction = jedis.multi(); + final Map> responses = new HashMap<>(); + keys.forEach(key -> responses.put(key, transaction.get(key))); + transaction.exec(); + + final Map> values = new HashMap<>(responses.size()); + responses.forEach((k, v) -> { + final String value = v.get(); + if (value == null || value.isEmpty()) { + values.put(k, Optional.empty()); + } else { + values.put(k, Optional.of(value)); + } + }); + + return values; + } + } + + @NotNull + public String updateTimestamp() { + synchronized (jedis) { + final OffsetDateTime now = OffsetDateTime.now(); + final String ts = DateTimeFormatter.ISO_INSTANT.format(now); + log.info("Updating Redis timestamp to {}", ts); + return jedis.set(TransitdataProperties.KEY_LAST_CACHE_UPDATE_TIMESTAMP, ts); + } + } + + public boolean checkResponse(@Nullable final String response) { + return response != null && response.trim().equalsIgnoreCase("OK"); + } + + public boolean checkResponse(@Nullable final Long response) { + return response != null && response == 1; + } // Azure Cache for Redis helper code public static Jedis createJedisClient(String cacheHostname, int port, String username, AccessToken accessToken, boolean useSsl) { From b6390fbdfab51bc1242856873e7aa6bd5e290f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20J=C3=A4rvinen?= Date: Fri, 6 Jun 2025 18:23:00 +0300 Subject: [PATCH 5/9] Little cleaning --- pom.xml | 2 +- .../java/fi/hsl/common/pulsar/PulsarApplication.java | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index db0c04b0..a703e983 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 fi.hsl transitdata-common - 2.0.3-RC7 + 2.0.3-RC9 jar Common utilities for Transitdata projects diff --git a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java index bb9810af..c6e44337 100644 --- a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java +++ b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java @@ -93,14 +93,9 @@ public PulsarApplicationContext initialize(@NotNull Config config) throws Except } if (config.getBoolean("redis.enabled")) { - int connTimeOutSecs = 2; - if (config.hasPath("redis.connTimeOutSecs")) { - connTimeOutSecs = config.getInt("redis.connTimeOutSecs"); - } jedis = createRedisClient( config.getString("redis.host"), - config.getInt("redis.port"), - connTimeOutSecs); + config.getInt("redis.port")); } if (config.getBoolean("health.enabled")) { @@ -159,8 +154,8 @@ public PulsarApplicationContext initialize(@NotNull Config config) throws Except } @NotNull - protected Jedis createRedisClient(@NotNull String redisHost, int port, int connTimeOutSecs) { - log.info("Connecting to Redis at " + redisHost + ":" + port + " with connection timeout of (s): "+ connTimeOutSecs); + protected Jedis createRedisClient(@NotNull String redisHost, int port) { + log.info("Connecting to Redis at " + redisHost + ":" + port); //Construct a Token Credential from Identity library, e.g. DefaultAzureCredential / ClientSecretCredential / Client CertificateCredential / ManagedIdentityCredential etc. DefaultAzureCredential defaultAzureCredential = new DefaultAzureCredentialBuilder().build(); From e7245d12fca58daf058d4d27eb8a1477a4cb4834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20J=C3=A4rvinen?= Date: Wed, 11 Jun 2025 14:25:53 +0300 Subject: [PATCH 6/9] Update src/main/java/fi/hsl/common/pulsar/PulsarApplication.java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Janne Löfhjelm --- src/main/java/fi/hsl/common/pulsar/PulsarApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java index c6e44337..ecbed2c1 100644 --- a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java +++ b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java @@ -155,7 +155,7 @@ public PulsarApplicationContext initialize(@NotNull Config config) throws Except @NotNull protected Jedis createRedisClient(@NotNull String redisHost, int port) { - log.info("Connecting to Redis at " + redisHost + ":" + port); + log.info("Connecting to Redis at {}:{}", redisHost, port); //Construct a Token Credential from Identity library, e.g. DefaultAzureCredential / ClientSecretCredential / Client CertificateCredential / ManagedIdentityCredential etc. DefaultAzureCredential defaultAzureCredential = new DefaultAzureCredentialBuilder().build(); From ec434244cd776c707be71d2cd839d9a75c92ab7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20J=C3=A4rvinen?= Date: Wed, 11 Jun 2025 14:47:42 +0300 Subject: [PATCH 7/9] More cleaning --- pom.xml | 2 +- .../hsl/common/pulsar/PulsarApplication.java | 31 +------------------ .../java/fi/hsl/common/redis/RedisUtils.java | 4 +-- 3 files changed, 4 insertions(+), 33 deletions(-) diff --git a/pom.xml b/pom.xml index a703e983..5bb16a25 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 fi.hsl transitdata-common - 2.0.3-RC9 + 2.0.3-RC10 jar Common utilities for Transitdata projects diff --git a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java index c6e44337..089ba3fd 100644 --- a/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java +++ b/src/main/java/fi/hsl/common/pulsar/PulsarApplication.java @@ -14,7 +14,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.Jedis; -import redis.clients.jedis.exceptions.JedisException; import java.util.Arrays; import java.util.HashMap; @@ -171,39 +170,11 @@ protected Jedis createRedisClient(@NotNull String redisHost, int port) { // Create Jedis client and connect to the Azure Cache for Redis over the TLS/SSL port using the access token as password. // Note: Cache Host Name, Port, Microsoft Entra access token and SSL connections are required below. - Jedis jedis = createJedisClient(redisHost, port, username, accessToken, useSsl); + jedis = createJedisClient(redisHost, port, username, accessToken, useSsl); // Configure the jedis instance for proactive authentication before token expires. tokenRefreshCache.setJedisInstanceToAuthenticate(jedis); - int maxTries = 3; - int i = 0; - - while (i < maxTries) { - try { - // Set a value against your key in the Redis cache. - jedis.set("Az:key", "testValue"); - System.out.println(jedis.get("Az:key")); - break; - } catch (JedisException e) { - // Handle The Exception as required in your application. - e.printStackTrace(); - - // For Exceptions containing Invalid Username Password / Permissions not granted error messages, look at troubleshooting section at the end of document. - - // Check if the client is broken, if it is then close and recreate it to create a new healthy connection. - if (jedis.isBroken()) { - jedis.close(); - accessToken = tokenRefreshCache.getAccessToken(); - jedis = createJedisClient(redisHost, port, username, accessToken, useSsl); - - // Configure the jedis instance for proactive authentication before token expires. - tokenRefreshCache.setJedisInstanceToAuthenticate(jedis); - } - } - i++; - } - log.info("Redis connected: " + jedis.isConnected()); return jedis; } diff --git a/src/main/java/fi/hsl/common/redis/RedisUtils.java b/src/main/java/fi/hsl/common/redis/RedisUtils.java index 9870df96..d083ed36 100644 --- a/src/main/java/fi/hsl/common/redis/RedisUtils.java +++ b/src/main/java/fi/hsl/common/redis/RedisUtils.java @@ -310,11 +310,11 @@ private class TokenRefreshTask extends TimerTask { public void run() { accessToken = tokenCredential.getToken(tokenRequestContext).block(); username = extractUsernameFromToken(accessToken.getToken()); - System.out.println("Refreshed Token with Expiry: " + accessToken.getExpiresAt().toEpochSecond()); + log.info("Refreshed Token with Expiry: " + accessToken.getExpiresAt().toEpochSecond()); if (jedisInstanceToAuthenticate != null && !CoreUtils.isNullOrEmpty(username)) { jedisInstanceToAuthenticate.auth(username, accessToken.getToken()); - System.out.println("Refreshed Jedis Connection with fresh access token, token expires at : " + log.info("Refreshed Jedis Connection with fresh access token, token expires at : " + accessToken.getExpiresAt().toEpochSecond()); } timer.schedule(new TokenRefreshTask(), getTokenRefreshDelay()); From 72776e4beff577c36d7f4360c878769df06fa75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janne=20L=C3=B6fhjelm?= Date: Wed, 11 Jun 2025 15:23:08 +0300 Subject: [PATCH 8/9] Add test extractUsernameFromTokenBase64PaddingWorks() --- .../fi/hsl/common/redis/RedisUtilsTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/java/fi/hsl/common/redis/RedisUtilsTest.java b/src/test/java/fi/hsl/common/redis/RedisUtilsTest.java index f7ec2aa4..00f9535a 100644 --- a/src/test/java/fi/hsl/common/redis/RedisUtilsTest.java +++ b/src/test/java/fi/hsl/common/redis/RedisUtilsTest.java @@ -8,6 +8,8 @@ import org.testcontainers.utility.DockerImageName; import redis.clients.jedis.Jedis; +import java.util.Base64; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -51,4 +53,20 @@ public void testSetGetExpiringValue() throws InterruptedException { assertFalse(redisUtils.getValue("test").isPresent()); } + + @Test + public void extractUsernameFromTokenBase64PaddingWorks() { + // Payload with length not a multiple of 4 (e.g., 2 or 3 mod 4) + String header = Base64.getUrlEncoder().withoutPadding().encodeToString("{\"alg\":\"none\"}".getBytes()); + + // 2 mod 4 length + String payload2 = Base64.getUrlEncoder().withoutPadding().encodeToString("{\"oid\":\"ab\"}".getBytes()); + String token2 = header + "." + payload2 + ".sig"; + assertEquals("ab", RedisUtils.extractUsernameFromToken(token2)); + + // 3 mod 4 length + String payload3 = Base64.getUrlEncoder().withoutPadding().encodeToString("{\"oid\":\"abc\"}".getBytes()); + String token3 = header + "." + payload3 + ".sig"; + assertEquals("abc", RedisUtils.extractUsernameFromToken(token3)); + } } From aa037e1901cd67b31af382faf04ee89a11330c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janne=20L=C3=B6fhjelm?= Date: Wed, 11 Jun 2025 15:41:48 +0300 Subject: [PATCH 9/9] Change base64 padding logics --- .../java/fi/hsl/common/redis/RedisUtils.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/fi/hsl/common/redis/RedisUtils.java b/src/main/java/fi/hsl/common/redis/RedisUtils.java index d083ed36..f52a6c5a 100644 --- a/src/main/java/fi/hsl/common/redis/RedisUtils.java +++ b/src/main/java/fi/hsl/common/redis/RedisUtils.java @@ -249,23 +249,24 @@ public static Jedis createJedisClient(String cacheHostname, int port, String use public static String extractUsernameFromToken(String token) { String[] parts = token.split("\\."); String base64 = parts[1]; - - switch (base64.length() % 4) { - case 2: - base64 += "=="; - break; - case 3: - base64 += "="; - break; - } - + + base64 = addPaddingToBase64String(base64); + byte[] jsonBytes = Base64.getDecoder().decode(base64); String json = new String(jsonBytes, StandardCharsets.UTF_8); JsonObject jwt = JsonParser.parseString(json).getAsJsonObject(); return jwt.get("oid").getAsString(); } - + + private static String addPaddingToBase64String(String input) { + if (input != null && !input.isEmpty()) { + int paddingLength = (4 - input.length() % 4) % 4; + input += "=".repeat(paddingLength); + } + return input; + } + /** * The token cache to store and proactively refresh the access token. */