Skip to content

Commit cfc7603

Browse files
committed
Hash token values for storage (elastic#41792)
This commit changes how access tokens and refresh tokens are stored in the tokens index. Access token values are now hashed before being stored in the id field of the `user_token` and before becoming part of the token document id. Refresh token values are hashed before being stored in the token field of the `refresh_token`. The tokens are hashed without a salt value since these are v4 UUID values that have enough entropy themselves. Both rainbow table attacks and offline brute force attacks are impractical. As a side effect of this change and in order to support multiple concurrent refreshes as introduced in elastic#39631, upon refreshing an <access token, refresh token> pair, the superseding access token and refresh tokens values are stored in the superseded token doc, encrypted with a key that is derived from the superseded refresh token. As such, subsequent requests to refresh the same token in the predefined time window will return the same superseding access token and refresh token values, without hitting the tokens index (as this only stores hashes of the token values). AES in GCM mode is used for encrypting the token values and the key derivation from the superseded refresh token uses a small number of iterations as it needs to be quick. For backwards compatibility reasons, the new behavior is only enabled when all nodes in a cluster are in the required version so that old nodes can cope with the token values in a mixed cluster during a rolling upgrade.
1 parent b759947 commit cfc7603

15 files changed

+627
-372
lines changed

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

+18
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,24 @@ public boolean verify(SecureString text, char[] hash) {
351351
return CharArrays.constantTimeEquals(computedHash, new String(saltAndHash, 12, saltAndHash.length - 12));
352352
}
353353
},
354+
/*
355+
* Unsalted SHA-256 , not suited for password storage.
356+
*/
357+
SHA256() {
358+
@Override
359+
public char[] hash(SecureString text) {
360+
MessageDigest md = MessageDigests.sha256();
361+
md.update(CharArrays.toUtf8Bytes(text.getChars()));
362+
return Base64.getEncoder().encodeToString(md.digest()).toCharArray();
363+
}
364+
365+
@Override
366+
public boolean verify(SecureString text, char[] hash) {
367+
MessageDigest md = MessageDigests.sha256();
368+
md.update(CharArrays.toUtf8Bytes(text.getChars()));
369+
return CharArrays.constantTimeEquals(Base64.getEncoder().encodeToString(md.digest()).toCharArray(), hash);
370+
}
371+
},
354372

355373
NOOP() {
356374
@Override

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

+13-2
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,19 @@
213213
"type": "date",
214214
"format": "epoch_millis"
215215
},
216-
"superseded_by": {
217-
"type": "keyword"
216+
"superseding": {
217+
"type": "object",
218+
"properties": {
219+
"encrypted_tokens": {
220+
"type": "binary"
221+
},
222+
"encryption_iv": {
223+
"type": "binary"
224+
},
225+
"encryption_salt": {
226+
"type": "binary"
227+
}
228+
}
218229
},
219230
"invalidated" : {
220231
"type" : "boolean"

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

+13-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,19 @@
3535
"type": "date",
3636
"format": "epoch_millis"
3737
},
38-
"superseded_by": {
39-
"type": "keyword"
38+
"superseding": {
39+
"type": "object",
40+
"properties": {
41+
"encrypted_tokens": {
42+
"type": "binary"
43+
},
44+
"encryption_iv": {
45+
"type": "binary"
46+
},
47+
"encryption_salt": {
48+
"type": "binary"
49+
}
50+
}
4051
},
4152
"invalidated" : {
4253
"type" : "boolean"

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import com.nimbusds.oauth2.sdk.id.State;
99
import com.nimbusds.openid.connect.sdk.Nonce;
10+
import org.apache.logging.log4j.LogManager;
11+
import org.apache.logging.log4j.Logger;
1012
import org.apache.logging.log4j.message.ParameterizedMessage;
1113
import org.elasticsearch.action.ActionListener;
1214
import org.elasticsearch.action.support.ActionFilters;
@@ -36,6 +38,7 @@ public class TransportOpenIdConnectAuthenticateAction
3638
private final ThreadPool threadPool;
3739
private final AuthenticationService authenticationService;
3840
private final TokenService tokenService;
41+
private static final Logger logger = LogManager.getLogger(TransportOpenIdConnectAuthenticateAction.class);
3942

4043
@Inject
4144
public TransportOpenIdConnectAuthenticateAction(ThreadPool threadPool, TransportService transportService,
@@ -67,9 +70,8 @@ protected void doExecute(Task task, OpenIdConnectAuthenticateRequest request,
6770
.get(OpenIdConnectRealm.CONTEXT_TOKEN_DATA);
6871
tokenService.createOAuth2Tokens(authentication, originatingAuthentication, tokenMetadata, true,
6972
ActionListener.wrap(tuple -> {
70-
final String tokenString = tokenService.getAccessTokenAsString(tuple.v1());
7173
final TimeValue expiresIn = tokenService.getExpirationDelay();
72-
listener.onResponse(new OpenIdConnectAuthenticateResponse(authentication.getUser().principal(), tokenString,
74+
listener.onResponse(new OpenIdConnectAuthenticateResponse(authentication.getUser().principal(), tuple.v1(),
7375
tuple.v2(), expiresIn));
7476
}, listener::onFailure));
7577
}, e -> {

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,9 @@ 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.createOAuth2Tokens(authentication, originatingAuthentication,
6565
tokenMeta, true, ActionListener.wrap(tuple -> {
66-
final String tokenString = tokenService.getAccessTokenAsString(tuple.v1());
6766
final TimeValue expiresIn = tokenService.getExpirationDelay();
6867
listener.onResponse(
69-
new SamlAuthenticateResponse(authentication.getUser().principal(), tokenString, tuple.v2(), expiresIn));
68+
new SamlAuthenticateResponse(authentication.getUser().principal(), tuple.v1(), tuple.v2(), expiresIn));
7069
}, listener::onFailure));
7170
}, e -> {
7271
logger.debug(() -> new ParameterizedMessage("SamlToken [{}] could not be authenticated", saml), e);

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,8 @@ private void createToken(CreateTokenRequest request, Authentication authenticati
8888
boolean includeRefreshToken, ActionListener<CreateTokenResponse> listener) {
8989
tokenService.createOAuth2Tokens(authentication, originatingAuth, Collections.emptyMap(), includeRefreshToken,
9090
ActionListener.wrap(tuple -> {
91-
final String tokenStr = tokenService.getAccessTokenAsString(tuple.v1());
9291
final String scope = getResponseScopeValue(request.getScope());
93-
final CreateTokenResponse response = new CreateTokenResponse(tokenStr, tokenService.getExpirationDelay(), scope,
92+
final CreateTokenResponse response = new CreateTokenResponse(tuple.v1(), tokenService.getExpirationDelay(), scope,
9493
tuple.v2());
9594
listener.onResponse(response);
9695
}, listener::onFailure));

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

+1-3
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,9 @@ 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.getAccessTokenAsString(tuple.v1());
3534
final String scope = getResponseScopeValue(request.getScope());
36-
3735
final CreateTokenResponse response =
38-
new CreateTokenResponse(tokenStr, tokenService.getExpirationDelay(), scope, tuple.v2());
36+
new CreateTokenResponse(tuple.v1(), tokenService.getExpirationDelay(), scope, tuple.v2());
3937
listener.onResponse(response);
4038
}, listener::onFailure));
4139
}

0 commit comments

Comments
 (0)