Skip to content

Commit 0b314f8

Browse files
cache API auth keys to offload an app and avoid frequent HTTP requests towards DynamoDB
1 parent c9555df commit 0b314f8

File tree

4 files changed

+48
-27
lines changed

4 files changed

+48
-27
lines changed

service/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@
6060
<version>3.0.5</version>
6161
</dependency>
6262

63+
<dependency>
64+
<groupId>com.github.ben-manes.caffeine</groupId>
65+
<artifactId>caffeine</artifactId>
66+
</dependency>
67+
6368
<!-- Test dependencies -->
6469
<dependency>
6570
<groupId>org.junit.jupiter</groupId>

service/src/main/java/io/github/sergejsvisockis/documentservice/auth/AuthenticationService.java

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,48 @@
11
package io.github.sergejsvisockis.documentservice.auth;
22

3+
import com.github.benmanes.caffeine.cache.Cache;
4+
import com.github.benmanes.caffeine.cache.Caffeine;
35
import io.github.sergejsvisockis.documentservice.repository.ApiKey;
46
import io.github.sergejsvisockis.documentservice.repository.ApiKeyRepository;
57
import jakarta.servlet.http.HttpServletRequest;
6-
import lombok.RequiredArgsConstructor;
78
import org.springframework.security.authentication.BadCredentialsException;
89
import org.springframework.security.core.Authentication;
910
import org.springframework.security.core.authority.AuthorityUtils;
1011
import org.springframework.stereotype.Service;
1112

1213
import java.time.LocalDateTime;
1314
import java.util.Base64;
14-
import java.util.Optional;
15+
import java.util.concurrent.TimeUnit;
1516

1617
@Service
17-
@RequiredArgsConstructor
1818
public class AuthenticationService {
1919

2020
static final String AUTH_TOKEN_HEADER_NAME = "x-api-key";
2121
static final String INCORRECT_API_KEY_MESSAGE = "Incorrect API key passed";
22-
static final String NO_API_KEY_FOUND_MESSAGE = "NO associated API key found";
22+
static final String NO_API_KEY_FOUND_MESSAGE = "No associated API key found";
2323
static final String API_KEY_EXPIRED_MESSAGE = "An API key has expired";
2424

25+
private final Cache<String, ApiKey> cache;
2526
private final ApiKeyRepository apiKeyRepository;
2627

28+
public AuthenticationService(ApiKeyRepository apiKeyRepository) {
29+
this.apiKeyRepository = apiKeyRepository;
30+
this.cache = Caffeine.newBuilder()
31+
.expireAfterAccess(2, TimeUnit.HOURS)
32+
.initialCapacity(10)
33+
.maximumSize(70)
34+
.build();
35+
}
36+
2737
public Authentication getAuthentication(HttpServletRequest request) {
2838
String apiKey = request.getHeader(AUTH_TOKEN_HEADER_NAME);
2939
if (apiKey == null) {
3040
throw new BadCredentialsException(INCORRECT_API_KEY_MESSAGE);
3141
}
3242

33-
Optional<ApiKey> apiAuthKey = findApiAuthKey(apiKey);
34-
if (apiAuthKey.isEmpty()) {
35-
throw new BadCredentialsException(NO_API_KEY_FOUND_MESSAGE);
36-
}
43+
ApiKey apiAuthKey = findApiAuthKey(apiKey);
3744

38-
if (!isValid(apiAuthKey.get())) {
45+
if (!isValid(apiAuthKey)) {
3946
throw new BadCredentialsException(API_KEY_EXPIRED_MESSAGE);
4047
}
4148

@@ -47,9 +54,21 @@ private boolean isValid(ApiKey apiKey) {
4754
return LocalDateTime.now().isBefore(validTo);
4855
}
4956

50-
private Optional<ApiKey> findApiAuthKey(String key) {
51-
return apiKeyRepository.findApiKey(encodeApiKey(key));
57+
private ApiKey findApiAuthKey(String key) {
58+
ApiKey fromCache = cache.getIfPresent(key);
59+
60+
if (fromCache != null) {
61+
return fromCache;
62+
}
63+
64+
ApiKey apiKey = apiKeyRepository.findApiKey(encodeApiKey(key));
65+
66+
if (apiKey == null) {
67+
throw new BadCredentialsException(NO_API_KEY_FOUND_MESSAGE);
68+
}
5269

70+
cache.put(key, apiKey);
71+
return apiKey;
5372
}
5473

5574
private String encodeApiKey(String key) {

service/src/main/java/io/github/sergejsvisockis/documentservice/repository/ApiKeyRepository.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
99

1010
import java.util.Map;
11-
import java.util.Optional;
1211

1312
@Repository
1413
@RequiredArgsConstructor
1514
public class ApiKeyRepository {
1615

1716
private final DynamoDbTemplate dynamoDbTemplate;
1817

19-
public Optional<ApiKey> findApiKey(String key) {
18+
public ApiKey findApiKey(String key) {
2019
ScanEnhancedRequest request = ScanEnhancedRequest.builder()
2120
.filterExpression(Expression.builder()
2221
.expression("apiKey = :key")
@@ -26,7 +25,8 @@ public Optional<ApiKey> findApiKey(String key) {
2625
return dynamoDbTemplate.scan(request, ApiKey.class)
2726
.stream()
2827
.flatMap(p -> p.items().stream())
29-
.findFirst();
28+
.findFirst()
29+
.orElse(null);
3030
}
3131

3232
}

service/src/test/java/io/github/sergejsvisockis/documentservice/auth/AuthenticationServiceTest.java

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77
import org.junit.jupiter.api.extension.ExtendWith;
88
import org.mockito.InjectMocks;
99
import org.mockito.Mock;
10+
import org.mockito.Mockito;
1011
import org.mockito.junit.jupiter.MockitoExtension;
1112
import org.springframework.security.authentication.BadCredentialsException;
1213
import org.springframework.security.core.Authentication;
1314

1415
import java.time.LocalDateTime;
1516
import java.util.Base64;
16-
import java.util.Optional;
1717

18+
import static io.github.sergejsvisockis.documentservice.auth.AuthenticationService.API_KEY_EXPIRED_MESSAGE;
19+
import static io.github.sergejsvisockis.documentservice.auth.AuthenticationService.INCORRECT_API_KEY_MESSAGE;
20+
import static io.github.sergejsvisockis.documentservice.auth.AuthenticationService.NO_API_KEY_FOUND_MESSAGE;
1821
import static org.junit.jupiter.api.Assertions.assertEquals;
1922
import static org.junit.jupiter.api.Assertions.assertThrows;
20-
import static org.mockito.ArgumentMatchers.anyString;
2123
import static org.mockito.Mockito.when;
2224

2325
@ExtendWith(MockitoExtension.class)
@@ -27,10 +29,6 @@ class AuthenticationServiceTest {
2729
private static final String VALID_API_KEY = "valid-api-key";
2830
private static final String ENCODED_API_KEY = Base64.getEncoder().encodeToString(VALID_API_KEY.getBytes());
2931

30-
private static final String INCORRECT_API_KEY_MESSAGE = "Incorrect API key passed";
31-
private static final String NO_API_KEY_FOUND_MESSAGE = "NO associated API key found";
32-
private static final String API_KEY_EXPIRED_MESSAGE = "An API key has expired";
33-
3432
@Mock
3533
private ApiKeyRepository apiKeyRepository;
3634

@@ -48,7 +46,7 @@ void shouldReturnAuthenticationWhenApiKeyIsValid() {
4846
apiKey.setExpirationDate(LocalDateTime.now().plusDays(1).toString());
4947

5048
when(request.getHeader(AUTH_TOKEN_HEADER_NAME)).thenReturn(VALID_API_KEY);
51-
when(apiKeyRepository.findApiKey(ENCODED_API_KEY)).thenReturn(Optional.of(apiKey));
49+
when(apiKeyRepository.findApiKey(ENCODED_API_KEY)).thenReturn(apiKey);
5250

5351
// when
5452
Authentication authentication = authenticationService.getAuthentication(request);
@@ -61,7 +59,7 @@ void shouldReturnAuthenticationWhenApiKeyIsValid() {
6159
void shouldThrowExceptionWhenApiKeyIsNotFound() {
6260
// given
6361
when(request.getHeader(AUTH_TOKEN_HEADER_NAME)).thenReturn(VALID_API_KEY);
64-
when(apiKeyRepository.findApiKey(anyString())).thenReturn(Optional.empty());
62+
when(apiKeyRepository.findApiKey(ENCODED_API_KEY)).thenReturn(null);
6563

6664
// when & then
6765
BadCredentialsException exception = assertThrows(BadCredentialsException.class,
@@ -83,12 +81,11 @@ void shouldThrowExceptionWhenApiKeyIsNull() {
8381
@Test
8482
void shouldThrowExceptionWhenApiKeyHasExpired() {
8583
// given
86-
ApiKey apiKey = new ApiKey();
87-
apiKey.setApiKey(ENCODED_API_KEY);
88-
apiKey.setExpirationDate(LocalDateTime.now().minusDays(1).toString());
89-
84+
ApiKey apiKey = Mockito.mock(ApiKey.class);
85+
when(apiKey.getExpirationDate()).thenReturn(LocalDateTime.now().minusDays(1).toString());
9086
when(request.getHeader(AUTH_TOKEN_HEADER_NAME)).thenReturn(VALID_API_KEY);
91-
when(apiKeyRepository.findApiKey(ENCODED_API_KEY)).thenReturn(Optional.of(apiKey));
87+
when(apiKeyRepository.findApiKey(ENCODED_API_KEY)).thenReturn(apiKey);
88+
9289

9390
// when & then
9491
BadCredentialsException exception = assertThrows(BadCredentialsException.class,

0 commit comments

Comments
 (0)