From a6fb5a2530e65f5ea969569718e764b14e6e09f0 Mon Sep 17 00:00:00 2001 From: Ilya Lisov Date: Mon, 22 Jan 2024 11:03:22 +0300 Subject: [PATCH] #18 Implement PersistentTokenService, token invalidation --- .../jwt/service/PersistentTokenService.java | 31 +++++++++ .../service/PersistentTokenServiceImpl.java | 16 ++++- .../jwt/storage/RedisTokenStorageImpl.java | 35 ++++++++++ .../ilyalisov/jwt/storage/TokenStorage.java | 20 ++++++ .../jwt/storage/TokenStorageImpl.java | 25 +++++++ .../jwt/fake/FakeTokenStorageImpl.java | 25 +++++++ .../PersistentTokenServiceImplTests.java | 68 ++++++++++++++++++- .../storage/RedisTokenStorageImplTests.java | 48 +++++++++++++ .../jwt/storage/TokenStorageImplTests.java | 49 +++++++++++++ 9 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 src/main/java/io/github/ilyalisov/jwt/service/PersistentTokenService.java diff --git a/src/main/java/io/github/ilyalisov/jwt/service/PersistentTokenService.java b/src/main/java/io/github/ilyalisov/jwt/service/PersistentTokenService.java new file mode 100644 index 0000000..70148f3 --- /dev/null +++ b/src/main/java/io/github/ilyalisov/jwt/service/PersistentTokenService.java @@ -0,0 +1,31 @@ +package io.github.ilyalisov.jwt.service; + +import io.github.ilyalisov.jwt.config.TokenParameters; + +/** + * Interface if PersistentTokenService. + */ +public interface PersistentTokenService extends TokenService { + + /** + * Removes JWT token from storage. Method removes all entries + * of the same token. + * + * @param token JWT token to be removed + * @return true - if JWT token was removed, false - otherwise + */ + boolean invalidate( + String token + ); + + /** + * Removes JWT token from storage. + * + * @param params params of JWT token + * @return true - if JWT token was removed, false - otherwise + */ + boolean invalidate( + TokenParameters params + ); + +} diff --git a/src/main/java/io/github/ilyalisov/jwt/service/PersistentTokenServiceImpl.java b/src/main/java/io/github/ilyalisov/jwt/service/PersistentTokenServiceImpl.java index 6dc1002..b99ff4e 100644 --- a/src/main/java/io/github/ilyalisov/jwt/service/PersistentTokenServiceImpl.java +++ b/src/main/java/io/github/ilyalisov/jwt/service/PersistentTokenServiceImpl.java @@ -17,7 +17,7 @@ /** * Implementation of TokenService with JWT token storage. */ -public class PersistentTokenServiceImpl implements TokenService { +public class PersistentTokenServiceImpl implements PersistentTokenService { /** * Secret key for verifying JWT token. @@ -171,4 +171,18 @@ public Map claims( return new HashMap<>(claims.getPayload()); } + @Override + public boolean invalidate( + final String token + ) { + return tokenStorage.remove(token); + } + + @Override + public boolean invalidate( + final TokenParameters params + ) { + return tokenStorage.remove(params); + } + } diff --git a/src/main/java/io/github/ilyalisov/jwt/storage/RedisTokenStorageImpl.java b/src/main/java/io/github/ilyalisov/jwt/storage/RedisTokenStorageImpl.java index 48d419b..c30d3d9 100644 --- a/src/main/java/io/github/ilyalisov/jwt/storage/RedisTokenStorageImpl.java +++ b/src/main/java/io/github/ilyalisov/jwt/storage/RedisTokenStorageImpl.java @@ -139,4 +139,39 @@ public String get( } } + @Override + public boolean remove( + final String token + ) { + try (Jedis jedis = jedisPool.getResource()) { + String script = """ + local keys = redis.call('keys', ARGV[1]) + for _, key in ipairs(keys) do + redis.call('del', key) + end + return #keys > 0 + """; + Long result = (Long) jedis.eval( + script, + 0, + "*", + token + ); + return result != null && result > 0; + } + } + + @Override + public boolean remove( + final TokenParameters params + ) { + try (Jedis jedis = jedisPool.getResource()) { + String tokenKey = redisSchema.subjectTokenKey( + params.getSubject(), + params.getType() + ); + return jedis.del(tokenKey) > 0; + } + } + } diff --git a/src/main/java/io/github/ilyalisov/jwt/storage/TokenStorage.java b/src/main/java/io/github/ilyalisov/jwt/storage/TokenStorage.java index 84e2688..dc413c7 100644 --- a/src/main/java/io/github/ilyalisov/jwt/storage/TokenStorage.java +++ b/src/main/java/io/github/ilyalisov/jwt/storage/TokenStorage.java @@ -40,4 +40,24 @@ String get( TokenParameters params ); + /** + * Removes JWT token from storage. + * + * @param token JWT token to be removed + * @return true - if JWT token was removed, false - otherwise + */ + boolean remove( + String token + ); + + /** + * Removes JWT token from storage. + * + * @param params params of JWT token + * @return true - if JWT token was removed, false - otherwise + */ + boolean remove( + TokenParameters params + ); + } diff --git a/src/main/java/io/github/ilyalisov/jwt/storage/TokenStorageImpl.java b/src/main/java/io/github/ilyalisov/jwt/storage/TokenStorageImpl.java index 31e0c41..1f23bd1 100644 --- a/src/main/java/io/github/ilyalisov/jwt/storage/TokenStorageImpl.java +++ b/src/main/java/io/github/ilyalisov/jwt/storage/TokenStorageImpl.java @@ -66,4 +66,29 @@ public String get( return tokens.get(tokenKey); } + @Override + public boolean remove( + final String token + ) { + boolean deleted = false; + for (Map.Entry entry : tokens.entrySet()) { + if (entry.getValue().equals(token)) { + tokens.remove(entry.getKey()); + deleted = true; + } + } + return deleted; + } + + @Override + public boolean remove( + final TokenParameters params + ) { + String tokenKey = subjectTokenKey( + params.getSubject(), + params.getType() + ); + return tokens.remove(tokenKey) != null; + } + } diff --git a/src/test/java/io/github/ilyalisov/jwt/fake/FakeTokenStorageImpl.java b/src/test/java/io/github/ilyalisov/jwt/fake/FakeTokenStorageImpl.java index 878eb0f..4440c43 100644 --- a/src/test/java/io/github/ilyalisov/jwt/fake/FakeTokenStorageImpl.java +++ b/src/test/java/io/github/ilyalisov/jwt/fake/FakeTokenStorageImpl.java @@ -52,4 +52,29 @@ public boolean exists( return token.equals(tokens.get(tokenKey)); } + @Override + public boolean remove( + final String token + ) { + boolean deleted = false; + for (Map.Entry entry : tokens.entrySet()) { + if (entry.getValue().equals(token)) { + tokens.remove(entry.getKey()); + deleted = true; + } + } + return deleted; + } + + @Override + public boolean remove( + final TokenParameters params + ) { + String tokenKey = subjectTokenKey( + params.getSubject(), + params.getType() + ); + return tokens.remove(tokenKey) != null; + } + } diff --git a/src/test/java/io/github/ilyalisov/jwt/service/PersistentTokenServiceImplTests.java b/src/test/java/io/github/ilyalisov/jwt/service/PersistentTokenServiceImplTests.java index 3b759be..0c00377 100644 --- a/src/test/java/io/github/ilyalisov/jwt/service/PersistentTokenServiceImplTests.java +++ b/src/test/java/io/github/ilyalisov/jwt/service/PersistentTokenServiceImplTests.java @@ -12,6 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -197,9 +198,74 @@ void shouldReturnCorrectClaims() { Map claims = tokenService.claims(token); assertNotNull(claims); - System.out.println(claims.get("key1")); assertEquals("value1", claims.get("key1")); assertEquals(123, claims.get("key2")); } + @Test + void shouldInvalidateByToken() { + String subject = "testSubject"; + String type = "any"; + Duration duration = Duration.ofMinutes(30); + + TokenParameters params = TokenParameters.builder( + subject, + type, + duration + ) + .build(); + String token = tokenService.create(params); + + tokenService.invalidate(token); + + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + TokenParameters newParams = TokenParameters.builder( + subject, + type, + duration + ) + .build(); + String newToken = tokenService.create(newParams); + System.out.println(token); + System.out.println(newToken); + assertNotEquals(token, newToken); + } + + @Test + void shouldInvalidateBySubjectAndType() { + String subject = "testSubject"; + String type = "any"; + Duration duration = Duration.ofMinutes(30); + + TokenParameters params = TokenParameters.builder( + subject, + type, + duration + ) + .build(); + String token = tokenService.create(params); + + tokenService.invalidate(params); + + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + TokenParameters newParams = TokenParameters.builder( + subject, + type, + duration + ) + .build(); + String newToken = tokenService.create(newParams); + assertNotEquals(token, newToken); + } + } diff --git a/src/test/java/io/github/ilyalisov/jwt/storage/RedisTokenStorageImplTests.java b/src/test/java/io/github/ilyalisov/jwt/storage/RedisTokenStorageImplTests.java index f1bc510..6489e5f 100644 --- a/src/test/java/io/github/ilyalisov/jwt/storage/RedisTokenStorageImplTests.java +++ b/src/test/java/io/github/ilyalisov/jwt/storage/RedisTokenStorageImplTests.java @@ -134,4 +134,52 @@ void getWithNonExistingTokenShouldReturnNull() { assertNull(tokenStorage.get(params)); } + @Test + void shouldInvalidateByToken() { + String subject = "testSubject"; + String type = "any"; + Duration duration = Duration.ofMinutes(30); + + TokenParameters params = TokenParameters.builder( + subject, + type, + duration + ) + .build(); + String token = "testToken"; + tokenStorage.save( + token, + params + ); + + tokenStorage.remove(token); + + String existingToken = tokenStorage.get(params); + assertNull(existingToken); + } + + @Test + void shouldInvalidateBySubjectAndType() { + String subject = "testSubject"; + String type = "any"; + Duration duration = Duration.ofMinutes(30); + + TokenParameters params = TokenParameters.builder( + subject, + type, + duration + ) + .build(); + String token = "testToken"; + tokenStorage.save( + token, + params + ); + + tokenStorage.remove(params); + + String existingToken = tokenStorage.get(params); + assertNull(existingToken); + } + } diff --git a/src/test/java/io/github/ilyalisov/jwt/storage/TokenStorageImplTests.java b/src/test/java/io/github/ilyalisov/jwt/storage/TokenStorageImplTests.java index bb5fe81..c1b374e 100644 --- a/src/test/java/io/github/ilyalisov/jwt/storage/TokenStorageImplTests.java +++ b/src/test/java/io/github/ilyalisov/jwt/storage/TokenStorageImplTests.java @@ -109,5 +109,54 @@ void getWithNonExistingTokenShouldReturnNull() { assertNull(tokenStorage.get(params)); } + + @Test + void shouldInvalidateByToken() { + String subject = "testSubject"; + String type = "any"; + Duration duration = Duration.ofMinutes(30); + + TokenParameters params = TokenParameters.builder( + subject, + type, + duration + ) + .build(); + String token = "testToken"; + tokenStorage.save( + token, + params + ); + + tokenStorage.remove(token); + + String existingToken = tokenStorage.get(params); + assertNull(existingToken); + } + + @Test + void shouldInvalidateBySubjectAndType() { + String subject = "testSubject"; + String type = "any"; + Duration duration = Duration.ofMinutes(30); + + TokenParameters params = TokenParameters.builder( + subject, + type, + duration + ) + .build(); + String token = "testToken"; + tokenStorage.save( + token, + params + ); + + tokenStorage.remove(params); + + String existingToken = tokenStorage.get(params); + assertNull(existingToken); + } + }