Skip to content

Commit 21703fe

Browse files
authored
Support concurrent refresh of refresh tokens (#38382)
This change adds supports for the concurrent refresh of access tokens as described in #36872 In short it allows subsequent client requests to refresh the same token that come within a predefined window of 60 seconds to be handled as duplicates of the original one and thus receive the same response with the same newly issued access token and refresh token. In order to support that, two new fields are added in the token document. One contains the instant (in epoqueMillis) when a given refresh token is refreshed and one that contains a pointer to the token document that stores the new refresh token and access token that was created by the original refresh. A side effect of this change, that was however also a intended enhancement for the token service, is that we needed to stop encrypting the string representation of the UserToken while serializing. ( It was necessary as we correctly used a new IV for every time we encrypted a token in serialization, so subsequent serializations of the same exact UserToken would produce different access token strings) This change also handles the serialization/deserialization BWC logic: - In mixed clusters we keep creating tokens in the old format and consume only old format tokens - In upgraded clusters, we start creating tokens in the new format but still remain able to consume old format tokens (that could have been created during the rolling upgrade and are still valid) Resolves #36872 Co-authored-by: Jay Modi [email protected]
1 parent 8d42513 commit 21703fe

File tree

12 files changed

+704
-299
lines changed

12 files changed

+704
-299
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package org.elasticsearch.xpack.core.security.authc.support;
88

99
import org.elasticsearch.ElasticsearchException;
10+
import org.elasticsearch.Version;
1011
import org.elasticsearch.common.Nullable;
1112
import org.elasticsearch.common.io.stream.StreamInput;
1213
import org.elasticsearch.common.io.stream.StreamOutput;
@@ -32,10 +33,9 @@ public class TokensInvalidationResult implements ToXContentObject, Writeable {
3233
private final List<String> invalidatedTokens;
3334
private final List<String> previouslyInvalidatedTokens;
3435
private final List<ElasticsearchException> errors;
35-
private final int attemptCount;
3636

3737
public TokensInvalidationResult(List<String> invalidatedTokens, List<String> previouslyInvalidatedTokens,
38-
@Nullable List<ElasticsearchException> errors, int attemptCount) {
38+
@Nullable List<ElasticsearchException> errors) {
3939
Objects.requireNonNull(invalidatedTokens, "invalidated_tokens must be provided");
4040
this.invalidatedTokens = invalidatedTokens;
4141
Objects.requireNonNull(previouslyInvalidatedTokens, "previously_invalidated_tokens must be provided");
@@ -45,18 +45,19 @@ public TokensInvalidationResult(List<String> invalidatedTokens, List<String> pre
4545
} else {
4646
this.errors = Collections.emptyList();
4747
}
48-
this.attemptCount = attemptCount;
4948
}
5049

5150
public TokensInvalidationResult(StreamInput in) throws IOException {
5251
this.invalidatedTokens = in.readStringList();
5352
this.previouslyInvalidatedTokens = in.readStringList();
5453
this.errors = in.readList(StreamInput::readException);
55-
this.attemptCount = in.readVInt();
54+
if (in.getVersion().before(Version.V_8_0_0)) {
55+
in.readVInt();
56+
}
5657
}
5758

5859
public static TokensInvalidationResult emptyResult() {
59-
return new TokensInvalidationResult(Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), 0);
60+
return new TokensInvalidationResult(Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
6061
}
6162

6263

@@ -72,10 +73,6 @@ public List<ElasticsearchException> getErrors() {
7273
return errors;
7374
}
7475

75-
public int getAttemptCount() {
76-
return attemptCount;
77-
}
78-
7976
@Override
8077
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
8178
builder.startObject()
@@ -100,6 +97,8 @@ public void writeTo(StreamOutput out) throws IOException {
10097
out.writeStringCollection(invalidatedTokens);
10198
out.writeStringCollection(previouslyInvalidatedTokens);
10299
out.writeCollection(errors, StreamOutput::writeException);
103-
out.writeVInt(attemptCount);
100+
if (out.getVersion().before(Version.V_8_0_0)) {
101+
out.writeVInt(5);
102+
}
104103
}
105104
}

x-pack/plugin/core/src/main/resources/security-index-template.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@
199199
"refreshed" : {
200200
"type" : "boolean"
201201
},
202+
"refresh_time": {
203+
"type": "date",
204+
"format": "epoch_millis"
205+
},
206+
"superseded_by": {
207+
"type": "keyword"
208+
},
202209
"invalidated" : {
203210
"type" : "boolean"
204211
},

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/token/InvalidateTokenResponseTests.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ public void testSerialization() throws IOException {
2929
TokensInvalidationResult result = new TokensInvalidationResult(Arrays.asList(generateRandomStringArray(20, 15, false)),
3030
Arrays.asList(generateRandomStringArray(20, 15, false)),
3131
Arrays.asList(new ElasticsearchException("foo", new IllegalArgumentException("this is an error message")),
32-
new ElasticsearchException("bar", new IllegalArgumentException("this is an error message2"))),
33-
randomIntBetween(0, 5));
32+
new ElasticsearchException("bar", new IllegalArgumentException("this is an error message2"))));
3433
InvalidateTokenResponse response = new InvalidateTokenResponse(result);
3534
try (BytesStreamOutput output = new BytesStreamOutput()) {
3635
response.writeTo(output);
@@ -47,8 +46,7 @@ public void testSerialization() throws IOException {
4746
}
4847

4948
result = new TokensInvalidationResult(Arrays.asList(generateRandomStringArray(20, 15, false)),
50-
Arrays.asList(generateRandomStringArray(20, 15, false)),
51-
Collections.emptyList(), randomIntBetween(0, 5));
49+
Arrays.asList(generateRandomStringArray(20, 15, false)), Collections.emptyList());
5250
response = new InvalidateTokenResponse(result);
5351
try (BytesStreamOutput output = new BytesStreamOutput()) {
5452
response.writeTo(output);
@@ -68,8 +66,7 @@ public void testToXContent() throws IOException {
6866
List previouslyInvalidatedTokens = Arrays.asList(generateRandomStringArray(20, 15, false));
6967
TokensInvalidationResult result = new TokensInvalidationResult(invalidatedTokens, previouslyInvalidatedTokens,
7068
Arrays.asList(new ElasticsearchException("foo", new IllegalArgumentException("this is an error message")),
71-
new ElasticsearchException("bar", new IllegalArgumentException("this is an error message2"))),
72-
randomIntBetween(0, 5));
69+
new ElasticsearchException("bar", new IllegalArgumentException("this is an error message2"))));
7370
InvalidateTokenResponse response = new InvalidateTokenResponse(result);
7471
XContentBuilder builder = XContentFactory.jsonBuilder();
7572
response.toXContent(builder, ToXContent.EMPTY_PARAMS);

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ protected void doExecute(Task task, SamlAuthenticateRequest request, ActionListe
6363
final Map<String, Object> tokenMeta = (Map<String, Object>) result.getMetadata().get(SamlRealm.CONTEXT_TOKEN_DATA);
6464
tokenService.createUserToken(authentication, originatingAuthentication,
6565
ActionListener.wrap(tuple -> {
66-
final String tokenString = tokenService.getUserTokenString(tuple.v1());
66+
final String tokenString = tokenService.getAccessTokenAsString(tuple.v1());
6767
final TimeValue expiresIn = tokenService.getExpirationDelay();
6868
listener.onResponse(
6969
new SamlAuthenticateResponse(authentication.getUser().principal(), tokenString, tuple.v2(), expiresIn));

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ private void createToken(CreateTokenRequest request, Authentication authenticati
8989
boolean includeRefreshToken, ActionListener<CreateTokenResponse> listener) {
9090
try {
9191
tokenService.createUserToken(authentication, originatingAuth, ActionListener.wrap(tuple -> {
92-
final String tokenStr = tokenService.getUserTokenString(tuple.v1());
92+
final String tokenStr = tokenService.getAccessTokenAsString(tuple.v1());
9393
final String scope = getResponseScopeValue(request.getScope());
9494

9595
final CreateTokenResponse response =

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportRefreshTokenAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public TransportRefreshTokenAction(TransportService transportService, ActionFilt
3131
@Override
3232
protected void doExecute(Task task, CreateTokenRequest request, ActionListener<CreateTokenResponse> listener) {
3333
tokenService.refreshToken(request.getRefreshToken(), ActionListener.wrap(tuple -> {
34-
final String tokenStr = tokenService.getUserTokenString(tuple.v1());
34+
final String tokenStr = tokenService.getAccessTokenAsString(tuple.v1());
3535
final String scope = getResponseScopeValue(request.getScope());
3636

3737
final CreateTokenResponse response =

0 commit comments

Comments
 (0)