Skip to content

Commit 23d149a

Browse files
committed
feat: add early expiry to tokens
Refreshing the token slightly ahead of its expiry can help prevent phony unauthorized access errors.
1 parent 6bd8056 commit 23d149a

3 files changed

Lines changed: 111 additions & 10 deletions

File tree

src/main/java/io/weaviate/client6/v1/internal/ReuseTokenProvider.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,25 @@
2020
@ThreadSafe
2121
final class ReuseTokenProvider implements TokenProvider {
2222
private final TokenProvider provider;
23+
private final long expiryDelta;
2324

2425
private volatile Token token;
2526

27+
/**
28+
* Create new {@link ReuseTokenProvider} from another {@link TokenProvider}.
29+
* Wrapping an instance ReuseTokenProvider returns that instance if the token is
30+
* {@code null}, so this method is safe to call with any TokenProvider.
31+
*/
32+
static TokenProvider wrap(Token t, TokenProvider tp, long expiryDelta) {
33+
if (tp instanceof ReuseTokenProvider rtp) {
34+
if (t == null) {
35+
// Use it directly, but set new expirty delta.
36+
return rtp.withExpiryDelta(expiryDelta);
37+
}
38+
}
39+
return new ReuseTokenProvider(t, tp, expiryDelta);
40+
}
41+
2642
/**
2743
* Create new {@link ReuseTokenProvider} from another {@link TokenProvider}.
2844
* Wrapping an instance ReuseTokenProvider returns that instance if the token is
@@ -34,12 +50,24 @@ static TokenProvider wrap(Token t, TokenProvider tp) {
3450
return rtp; // Use it directly.
3551
}
3652
}
37-
return new ReuseTokenProvider(t, tp);
53+
return new ReuseTokenProvider(t, tp, 0);
54+
}
55+
56+
/**
57+
* Create a new TokenProvider with a different expiryDelta.
58+
* Tokens obtained from this TokenProvider with have the same early expiry.
59+
*
60+
* @param expiryDelta Early expiry in seconds.
61+
* @return A new TokenProvider.
62+
*/
63+
TokenProvider withExpiryDelta(long expirtyDelta) {
64+
return new ReuseTokenProvider(this.token, this.provider, expirtyDelta);
3865
}
3966

40-
private ReuseTokenProvider(Token t, TokenProvider tp) {
67+
private ReuseTokenProvider(Token t, TokenProvider tp, long expiryDelta) {
4168
this.provider = tp;
4269
this.token = t;
70+
this.expiryDelta = expiryDelta;
4371
}
4472

4573
@Override
@@ -49,7 +77,7 @@ public Token getToken() {
4977
}
5078
synchronized (this) {
5179
if (token == null || !token.isValid()) {
52-
token = provider.getToken();
80+
token = provider.getToken().withExpiryDelta(expiryDelta);
5381
}
5482
}
5583
return token;

src/main/java/io/weaviate/client6/v1/internal/TokenProvider.java

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public interface TokenProvider {
1313
Token getToken();
1414

1515
/** Token represents an access_token + refresh_token pair. */
16-
public record Token(String accessToken, String refreshToken, Instant createdAt, long expiresIn) {
16+
public record Token(String accessToken, String refreshToken, Instant createdAt, long expiresIn, long expiryDelta) {
1717
/**
1818
* Returns {@code true} if remaining lifetime of the token is greater than 0.
1919
* Tokens created with {@link #expireNever} are always valid.
@@ -22,8 +22,21 @@ public boolean isValid() {
2222
if (expiresIn == -1) {
2323
return true;
2424
}
25-
// TODO: adjust for expireDelta
26-
return Instant.now().isAfter(createdAt.plusSeconds(expiresIn));
25+
return Instant.now().isBefore(createdAt.plusSeconds(expiresIn - expiryDelta));
26+
}
27+
28+
/**
29+
* Set early expiry for the Token.
30+
*
31+
* <p>
32+
* A Token with {@link #expiresIn} of 10s and {@link #expiryDelta} of 3s
33+
* will be invalid 7s after being created.
34+
*
35+
* @param expiryDelta Early expiry in seconds.
36+
* @return A Token identical to the source one, but with a different expiry.
37+
*/
38+
public Token withExpiryDelta(long expiryDelta) {
39+
return new Token(accessToken, refreshToken, createdAt, expiresIn, expiryDelta);
2740
}
2841

2942
/**
@@ -36,11 +49,13 @@ public boolean isValid() {
3649
* @return A new Token.
3750
*/
3851
public static Token expireAfter(String accessToken, String refreshToken, long expiresIn) {
39-
return new Token(accessToken, refreshToken, Instant.now(), expiresIn);
52+
return new Token(accessToken, refreshToken, Instant.now(), expiresIn, 0);
4053
}
4154

4255
/**
4356
* Create a token that does not have a refresh_token.
57+
* For example, a token obtained via a Client Credentials grant
58+
* can only be renewed using that grant type.
4459
*
4560
* @param accessToken Access token.
4661
* @param expiresIn Remaining token lifetime in seconds.
@@ -62,6 +77,20 @@ public static Token expireNever(String accessToken) {
6277
}
6378
}
6479

80+
/**
81+
* Refreshing the token slightly ahead of time will help prevent
82+
* phony unauthorized access errors.
83+
*
84+
* This value is currently not configuratble and should be seen
85+
* as an internal implementation detail.
86+
*/
87+
static long DEFAULT_EARLY_EXPIRY = 30;
88+
89+
/**
90+
* Authorize using a token that never expires and doesn't need to be refreshed.
91+
*
92+
* @param apiKey Access token.
93+
*/
6594
public static TokenProvider staticToken(String accessToken) {
6695
final var token = Token.expireNever(accessToken);
6796
return () -> token;
@@ -83,7 +112,7 @@ public static TokenProvider staticToken(String accessToken) {
83112
public static TokenProvider bearerToken(OidcConfig oidc, String accessToken, String refreshToken, long expiresIn) {
84113
final var token = Token.expireAfter(accessToken, refreshToken, expiresIn);
85114
final var provider = NimbusTokenProvider.refreshToken(oidc, token);
86-
return reuse(token, provider);
115+
return reuse(token, provider, DEFAULT_EARLY_EXPIRY);
87116
}
88117

89118
/**
@@ -99,7 +128,7 @@ public static TokenProvider bearerToken(OidcConfig oidc, String accessToken, Str
99128
*/
100129
public static TokenProvider resourceOwnerPassword(OidcConfig oidc, String username, String password) {
101130
final var passwordGrant = NimbusTokenProvider.resourceOwnerPassword(oidc, username, password);
102-
return reuse(null, exchange(oidc, passwordGrant));
131+
return reuse(null, exchange(oidc, passwordGrant), DEFAULT_EARLY_EXPIRY);
103132
}
104133

105134
/**
@@ -115,7 +144,7 @@ public static TokenProvider resourceOwnerPassword(OidcConfig oidc, String userna
115144
*/
116145
public static TokenProvider clientCredentials(OidcConfig oidc, String clientId, String clientSecret) {
117146
final var provider = NimbusTokenProvider.clientCredentials(oidc, clientId, clientSecret);
118-
return reuse(null, provider);
147+
return reuse(null, provider, DEFAULT_EARLY_EXPIRY);
119148
}
120149

121150
/**
@@ -133,6 +162,14 @@ static TokenProvider reuse(Token t, TokenProvider tp) {
133162
return ReuseTokenProvider.wrap(t, tp);
134163
}
135164

165+
/**
166+
* Obtain a TokenProvider which reuses tokens obtained
167+
* from another TokenProvider until they expire.
168+
*/
169+
static TokenProvider reuse(Token t, TokenProvider tp, long expiryDelta) {
170+
return ReuseTokenProvider.wrap(t, tp, expiryDelta);
171+
}
172+
136173
public record ProviderMetadata(URI tokenEndpoint) {
137174
}
138175

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.weaviate.client6.v1.internal;
2+
3+
import java.time.Instant;
4+
5+
import org.assertj.core.api.Assertions;
6+
import org.junit.Test;
7+
import org.junit.runner.RunWith;
8+
9+
import com.jparams.junit4.JParamsTestRunner;
10+
import com.jparams.junit4.data.DataMethod;
11+
12+
import io.weaviate.client6.v1.internal.TokenProvider.Token;
13+
14+
@RunWith(JParamsTestRunner.class)
15+
public class TokenTest {
16+
17+
public static Object[][] testCaseTokens() {
18+
return new Object[][] {
19+
{ Token.expireNever("access_token"), true },
20+
{ Token.expireAfter("access_token", "refresh_token", 100), true },
21+
{ Token.expireAfter("access_token", 100), true },
22+
{ new Token("access_token", "refresh_token", Instant.now().minusSeconds(10), 5, 0), false },
23+
{ Token.expireAfter("access_token", 0), false },
24+
{ Token.expireAfter("access_token", 5).withExpiryDelta(10), false },
25+
{ Token.expireAfter("access_token", 100).withExpiryDelta(10), true },
26+
};
27+
}
28+
29+
@DataMethod(source = TokenTest.class, method = "testCaseTokens")
30+
@Test
31+
public void test_isValid(Token token, boolean wantValid) {
32+
Assertions.assertThat(token.isValid())
33+
.as(token.toString())
34+
.isEqualTo(wantValid);
35+
}
36+
}

0 commit comments

Comments
 (0)