Skip to content

Commit 0dcc872

Browse files
committed
fix: rework ttl access token cache
Closes #90
1 parent f0e653e commit 0dcc872

20 files changed

+369
-80
lines changed

.husky/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
_

.husky/commit-msg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
. "$(dirname "$0")/_/husky.sh"
3+
4+
npx --no-install commitlint --edit $1

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"engines": {
33
"node": ">=10"
44
},
5+
"scripts": {
6+
"prepare": "husky install"
7+
},
58
"devDependencies": {
69
"@commitlint/cli": "12.1.4",
710
"@commitlint/config-conventional": "12.1.4",

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
<guava-testlib.version>31.0.1-jre</guava-testlib.version>
5959
<json.version>20210307</json.version>
6060
<commons-text.version>1.9</commons-text.version>
61+
<java-jwt.version>3.18.2</java-jwt.version>
62+
<nimbus-jose-jwt.version>9.14</nimbus-jose-jwt.version>
6163
</properties>
6264
<!-- Define where the source code for this project lives -->
6365
<scm>
@@ -134,6 +136,11 @@
134136
<artifactId>commons-text</artifactId>
135137
<version>${commons-text.version}</version>
136138
</dependency>
139+
<dependency>
140+
<groupId>com.nimbusds</groupId>
141+
<artifactId>nimbus-jose-jwt</artifactId>
142+
<version>${nimbus-jose-jwt.version}</version>
143+
</dependency>
137144
<!-- JUnit is a Java testing framework.
138145
It is optional in the workflow of deploying with maven-semantic-release. -->
139146
<dependency>

src/main/java/fr/redfroggy/ilg/spring/boot/autoconfigure/AuthorizationInterceptor.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.slf4j.LoggerFactory;
66
import org.springframework.http.HttpHeaders;
77
import org.springframework.http.HttpRequest;
8+
import org.springframework.http.HttpStatus;
89
import org.springframework.http.MediaType;
910
import org.springframework.http.client.ClientHttpRequestExecution;
1011
import org.springframework.http.client.ClientHttpRequestInterceptor;
@@ -30,9 +31,18 @@ public AuthorizationInterceptor(AuthenticationApiClient auth) {
3031
@Override
3132
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException
3233
{
34+
ClientHttpResponse response = addAuthorizationHeader(request, body, execution);
35+
if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
36+
log.info("Reload tokens due to 401 status");
37+
auth.invalidateTokens();
38+
return addAuthorizationHeader(request, body, execution);
39+
}
40+
return response;
41+
}
42+
43+
private ClientHttpResponse addAuthorizationHeader(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
3344
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_JSON_UTF8));
3445
request.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_TYPE +" "+ auth.getTokens().getToken());
35-
ClientHttpResponse response = execution.execute(request, body);
36-
return response;
46+
return execution.execute(request, body);
3747
}
3848
}

src/main/java/fr/redfroggy/ilg/spring/boot/autoconfigure/IlgConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fr.redfroggy.ilg.spring.boot.autoconfigure;
22

33
import fr.redfroggy.ilg.spring.boot.autoconfigure.client.AuthenticationApiClient;
4+
import fr.redfroggy.ilg.spring.boot.autoconfigure.client.cache.JwtCache;
45
import org.springframework.beans.factory.annotation.Qualifier;
56
import org.springframework.boot.context.properties.EnableConfigurationProperties;
67
import org.springframework.context.annotation.Bean;
@@ -23,7 +24,8 @@ public RestTemplate simpleRestTemplate()
2324

2425
@Bean
2526
@DependsOn("simpleRestTemplate")
26-
public AuthenticationApiClient authenticationService(IlgProperties properties, RestTemplate simpleRestTemplate) {
27-
return new AuthenticationApiClient(properties, simpleRestTemplate);
27+
public AuthenticationApiClient authenticationService(IlgProperties properties,
28+
RestTemplate simpleRestTemplate, JwtCache jwtCache) {
29+
return new AuthenticationApiClient(properties, simpleRestTemplate, jwtCache);
2830
}
2931
}

src/main/java/fr/redfroggy/ilg/spring/boot/autoconfigure/IlgProperties.java

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package fr.redfroggy.ilg.spring.boot.autoconfigure;
22

3-
import org.hibernate.validator.constraints.NotBlank;
4-
import org.hibernate.validator.constraints.NotEmpty;
53
import org.springframework.boot.context.properties.ConfigurationProperties;
64
import org.springframework.validation.annotation.Validated;
75

6+
import javax.validation.constraints.NotEmpty;
7+
88
/**
99
* Properties to manage authentication to Ilg api
1010
*
@@ -22,8 +22,6 @@ public class IlgProperties {
2222

2323
private boolean debugging;
2424

25-
private String tokenCacheSpec = "expireAfterWrite=14m";
26-
2725
private boolean decode404;
2826

2927
@NotEmpty
@@ -61,15 +59,6 @@ public void setDebugging(boolean debugging) {
6159
this.debugging = debugging;
6260
}
6361

64-
@NotBlank
65-
public String getTokenCacheSpec() {
66-
return tokenCacheSpec;
67-
}
68-
69-
public void setTokenCacheSpec(String tokenCacheSpec) {
70-
this.tokenCacheSpec = tokenCacheSpec;
71-
}
72-
7362
public boolean isDecode404() {
7463
return decode404;
7564
}

src/main/java/fr/redfroggy/ilg/spring/boot/autoconfigure/client/AuthenticationApiClient.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package fr.redfroggy.ilg.spring.boot.autoconfigure.client;
22

3-
import com.github.benmanes.caffeine.cache.Caffeine;
4-
import com.github.benmanes.caffeine.cache.LoadingCache;
53
import fr.redfroggy.ilg.client.authentication.AuthenticationApi;
64
import fr.redfroggy.ilg.client.authentication.AuthenticationJwt;
75
import fr.redfroggy.ilg.client.authentication.Credentials;
86
import fr.redfroggy.ilg.spring.boot.autoconfigure.IlgProperties;
7+
import fr.redfroggy.ilg.spring.boot.autoconfigure.client.cache.JwtCache;
98
import org.slf4j.Logger;
109
import org.slf4j.LoggerFactory;
1110
import org.springframework.http.ResponseEntity;
@@ -20,35 +19,36 @@ public class AuthenticationApiClient implements AuthenticationApi {
2019

2120
private final Credentials credentials;
2221
private final RestTemplate client;
23-
private final LoadingCache<String, AuthenticationJwt> tokens;
22+
23+
private final JwtCache jwtCache;
2424

2525
private final String baseUrl;
2626

27-
public AuthenticationApiClient(IlgProperties properties, RestTemplate client) {
27+
public AuthenticationApiClient(IlgProperties properties, RestTemplate client,
28+
JwtCache jwtCache) {
2829
Assert.notNull(properties, "Can't build token service without properties");
30+
Assert.notNull(jwtCache, "Can't build token service without jwt cache");
2931

3032
baseUrl = properties.getUrl();
3133
credentials = new Credentials(properties.getUsername(),
3234
properties.getPassword());
33-
3435
this.client = client;
35-
36-
tokens = Caffeine.from(properties.getTokenCacheSpec())
37-
.build(key -> getNewTokens());
36+
this.jwtCache = jwtCache;
3837
}
3938

4039
public AuthenticationJwt getTokens() {
41-
return tokens.get("tokens");
40+
return jwtCache.get("tokens", () -> getNewTokens());
4241
}
4342

44-
public AuthenticationJwt getNewTokens() {
43+
private AuthenticationJwt getNewTokens() {
44+
AuthenticationJwt jwt = login(credentials).getBody();
4545
log.info("New ilg tokens requested");
46-
return login(credentials).getBody();
46+
return jwt;
4747
}
4848

4949
public void invalidateTokens() {
5050
log.info("Evict requested tokens");
51-
tokens.invalidate("tokens");
51+
jwtCache.invalidate("tokens");
5252
}
5353

5454
@Override
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package fr.redfroggy.ilg.spring.boot.autoconfigure.client.cache;
2+
3+
import com.github.benmanes.caffeine.cache.Cache;
4+
import com.github.benmanes.caffeine.cache.Caffeine;
5+
import com.nimbusds.jwt.JWT;
6+
import com.nimbusds.jwt.JWTParser;
7+
import fr.redfroggy.ilg.client.authentication.AuthenticationJwt;
8+
import fr.redfroggy.ilg.spring.boot.autoconfigure.client.AuthenticationApiClient;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
import org.springframework.stereotype.Component;
12+
13+
import java.text.ParseException;
14+
import java.time.Duration;
15+
import java.time.Instant;
16+
import java.util.Date;
17+
import java.util.Optional;
18+
import java.util.function.Supplier;
19+
20+
@Component
21+
public class JwtCache {
22+
23+
private static final Logger log = LoggerFactory.getLogger(AuthenticationApiClient.class);
24+
25+
private Cache<String, AuthenticationJwt> authorizedCache = Caffeine.newBuilder().build();
26+
27+
private Duration durationCache = Duration.ZERO;
28+
29+
public AuthenticationJwt get(String key, Supplier<AuthenticationJwt> getNewToken) {
30+
return Optional.ofNullable(authorizedCache.getIfPresent(key))
31+
.orElseGet(() -> putAndGet(key,getNewToken) );
32+
}
33+
34+
public AuthenticationJwt putAndGet(String key, Supplier<AuthenticationJwt> getNewToken) {
35+
AuthenticationJwt jwt = getNewToken.get();
36+
JWT accessToken;
37+
Date expirationTime = null;
38+
try {
39+
accessToken = JWTParser.parse(jwt.getToken());
40+
expirationTime = accessToken.getJWTClaimsSet().getExpirationTime();
41+
} catch (ParseException e) {
42+
log.error("Cannot parse authentication jwt.",e);
43+
}
44+
Duration durationCache = Duration.between(Instant.now().plusSeconds(5), expirationTime.toInstant());
45+
if (!(durationCache.getSeconds() == this.durationCache.getSeconds())) {
46+
log.info("New duration for jwt cache {} s (previous is {} s)", durationCache.getSeconds(),
47+
this.durationCache.getSeconds());
48+
this.durationCache = durationCache;
49+
authorizedCache = Caffeine.newBuilder().expireAfterWrite(durationCache).build();
50+
}
51+
authorizedCache.put(key, jwt);
52+
return jwt;
53+
}
54+
55+
public void invalidate(String key) {
56+
authorizedCache.invalidate(key);
57+
}
58+
}

src/test/java/fr/redfroggy/ilg/client/ApiClientMockRestTest.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.fasterxml.jackson.core.JsonProcessingException;
44
import com.fasterxml.jackson.databind.ObjectMapper;
55
import fr.redfroggy.ilg.client.authentication.AuthenticationJwt;
6+
import fr.redfroggy.ilg.spring.boot.autoconfigure.client.cache.JWTFixture;
67
import org.hamcrest.core.StringContains;
78
import org.junit.jupiter.api.BeforeEach;
89
import org.springframework.beans.factory.annotation.Autowired;
@@ -16,6 +17,8 @@
1617

1718
import java.net.URI;
1819
import java.net.URISyntaxException;
20+
import java.time.Instant;
21+
import java.util.Date;
1922

2023
import static org.hamcrest.Matchers.startsWith;
2124
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
@@ -32,10 +35,12 @@ public abstract class ApiClientMockRestTest {
3235
private MockRestServiceServer mockAuthorizedServer;
3336
protected ObjectMapper mapper = new ObjectMapper();
3437

38+
public static final String ACCESS_TOKEN = JWTFixture.anAccessToken(Date.from(Instant.now().plusSeconds(360)));
39+
3540
@BeforeEach
3641
public void init() throws URISyntaxException, JsonProcessingException {
3742
mockAuthorizedServer = MockRestServiceServer.createServer(simpleRestTemplate);
38-
AuthenticationJwt jwt = new AuthenticationJwt("test-token", "test-refreshToken");
43+
AuthenticationJwt jwt = new AuthenticationJwt(ACCESS_TOKEN, "test-refreshToken");
3944
mockAuthorizedServer.expect(ExpectedCount.once(),
4045
requestTo(new URI("http://ilg.fr/login_json")))
4146
.andRespond(withStatus(HttpStatus.OK)
@@ -50,7 +55,7 @@ protected void mockApi(String uri, String body) throws URISyntaxException {
5055
mockApiServer.expect(ExpectedCount.once(),
5156
requestTo(new URI(uri)))
5257
.andExpect(method(HttpMethod.GET))
53-
.andExpect(header("authorization", "Bearer test-token"))
58+
.andExpect(header("authorization", "Bearer "+ACCESS_TOKEN))
5459
.andExpect(header("accept", MediaType.APPLICATION_JSON_UTF8_VALUE))
5560
.andRespond(withStatus(HttpStatus.OK)
5661
.contentType(MediaType.APPLICATION_JSON)
@@ -63,7 +68,7 @@ protected void mockPostData(String uri, String body, String formDataKey, String
6368
requestTo(
6469
new URI(uri)))
6570
.andExpect(method(HttpMethod.POST))
66-
.andExpect(header("authorization","Bearer test-token"))
71+
.andExpect(header("authorization","Bearer "+ACCESS_TOKEN))
6772
.andExpect(header("accept",MediaType.APPLICATION_JSON_UTF8_VALUE))
6873
.andExpect(header(HttpHeaders.CONTENT_TYPE, startsWith("multipart/form-data")))
6974
.andExpect(content().string(StringContains.containsString("Content-Disposition: form-data; " +

0 commit comments

Comments
 (0)