diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java
index bda66d6a2c8cd..9bc2487f89b12 100644
--- a/server/src/main/java/org/elasticsearch/TransportVersions.java
+++ b/server/src/main/java/org/elasticsearch/TransportVersions.java
@@ -52,7 +52,10 @@ static TransportVersion def(int id) {
@UpdateForV9(owner = UpdateForV9.Owner.CORE_INFRA) // remove the transport versions with which v9 will not need to interact
public static final TransportVersion ZERO = def(0);
public static final TransportVersion V_7_0_0 = def(7_00_00_99);
+ public static final TransportVersion V_7_1_0 = def(7_01_00_99);
+ public static final TransportVersion V_7_2_0 = def(7_02_00_99);
public static final TransportVersion V_7_3_0 = def(7_03_00_99);
+ public static final TransportVersion V_7_3_2 = def(7_03_02_99);
public static final TransportVersion V_7_4_0 = def(7_04_00_99);
public static final TransportVersion V_7_6_0 = def(7_06_00_99);
public static final TransportVersion V_7_8_0 = def(7_08_00_99);
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java
index f44409daa37f8..3ebfad04a0f13 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java
@@ -55,8 +55,10 @@ public SecurityFeatureSetUsage(StreamInput in) throws IOException {
realmsUsage = in.readGenericMap();
rolesStoreUsage = in.readGenericMap();
sslUsage = in.readGenericMap();
- tokenServiceUsage = in.readGenericMap();
- apiKeyServiceUsage = in.readGenericMap();
+ if (in.getTransportVersion().onOrAfter(TransportVersions.V_7_2_0)) {
+ tokenServiceUsage = in.readGenericMap();
+ apiKeyServiceUsage = in.readGenericMap();
+ }
auditUsage = in.readGenericMap();
ipFilterUsage = in.readGenericMap();
anonymousUsage = in.readGenericMap();
@@ -121,8 +123,10 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeGenericMap(realmsUsage);
out.writeGenericMap(rolesStoreUsage);
out.writeGenericMap(sslUsage);
- out.writeGenericMap(tokenServiceUsage);
- out.writeGenericMap(apiKeyServiceUsage);
+ if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_2_0)) {
+ out.writeGenericMap(tokenServiceUsage);
+ out.writeGenericMap(apiKeyServiceUsage);
+ }
out.writeGenericMap(auditUsage);
out.writeGenericMap(ipFilterUsage);
out.writeGenericMap(anonymousUsage);
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java
index 59c16fc8a7a72..8fe018a825468 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java
@@ -59,6 +59,9 @@ public TokensInvalidationResult(StreamInput in) throws IOException {
this.invalidatedTokens = in.readStringCollectionAsList();
this.previouslyInvalidatedTokens = in.readStringCollectionAsList();
this.errors = in.readCollectionAsList(StreamInput::readException);
+ if (in.getTransportVersion().before(TransportVersions.V_7_2_0)) {
+ in.readVInt();
+ }
if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_0_0)) {
this.restStatus = RestStatus.readFrom(in);
}
@@ -108,6 +111,9 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeStringCollection(invalidatedTokens);
out.writeStringCollection(previouslyInvalidatedTokens);
out.writeCollection(errors, StreamOutput::writeException);
+ if (out.getTransportVersion().before(TransportVersions.V_7_2_0)) {
+ out.writeVInt(5);
+ }
if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_0_0)) {
RestStatus.writeTo(out, restStatus);
}
diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java
index b56ea7ae3e456..fef1a98ca67e9 100644
--- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java
+++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java
@@ -327,8 +327,8 @@ public void testInvalidateNotValidAccessTokens() throws Exception {
ResponseException.class,
() -> invalidateAccessToken(
tokenService.prependVersionAndEncodeAccessToken(
- TransportVersions.MINIMUM_COMPATIBLE,
- tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, randomBoolean()).v1()
+ TransportVersions.V_7_3_2,
+ tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, randomBoolean()).v1()
)
)
);
@@ -347,7 +347,7 @@ public void testInvalidateNotValidAccessTokens() throws Exception {
byte[] longerAccessToken = new byte[randomIntBetween(17, 24)];
random().nextBytes(longerAccessToken);
invalidateResponse = invalidateAccessToken(
- tokenService.prependVersionAndEncodeAccessToken(TransportVersions.MINIMUM_COMPATIBLE, longerAccessToken)
+ tokenService.prependVersionAndEncodeAccessToken(TransportVersions.V_7_3_2, longerAccessToken)
);
assertThat(invalidateResponse.invalidated(), equalTo(0));
assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0));
@@ -365,7 +365,7 @@ public void testInvalidateNotValidAccessTokens() throws Exception {
byte[] shorterAccessToken = new byte[randomIntBetween(12, 15)];
random().nextBytes(shorterAccessToken);
invalidateResponse = invalidateAccessToken(
- tokenService.prependVersionAndEncodeAccessToken(TransportVersions.MINIMUM_COMPATIBLE, shorterAccessToken)
+ tokenService.prependVersionAndEncodeAccessToken(TransportVersions.V_7_3_2, shorterAccessToken)
);
assertThat(invalidateResponse.invalidated(), equalTo(0));
assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0));
@@ -394,8 +394,8 @@ public void testInvalidateNotValidAccessTokens() throws Exception {
invalidateResponse = invalidateAccessToken(
tokenService.prependVersionAndEncodeAccessToken(
- TransportVersions.MINIMUM_COMPATIBLE,
- tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, randomBoolean()).v1()
+ TransportVersions.V_7_3_2,
+ tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, randomBoolean()).v1()
)
);
assertThat(invalidateResponse.invalidated(), equalTo(0));
@@ -420,8 +420,8 @@ public void testInvalidateNotValidRefreshTokens() throws Exception {
ResponseException.class,
() -> invalidateRefreshToken(
TokenService.prependVersionAndEncodeRefreshToken(
- TransportVersions.MINIMUM_COMPATIBLE,
- tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, true).v2()
+ TransportVersions.V_7_3_2,
+ tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, true).v2()
)
)
);
@@ -441,7 +441,7 @@ public void testInvalidateNotValidRefreshTokens() throws Exception {
byte[] longerRefreshToken = new byte[randomIntBetween(17, 24)];
random().nextBytes(longerRefreshToken);
invalidateResponse = invalidateRefreshToken(
- TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.MINIMUM_COMPATIBLE, longerRefreshToken)
+ TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.V_7_3_2, longerRefreshToken)
);
assertThat(invalidateResponse.invalidated(), equalTo(0));
assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0));
@@ -459,7 +459,7 @@ public void testInvalidateNotValidRefreshTokens() throws Exception {
byte[] shorterRefreshToken = new byte[randomIntBetween(12, 15)];
random().nextBytes(shorterRefreshToken);
invalidateResponse = invalidateRefreshToken(
- TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.MINIMUM_COMPATIBLE, shorterRefreshToken)
+ TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.V_7_3_2, shorterRefreshToken)
);
assertThat(invalidateResponse.invalidated(), equalTo(0));
assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0));
@@ -488,8 +488,8 @@ public void testInvalidateNotValidRefreshTokens() throws Exception {
invalidateResponse = invalidateRefreshToken(
TokenService.prependVersionAndEncodeRefreshToken(
- TransportVersions.MINIMUM_COMPATIBLE,
- tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, true).v2()
+ TransportVersions.V_7_3_2,
+ tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, true).v2()
)
);
assertThat(invalidateResponse.invalidated(), equalTo(0));
@@ -758,11 +758,18 @@ public void testAuthenticateWithWrongToken() throws Exception {
assertAuthenticateWithToken(response.accessToken(), TEST_USER_NAME);
// Now attempt to authenticate with an invalid access token string
assertUnauthorizedToken(randomAlphaOfLengthBetween(0, 128));
- // Now attempt to authenticate with an invalid access token with valid structure (after 8.0 pre 8.10)
+ // Now attempt to authenticate with an invalid access token with valid structure (pre 7.2)
assertUnauthorizedToken(
tokenService.prependVersionAndEncodeAccessToken(
- TransportVersions.V_8_0_0,
- tokenService.getRandomTokenBytes(TransportVersions.V_8_0_0, randomBoolean()).v1()
+ TransportVersions.V_7_1_0,
+ tokenService.getRandomTokenBytes(TransportVersions.V_7_1_0, randomBoolean()).v1()
+ )
+ );
+ // Now attempt to authenticate with an invalid access token with valid structure (after 7.2 pre 8.10)
+ assertUnauthorizedToken(
+ tokenService.prependVersionAndEncodeAccessToken(
+ TransportVersions.V_7_4_0,
+ tokenService.getRandomTokenBytes(TransportVersions.V_7_4_0, randomBoolean()).v1()
)
);
// Now attempt to authenticate with an invalid access token with valid structure (current version)
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java
index 900436a1fd874..4f7ba7808b823 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java
@@ -48,7 +48,9 @@
import org.elasticsearch.common.cache.CacheBuilder;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.InputStreamStreamInput;
+import org.elasticsearch.common.io.stream.OutputStreamStreamOutput;
import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
@@ -57,6 +59,7 @@
import org.elasticsearch.common.util.concurrent.AbstractRunnable;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.core.Nullable;
+import org.elasticsearch.core.Streams;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.core.Tuple;
@@ -90,8 +93,10 @@
import org.elasticsearch.xpack.security.support.SecurityIndexManager;
import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
+import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
@@ -127,6 +132,7 @@
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
@@ -195,8 +201,14 @@ public class TokenService {
// UUIDs are 16 bytes encoded base64 without padding, therefore the length is (16 / 3) * 4 + ((16 % 3) * 8 + 5) / 6 chars
private static final int TOKEN_LENGTH = 22;
private static final String TOKEN_DOC_ID_PREFIX = TOKEN_DOC_TYPE + "_";
+ static final int LEGACY_MINIMUM_BYTES = VERSION_BYTES + SALT_BYTES + IV_BYTES + 1;
static final int MINIMUM_BYTES = VERSION_BYTES + TOKEN_LENGTH + 1;
+ static final int LEGACY_MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * LEGACY_MINIMUM_BYTES) / 3)).intValue();
public static final int MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * MINIMUM_BYTES) / 3)).intValue();
+ static final TransportVersion VERSION_HASHED_TOKENS = TransportVersions.V_7_2_0;
+ static final TransportVersion VERSION_TOKENS_INDEX_INTRODUCED = TransportVersions.V_7_2_0;
+ static final TransportVersion VERSION_ACCESS_TOKENS_AS_UUIDS = TransportVersions.V_7_2_0;
+ static final TransportVersion VERSION_MULTIPLE_CONCURRENT_REFRESHES = TransportVersions.V_7_2_0;
static final TransportVersion VERSION_CLIENT_AUTH_FOR_REFRESH = TransportVersions.V_8_2_0;
static final TransportVersion VERSION_GET_TOKEN_DOC_FOR_REFRESH = TransportVersions.V_8_10_X;
@@ -261,7 +273,8 @@ public TokenService(
/**
* Creates an access token and optionally a refresh token as well, based on the provided authentication and metadata with
- * auto-generated values. The created tokens are stored a specific security tokens index.
+ * auto-generated values. The created tokens are stored in the security index for versions up to
+ * {@link #VERSION_TOKENS_INDEX_INTRODUCED} and to a specific security tokens index for later versions.
*/
public void createOAuth2Tokens(
Authentication authentication,
@@ -278,7 +291,8 @@ public void createOAuth2Tokens(
/**
* Creates an access token and optionally a refresh token as well from predefined values, based on the provided authentication and
- * metadata. The created tokens are stored in a specific security tokens index.
+ * metadata. The created tokens are stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED} and to a
+ * specific security tokens index for later versions.
*/
// public for testing
public void createOAuth2Tokens(
@@ -300,15 +314,21 @@ public void createOAuth2Tokens(
*
* @param accessTokenBytes The predefined seed value for the access token. This will then be
*
- * - Hashed before stored
- * - Stored in a specific security tokens index
+ * - Encrypted before stored for versions before {@link #VERSION_TOKENS_INDEX_INTRODUCED}
+ * - Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
+ * - Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
+ * - Stored in a specific security tokens index for versions after
+ * {@link #VERSION_TOKENS_INDEX_INTRODUCED}
* - Prepended with a version ID and Base64 encoded before returned to the caller of the APIs
*
* @param refreshTokenBytes The predefined seed value for the access token. This will then be
*
- * - Hashed before stored
- * - Stored in a specific security tokens index
- * - Prepended with a version ID and Base64 encoded before returned to the caller of the APIs
+ * - Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
+ * - Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
+ * - Stored in a specific security tokens index for versions after
+ * {@link #VERSION_TOKENS_INDEX_INTRODUCED}
+ * - Prepended with a version ID and encoded with Base64 before returned to the caller of the APIs
+ * for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
*
* @param tokenVersion The version of the nodes with which these tokens will be compatible.
* @param authentication The authentication object representing the user for which the tokens are created
@@ -364,7 +384,7 @@ private void createOAuth2Tokens(
} else {
refreshTokenToStore = refreshTokenToReturn = null;
}
- } else {
+ } else if (tokenVersion.onOrAfter(VERSION_HASHED_TOKENS)) {
assert accessTokenBytes.length == RAW_TOKEN_BYTES_LENGTH;
userTokenId = hashTokenString(Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes));
accessTokenToStore = null;
@@ -375,6 +395,18 @@ private void createOAuth2Tokens(
} else {
refreshTokenToStore = refreshTokenToReturn = null;
}
+ } else {
+ assert accessTokenBytes.length == RAW_TOKEN_BYTES_LENGTH;
+ userTokenId = Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes);
+ accessTokenToStore = null;
+ if (refreshTokenBytes != null) {
+ assert refreshTokenBytes.length == RAW_TOKEN_BYTES_LENGTH;
+ refreshTokenToStore = refreshTokenToReturn = Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(
+ refreshTokenBytes
+ );
+ } else {
+ refreshTokenToStore = refreshTokenToReturn = null;
+ }
}
UserToken userToken = new UserToken(userTokenId, tokenVersion, tokenAuth, getExpirationTime(), metadata);
tokenDocument = createTokenDocument(userToken, accessTokenToStore, refreshTokenToStore, originatingClientAuth);
@@ -387,22 +419,23 @@ private void createOAuth2Tokens(
final RefreshPolicy tokenCreationRefreshPolicy = tokenVersion.onOrAfter(VERSION_GET_TOKEN_DOC_FOR_REFRESH)
? RefreshPolicy.NONE
: RefreshPolicy.WAIT_UNTIL;
+ final SecurityIndexManager tokensIndex = getTokensIndexForVersion(tokenVersion);
logger.debug(
() -> format(
"Using refresh policy [%s] when creating token doc [%s] in the security index [%s]",
tokenCreationRefreshPolicy,
documentId,
- securityTokensIndex.aliasName()
+ tokensIndex.aliasName()
)
);
- final IndexRequest indexTokenRequest = client.prepareIndex(securityTokensIndex.aliasName())
+ final IndexRequest indexTokenRequest = client.prepareIndex(tokensIndex.aliasName())
.setId(documentId)
.setOpType(OpType.CREATE)
.setSource(tokenDocument, XContentType.JSON)
.setRefreshPolicy(tokenCreationRefreshPolicy)
.request();
- securityTokensIndex.prepareIndexIfNeededThenExecute(
- ex -> listener.onFailure(traceLog("prepare tokens index [" + securityTokensIndex.aliasName() + "]", documentId, ex)),
+ tokensIndex.prepareIndexIfNeededThenExecute(
+ ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", documentId, ex)),
() -> executeAsyncWithOrigin(
client,
SECURITY_ORIGIN,
@@ -521,16 +554,17 @@ private void getTokenDocById(
@Nullable String storedRefreshToken,
ActionListener listener
) {
- final SecurityIndexManager frozenTokensIndex = securityTokensIndex.defensiveCopy();
+ final SecurityIndexManager tokensIndex = getTokensIndexForVersion(tokenVersion);
+ final SecurityIndexManager frozenTokensIndex = tokensIndex.defensiveCopy();
if (frozenTokensIndex.isAvailable(PRIMARY_SHARDS) == false) {
- logger.warn("failed to get access token [{}] because index [{}] is not available", tokenId, securityTokensIndex.aliasName());
+ logger.warn("failed to get access token [{}] because index [{}] is not available", tokenId, tokensIndex.aliasName());
listener.onFailure(frozenTokensIndex.getUnavailableReason(PRIMARY_SHARDS));
return;
}
- final GetRequest getRequest = client.prepareGet(securityTokensIndex.aliasName(), getTokenDocumentId(tokenId)).request();
+ final GetRequest getRequest = client.prepareGet(tokensIndex.aliasName(), getTokenDocumentId(tokenId)).request();
final Consumer onFailure = ex -> listener.onFailure(traceLog("get token from id", tokenId, ex));
- securityTokensIndex.checkIndexVersionThenExecute(
- ex -> listener.onFailure(traceLog("prepare tokens index [" + securityTokensIndex.aliasName() + "]", tokenId, ex)),
+ tokensIndex.checkIndexVersionThenExecute(
+ ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", tokenId, ex)),
() -> executeAsyncWithOrigin(
client.threadPool().getThreadContext(),
SECURITY_ORIGIN,
@@ -576,11 +610,7 @@ private void getTokenDocById(
// if the index or the shard is not there / available we assume that
// the token is not valid
if (isShardNotAvailableException(e)) {
- logger.warn(
- "failed to get token doc [{}] because index [{}] is not available",
- tokenId,
- securityTokensIndex.aliasName()
- );
+ logger.warn("failed to get token doc [{}] because index [{}] is not available", tokenId, tokensIndex.aliasName());
} else {
logger.error(() -> "failed to get token doc [" + tokenId + "]", e);
}
@@ -620,7 +650,7 @@ void decodeToken(String token, boolean validateUserToken, ActionListener VERSION_ACCESS_TOKENS_UUIDS cluster
if (in.available() < MINIMUM_BYTES) {
logger.debug("invalid token, smaller than [{}] bytes", MINIMUM_BYTES);
@@ -630,6 +660,41 @@ void decodeToken(String token, boolean validateUserToken, ActionListener {
+ if (decodeKey != null) {
+ try {
+ final Cipher cipher = getDecryptionCipher(iv, decodeKey, version, decodedSalt);
+ final String tokenId = decryptTokenId(encryptedTokenId, cipher, version);
+ getAndValidateUserToken(tokenId, version, null, validateUserToken, listener);
+ } catch (IOException | GeneralSecurityException e) {
+ // could happen with a token that is not ours
+ logger.warn("invalid token", e);
+ listener.onResponse(null);
+ }
+ } else {
+ // could happen with a token that is not ours
+ listener.onResponse(null);
+ }
+ }, listener::onFailure));
+ } else {
+ logger.debug(() -> format("invalid key %s key: %s", passphraseHash, keyCache.cache.keySet()));
+ listener.onResponse(null);
+ }
}
} catch (Exception e) {
// could happen with a token that is not ours
@@ -787,7 +852,11 @@ private void indexInvalidation(
final Set idsOfOlderTokens = new HashSet<>();
boolean anyOlderTokensBeforeRefreshViaGet = false;
for (UserToken userToken : userTokens) {
- idsOfRecentTokens.add(userToken.getId());
+ if (userToken.getTransportVersion().onOrAfter(VERSION_TOKENS_INDEX_INTRODUCED)) {
+ idsOfRecentTokens.add(userToken.getId());
+ } else {
+ idsOfOlderTokens.add(userToken.getId());
+ }
anyOlderTokensBeforeRefreshViaGet |= userToken.getTransportVersion().before(VERSION_GET_TOKEN_DOC_FOR_REFRESH);
}
final RefreshPolicy tokensInvalidationRefreshPolicy = anyOlderTokensBeforeRefreshViaGet
@@ -1055,7 +1124,7 @@ private void findTokenFromRefreshToken(String refreshToken, Iterator
);
getTokenDocById(userTokenId, version, null, storedRefreshToken, listener);
}
- } else {
+ } else if (version.onOrAfter(VERSION_HASHED_TOKENS)) {
final String unencodedRefreshToken = in.readString();
if (unencodedRefreshToken.length() != TOKEN_LENGTH) {
logger.debug("Decoded refresh token [{}] with version [{}] is invalid.", unencodedRefreshToken, version);
@@ -1064,6 +1133,9 @@ private void findTokenFromRefreshToken(String refreshToken, Iterator
final String hashedRefreshToken = hashTokenString(unencodedRefreshToken);
findTokenFromRefreshToken(hashedRefreshToken, securityTokensIndex, backoff, listener);
}
+ } else {
+ logger.debug("Unrecognized refresh token version [{}].", version);
+ listener.onResponse(null);
}
} catch (IOException e) {
logger.debug(() -> "Could not decode refresh token [" + refreshToken + "].", e);
@@ -1178,6 +1250,7 @@ private void innerRefresh(
return;
}
final RefreshTokenStatus refreshTokenStatus = checkRefreshResult.v1();
+ final SecurityIndexManager refreshedTokenIndex = getTokensIndexForVersion(refreshTokenStatus.getTransportVersion());
if (refreshTokenStatus.isRefreshed()) {
logger.debug(
"Token document [{}] was recently refreshed, when a new token document was generated. Reusing that result.",
@@ -1185,29 +1258,31 @@ private void innerRefresh(
);
final Tuple parsedTokens = parseTokensFromDocument(tokenDoc.sourceAsMap(), null);
Authentication authentication = parsedTokens.v1().getAuthentication();
- decryptAndReturnSupersedingTokens(refreshToken, refreshTokenStatus, securityTokensIndex, authentication, listener);
+ decryptAndReturnSupersedingTokens(refreshToken, refreshTokenStatus, refreshedTokenIndex, authentication, listener);
} else {
final TransportVersion newTokenVersion = getTokenVersionCompatibility();
final Tuple newTokenBytes = getRandomTokenBytes(newTokenVersion, true);
final Map updateMap = new HashMap<>();
updateMap.put("refreshed", true);
- updateMap.put("refresh_time", clock.instant().toEpochMilli());
- try {
- final byte[] iv = getRandomBytes(IV_BYTES);
- final byte[] salt = getRandomBytes(SALT_BYTES);
- String encryptedAccessAndRefreshToken = encryptSupersedingTokens(
- newTokenBytes.v1(),
- newTokenBytes.v2(),
- refreshToken,
- iv,
- salt
- );
- updateMap.put("superseding.encrypted_tokens", encryptedAccessAndRefreshToken);
- updateMap.put("superseding.encryption_iv", Base64.getEncoder().encodeToString(iv));
- updateMap.put("superseding.encryption_salt", Base64.getEncoder().encodeToString(salt));
- } catch (GeneralSecurityException e) {
- logger.warn("could not encrypt access token and refresh token string", e);
- onFailure.accept(invalidGrantException("could not refresh the requested token"));
+ if (newTokenVersion.onOrAfter(VERSION_MULTIPLE_CONCURRENT_REFRESHES)) {
+ updateMap.put("refresh_time", clock.instant().toEpochMilli());
+ try {
+ final byte[] iv = getRandomBytes(IV_BYTES);
+ final byte[] salt = getRandomBytes(SALT_BYTES);
+ String encryptedAccessAndRefreshToken = encryptSupersedingTokens(
+ newTokenBytes.v1(),
+ newTokenBytes.v2(),
+ refreshToken,
+ iv,
+ salt
+ );
+ updateMap.put("superseding.encrypted_tokens", encryptedAccessAndRefreshToken);
+ updateMap.put("superseding.encryption_iv", Base64.getEncoder().encodeToString(iv));
+ updateMap.put("superseding.encryption_salt", Base64.getEncoder().encodeToString(salt));
+ } catch (GeneralSecurityException e) {
+ logger.warn("could not encrypt access token and refresh token string", e);
+ onFailure.accept(invalidGrantException("could not refresh the requested token"));
+ }
}
assert tokenDoc.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO : "expected an assigned sequence number";
assert tokenDoc.primaryTerm() != SequenceNumbers.UNASSIGNED_PRIMARY_TERM : "expected an assigned primary term";
@@ -1218,17 +1293,17 @@ private void innerRefresh(
"Using refresh policy [%s] when updating token doc [%s] for refresh in the security index [%s]",
tokenRefreshUpdateRefreshPolicy,
tokenDoc.id(),
- securityTokensIndex.aliasName()
+ refreshedTokenIndex.aliasName()
)
);
- final UpdateRequestBuilder updateRequest = client.prepareUpdate(securityTokensIndex.aliasName(), tokenDoc.id())
+ final UpdateRequestBuilder updateRequest = client.prepareUpdate(refreshedTokenIndex.aliasName(), tokenDoc.id())
.setDoc("refresh_token", updateMap)
.setFetchSource(logger.isDebugEnabled())
.setRefreshPolicy(tokenRefreshUpdateRefreshPolicy)
.setIfSeqNo(tokenDoc.seqNo())
.setIfPrimaryTerm(tokenDoc.primaryTerm());
- securityTokensIndex.prepareIndexIfNeededThenExecute(
- ex -> listener.onFailure(traceLog("prepare index [" + securityTokensIndex.aliasName() + "]", ex)),
+ refreshedTokenIndex.prepareIndexIfNeededThenExecute(
+ ex -> listener.onFailure(traceLog("prepare index [" + refreshedTokenIndex.aliasName() + "]", ex)),
() -> executeAsyncWithOrigin(
client.threadPool().getThreadContext(),
SECURITY_ORIGIN,
@@ -1274,7 +1349,7 @@ private void innerRefresh(
if (cause instanceof VersionConflictEngineException) {
// The document has been updated by another thread, get it again.
logger.debug("version conflict while updating document [{}], attempting to get it again", tokenDoc.id());
- getTokenDocAsync(tokenDoc.id(), securityTokensIndex, true, new ActionListener<>() {
+ getTokenDocAsync(tokenDoc.id(), refreshedTokenIndex, true, new ActionListener<>() {
@Override
public void onResponse(GetResponse response) {
if (response.isExists()) {
@@ -1293,7 +1368,7 @@ public void onFailure(Exception e) {
logger.info("could not get token document [{}] for refresh, retrying", tokenDoc.id());
client.threadPool()
.schedule(
- () -> getTokenDocAsync(tokenDoc.id(), securityTokensIndex, true, this),
+ () -> getTokenDocAsync(tokenDoc.id(), refreshedTokenIndex, true, this),
backoff.next(),
client.threadPool().generic()
);
@@ -1614,13 +1689,17 @@ private static Optional checkMultipleRefreshes(
RefreshTokenStatus refreshTokenStatus
) {
if (refreshTokenStatus.isRefreshed()) {
- if (refreshRequested.isAfter(refreshTokenStatus.getRefreshInstant().plus(30L, ChronoUnit.SECONDS))) {
- return Optional.of(invalidGrantException("token has already been refreshed more than 30 seconds in the past"));
- }
- if (refreshRequested.isBefore(refreshTokenStatus.getRefreshInstant().minus(30L, ChronoUnit.SECONDS))) {
- return Optional.of(
- invalidGrantException("token has been refreshed more than 30 seconds in the future, clock skew too great")
- );
+ if (refreshTokenStatus.getTransportVersion().onOrAfter(VERSION_MULTIPLE_CONCURRENT_REFRESHES)) {
+ if (refreshRequested.isAfter(refreshTokenStatus.getRefreshInstant().plus(30L, ChronoUnit.SECONDS))) {
+ return Optional.of(invalidGrantException("token has already been refreshed more than 30 seconds in the past"));
+ }
+ if (refreshRequested.isBefore(refreshTokenStatus.getRefreshInstant().minus(30L, ChronoUnit.SECONDS))) {
+ return Optional.of(
+ invalidGrantException("token has been refreshed more than 30 seconds in the future, clock skew too great")
+ );
+ }
+ } else {
+ return Optional.of(invalidGrantException("token has already been refreshed"));
}
}
return Optional.empty();
@@ -1900,6 +1979,21 @@ private void ensureEnabled() {
}
}
+ /**
+ * In version {@code #VERSION_TOKENS_INDEX_INTRODUCED} security tokens were moved into a separate index, away from the other entities in
+ * the main security index, due to their ephemeral nature. They moved "seamlessly" - without manual user intervention. In this way, new
+ * tokens are created in the new index, while the existing ones were left in place - to be accessed from the old index - and due to be
+ * removed automatically by the {@code ExpiredTokenRemover} periodic job. Therefore, in general, when searching for a token we need to
+ * consider both the new and the old indices.
+ */
+ private SecurityIndexManager getTokensIndexForVersion(TransportVersion version) {
+ if (version.onOrAfter(VERSION_TOKENS_INDEX_INTRODUCED)) {
+ return securityTokensIndex;
+ } else {
+ return securityMainIndex;
+ }
+ }
+
public TimeValue getExpirationDelay() {
return expirationDelay;
}
@@ -1928,13 +2022,41 @@ public String prependVersionAndEncodeAccessToken(TransportVersion version, byte[
out.writeByteArray(accessTokenBytes);
return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes);
}
- } else {
+ } else if (version.onOrAfter(VERSION_ACCESS_TOKENS_AS_UUIDS)) {
try (BytesStreamOutput out = new BytesStreamOutput(MINIMUM_BASE64_BYTES)) {
out.setTransportVersion(version);
TransportVersion.writeVersion(version, out);
out.writeString(Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes));
return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes);
}
+ } else {
+ // we know that the minimum length is larger than the default of the ByteArrayOutputStream so set the size to this explicitly
+ try (
+ ByteArrayOutputStream os = new ByteArrayOutputStream(LEGACY_MINIMUM_BASE64_BYTES);
+ OutputStream base64 = Base64.getEncoder().wrap(os);
+ StreamOutput out = new OutputStreamStreamOutput(base64)
+ ) {
+ out.setTransportVersion(version);
+ KeyAndCache keyAndCache = keyCache.activeKeyCache;
+ TransportVersion.writeVersion(version, out);
+ out.writeByteArray(keyAndCache.getSalt().bytes);
+ out.writeByteArray(keyAndCache.getKeyHash().bytes);
+ final byte[] initializationVector = getRandomBytes(IV_BYTES);
+ out.writeByteArray(initializationVector);
+ try (
+ CipherOutputStream encryptedOutput = new CipherOutputStream(
+ out,
+ getEncryptionCipher(initializationVector, keyAndCache, version)
+ );
+ StreamOutput encryptedStreamOutput = new OutputStreamStreamOutput(encryptedOutput)
+ ) {
+ encryptedStreamOutput.setTransportVersion(version);
+ encryptedStreamOutput.writeString(Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes));
+ // StreamOutput needs to be closed explicitly because it wraps CipherOutputStream
+ encryptedStreamOutput.close();
+ return new String(os.toByteArray(), StandardCharsets.UTF_8);
+ }
+ }
}
}
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java
index 702af75141093..75c2507a1dc5f 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java
@@ -126,6 +126,7 @@
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -147,6 +148,7 @@ public class TokenServiceTests extends ESTestCase {
private SecurityIndexManager securityMainIndex;
private SecurityIndexManager securityTokensIndex;
private ClusterService clusterService;
+ private DiscoveryNode pre72OldNode;
private DiscoveryNode pre8500040OldNode;
private Settings tokenServiceEnabledSettings = Settings.builder()
.put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true)
@@ -226,12 +228,31 @@ public void setupClient() {
licenseState = mock(MockLicenseState.class);
when(licenseState.isAllowed(Security.TOKEN_SERVICE_FEATURE)).thenReturn(true);
+ if (randomBoolean()) {
+ // version 7.2 was an "inflection" point in the Token Service development (access_tokens as UUIDS, multiple concurrent
+ // refreshes,
+ // tokens docs on a separate index)
+ pre72OldNode = addAnother7071DataNode(this.clusterService);
+ }
if (randomBoolean()) {
// before refresh tokens used GET, i.e. TokenService#VERSION_GET_TOKEN_DOC_FOR_REFRESH
pre8500040OldNode = addAnotherPre8500DataNode(this.clusterService);
}
}
+ private static DiscoveryNode addAnother7071DataNode(ClusterService clusterService) {
+ Version version;
+ TransportVersion transportVersion;
+ if (randomBoolean()) {
+ version = Version.V_7_0_0;
+ transportVersion = TransportVersions.V_7_0_0;
+ } else {
+ version = Version.V_7_1_0;
+ transportVersion = TransportVersions.V_7_1_0;
+ }
+ return addAnotherDataNodeWithVersion(clusterService, version, transportVersion);
+ }
+
private static DiscoveryNode addAnotherPre8500DataNode(ClusterService clusterService) {
Version version;
TransportVersion transportVersion;
@@ -280,6 +301,53 @@ public static void shutdownThreadpool() {
threadPool = null;
}
+ public void testAttachAndGetToken() throws Exception {
+ TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC());
+ // This test only makes sense in mixed clusters with pre v7.2.0 nodes where the Token Service Key is used (to encrypt tokens)
+ if (null == pre72OldNode) {
+ pre72OldNode = addAnother7071DataNode(this.clusterService);
+ }
+ Authentication authentication = AuthenticationTestHelper.builder()
+ .user(new User("joe", "admin"))
+ .realmRef(new RealmRef("native_realm", "native", "node1"))
+ .build(false);
+ PlainActionFuture tokenFuture = new PlainActionFuture<>();
+ Tuple newTokenBytes = tokenService.getRandomTokenBytes(randomBoolean());
+ tokenService.createOAuth2Tokens(
+ newTokenBytes.v1(),
+ newTokenBytes.v2(),
+ authentication,
+ authentication,
+ Collections.emptyMap(),
+ tokenFuture
+ );
+ final String accessToken = tokenFuture.get().getAccessToken();
+ assertNotNull(accessToken);
+ mockGetTokenFromAccessTokenBytes(tokenService, newTokenBytes.v1(), authentication, false, null);
+
+ ThreadContext requestContext = new ThreadContext(Settings.EMPTY);
+ requestContext.putHeader("Authorization", randomFrom("Bearer ", "BEARER ", "bearer ") + accessToken);
+
+ try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) {
+ PlainActionFuture future = new PlainActionFuture<>();
+ final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext);
+ tokenService.tryAuthenticateToken(bearerToken, future);
+ UserToken serialized = future.get();
+ assertAuthentication(authentication, serialized.getAuthentication());
+ }
+
+ try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) {
+ // verify a second separate token service with its own salt can also verify
+ TokenService anotherService = createTokenService(tokenServiceEnabledSettings, systemUTC());
+ anotherService.refreshMetadata(tokenService.getTokenMetadata());
+ PlainActionFuture future = new PlainActionFuture<>();
+ final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext);
+ anotherService.tryAuthenticateToken(bearerToken, future);
+ UserToken fromOtherService = future.get();
+ assertAuthentication(authentication, fromOtherService.getAuthentication());
+ }
+ }
+
public void testInvalidAuthorizationHeader() throws Exception {
TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC());
ThreadContext requestContext = new ThreadContext(Settings.EMPTY);
@@ -296,6 +364,89 @@ public void testInvalidAuthorizationHeader() throws Exception {
}
}
+ public void testPassphraseWorks() throws Exception {
+ TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC());
+ // This test only makes sense in mixed clusters with pre v7.1.0 nodes where the Key is actually used
+ if (null == pre72OldNode) {
+ pre72OldNode = addAnother7071DataNode(this.clusterService);
+ }
+ Authentication authentication = AuthenticationTestHelper.builder()
+ .user(new User("joe", "admin"))
+ .realmRef(new RealmRef("native_realm", "native", "node1"))
+ .build(false);
+ PlainActionFuture tokenFuture = new PlainActionFuture<>();
+ Tuple newTokenBytes = tokenService.getRandomTokenBytes(randomBoolean());
+ tokenService.createOAuth2Tokens(
+ newTokenBytes.v1(),
+ newTokenBytes.v2(),
+ authentication,
+ authentication,
+ Collections.emptyMap(),
+ tokenFuture
+ );
+ final String accessToken = tokenFuture.get().getAccessToken();
+ assertNotNull(accessToken);
+ mockGetTokenFromAccessTokenBytes(tokenService, newTokenBytes.v1(), authentication, false, null);
+
+ ThreadContext requestContext = new ThreadContext(Settings.EMPTY);
+ storeTokenHeader(requestContext, accessToken);
+
+ try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) {
+ PlainActionFuture future = new PlainActionFuture<>();
+ final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext);
+ tokenService.tryAuthenticateToken(bearerToken, future);
+ UserToken serialized = future.get();
+ assertAuthentication(authentication, serialized.getAuthentication());
+ }
+
+ try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) {
+ // verify a second separate token service with its own passphrase cannot verify
+ TokenService anotherService = createTokenService(tokenServiceEnabledSettings, systemUTC());
+ PlainActionFuture future = new PlainActionFuture<>();
+ final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext);
+ anotherService.tryAuthenticateToken(bearerToken, future);
+ assertNull(future.get());
+ }
+ }
+
+ public void testGetTokenWhenKeyCacheHasExpired() throws Exception {
+ TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC());
+ // This test only makes sense in mixed clusters with pre v7.1.0 nodes where the Key is actually used
+ if (null == pre72OldNode) {
+ pre72OldNode = addAnother7071DataNode(this.clusterService);
+ }
+ Authentication authentication = AuthenticationTestHelper.builder()
+ .user(new User("joe", "admin"))
+ .realmRef(new RealmRef("native_realm", "native", "node1"))
+ .build(false);
+
+ PlainActionFuture tokenFuture = new PlainActionFuture<>();
+ Tuple newTokenBytes = tokenService.getRandomTokenBytes(randomBoolean());
+ tokenService.createOAuth2Tokens(
+ newTokenBytes.v1(),
+ newTokenBytes.v2(),
+ authentication,
+ authentication,
+ Collections.emptyMap(),
+ tokenFuture
+ );
+ String accessToken = tokenFuture.get().getAccessToken();
+ assertThat(accessToken, notNullValue());
+
+ tokenService.clearActiveKeyCache();
+
+ tokenService.createOAuth2Tokens(
+ newTokenBytes.v1(),
+ newTokenBytes.v2(),
+ authentication,
+ authentication,
+ Collections.emptyMap(),
+ tokenFuture
+ );
+ accessToken = tokenFuture.get().getAccessToken();
+ assertThat(accessToken, notNullValue());
+ }
+
public void testAuthnWithInvalidatedToken() throws Exception {
when(securityMainIndex.indexExists()).thenReturn(true);
TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC());
@@ -669,6 +820,57 @@ public void testMalformedRefreshTokens() throws Exception {
}
}
+ public void testNonExistingPre72Token() throws Exception {
+ TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC());
+ // mock another random token so that we don't find a token in TokenService#getUserTokenFromId
+ Authentication authentication = AuthenticationTestHelper.builder()
+ .user(new User("joe", "admin"))
+ .realmRef(new RealmRef("native_realm", "native", "node1"))
+ .build(false);
+ mockGetTokenFromAccessTokenBytes(tokenService, tokenService.getRandomTokenBytes(randomBoolean()).v1(), authentication, false, null);
+ ThreadContext requestContext = new ThreadContext(Settings.EMPTY);
+ storeTokenHeader(
+ requestContext,
+ tokenService.prependVersionAndEncodeAccessToken(
+ TransportVersions.V_7_1_0,
+ tokenService.getRandomTokenBytes(TransportVersions.V_7_1_0, randomBoolean()).v1()
+ )
+ );
+
+ try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) {
+ PlainActionFuture future = new PlainActionFuture<>();
+ final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext);
+ tokenService.tryAuthenticateToken(bearerToken, future);
+ assertNull(future.get());
+ }
+ }
+
+ public void testNonExistingUUIDToken() throws Exception {
+ TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC());
+ // mock another random token so that we don't find a token in TokenService#getUserTokenFromId
+ Authentication authentication = AuthenticationTestHelper.builder()
+ .user(new User("joe", "admin"))
+ .realmRef(new RealmRef("native_realm", "native", "node1"))
+ .build(false);
+ mockGetTokenFromAccessTokenBytes(tokenService, tokenService.getRandomTokenBytes(randomBoolean()).v1(), authentication, false, null);
+ ThreadContext requestContext = new ThreadContext(Settings.EMPTY);
+ TransportVersion uuidTokenVersion = randomFrom(TransportVersions.V_7_2_0, TransportVersions.V_7_3_2);
+ storeTokenHeader(
+ requestContext,
+ tokenService.prependVersionAndEncodeAccessToken(
+ uuidTokenVersion,
+ tokenService.getRandomTokenBytes(uuidTokenVersion, randomBoolean()).v1()
+ )
+ );
+
+ try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) {
+ PlainActionFuture future = new PlainActionFuture<>();
+ final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext);
+ tokenService.tryAuthenticateToken(bearerToken, future);
+ assertNull(future.get());
+ }
+ }
+
public void testNonExistingLatestTokenVersion() throws Exception {
TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC());
// mock another random token so that we don't find a token in TokenService#getUserTokenFromId
@@ -723,11 +925,18 @@ public void testIndexNotAvailable() throws Exception {
return Void.TYPE;
}).when(client).get(any(GetRequest.class), anyActionListener());
- final SecurityIndexManager tokensIndex = securityTokensIndex;
- when(securityMainIndex.isAvailable(SecurityIndexManager.Availability.PRIMARY_SHARDS)).thenReturn(false);
- when(securityMainIndex.indexExists()).thenReturn(false);
- when(securityMainIndex.defensiveCopy()).thenReturn(securityMainIndex);
-
+ final SecurityIndexManager tokensIndex;
+ if (pre72OldNode != null) {
+ tokensIndex = securityMainIndex;
+ when(securityTokensIndex.isAvailable(SecurityIndexManager.Availability.PRIMARY_SHARDS)).thenReturn(false);
+ when(securityTokensIndex.indexExists()).thenReturn(false);
+ when(securityTokensIndex.defensiveCopy()).thenReturn(securityTokensIndex);
+ } else {
+ tokensIndex = securityTokensIndex;
+ when(securityMainIndex.isAvailable(SecurityIndexManager.Availability.PRIMARY_SHARDS)).thenReturn(false);
+ when(securityMainIndex.indexExists()).thenReturn(false);
+ when(securityMainIndex.defensiveCopy()).thenReturn(securityMainIndex);
+ }
try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) {
PlainActionFuture future = new PlainActionFuture<>();
final SecureString bearerToken3 = Authenticator.extractBearerTokenFromHeader(requestContext);
@@ -779,6 +988,7 @@ public void testGetAuthenticationWorksWithExpiredUserToken() throws Exception {
}
public void testSupersedingTokenEncryption() throws Exception {
+ assumeTrue("Superseding tokens are only created in post 7.2 clusters", pre72OldNode == null);
TokenService tokenService = createTokenService(tokenServiceEnabledSettings, Clock.systemUTC());
Authentication authentication = AuthenticationTests.randomAuthentication(null, null);
PlainActionFuture tokenFuture = new PlainActionFuture<>();
@@ -813,11 +1023,13 @@ public void testSupersedingTokenEncryption() throws Exception {
authentication,
tokenFuture
);
-
- assertThat(
- tokenService.prependVersionAndEncodeAccessToken(version, newTokenBytes.v1()),
- equalTo(tokenFuture.get().getAccessToken())
- );
+ if (version.onOrAfter(TokenService.VERSION_ACCESS_TOKENS_AS_UUIDS)) {
+ // previous versions serialized the access token encrypted and the cipher text was different each time (due to different IVs)
+ assertThat(
+ tokenService.prependVersionAndEncodeAccessToken(version, newTokenBytes.v1()),
+ equalTo(tokenFuture.get().getAccessToken())
+ );
+ }
assertThat(
TokenService.prependVersionAndEncodeRefreshToken(version, newTokenBytes.v2()),
equalTo(tokenFuture.get().getRefreshToken())
@@ -946,8 +1158,10 @@ public static String tokenDocIdFromAccessTokenBytes(byte[] accessTokenBytes, Tra
MessageDigest userTokenIdDigest = sha256();
userTokenIdDigest.update(accessTokenBytes, RAW_TOKEN_BYTES_LENGTH, RAW_TOKEN_DOC_ID_BYTES_LENGTH);
return Base64.getUrlEncoder().withoutPadding().encodeToString(userTokenIdDigest.digest());
- } else {
+ } else if (tokenVersion.onOrAfter(TokenService.VERSION_ACCESS_TOKENS_AS_UUIDS)) {
return TokenService.hashTokenString(Base64.getUrlEncoder().withoutPadding().encodeToString(accessTokenBytes));
+ } else {
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(accessTokenBytes);
}
}
@@ -964,9 +1178,12 @@ private void mockTokenForRefreshToken(
if (userToken.getTransportVersion().onOrAfter(VERSION_GET_TOKEN_DOC_FOR_REFRESH)) {
storedAccessToken = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256().digest(accessTokenBytes));
storedRefreshToken = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256().digest(refreshTokenBytes));
- } else {
+ } else if (userToken.getTransportVersion().onOrAfter(TokenService.VERSION_HASHED_TOKENS)) {
storedAccessToken = null;
storedRefreshToken = TokenService.hashTokenString(Base64.getUrlEncoder().withoutPadding().encodeToString(refreshTokenBytes));
+ } else {
+ storedAccessToken = null;
+ storedRefreshToken = Base64.getUrlEncoder().withoutPadding().encodeToString(refreshTokenBytes);
}
final RealmRef realmRef = new RealmRef(
refreshTokenStatus == null ? randomAlphaOfLength(6) : refreshTokenStatus.getAssociatedRealm(),