Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
package org.elasticsearch.xpack.security.authc;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
import org.apache.lucene.util.UnicodeUtil;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.TransportVersion;
import org.elasticsearch.TransportVersions;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.DocWriteRequest.OpType;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.DocWriteResponse.Result;
import org.elasticsearch.action.bulk.BackoffPolicy;
import org.elasticsearch.action.bulk.BulkItemResponse;
import org.elasticsearch.action.bulk.BulkRequestBuilder;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.TransportIndexAction;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.ContextPreservingActionListener;
import org.elasticsearch.action.support.TransportActions;
import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateRequestBuilder;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.internal.Client;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ClusterStateUpdateTask;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Priority;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.cache.Cache;
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;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.Maps;
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;
import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.index.engine.VersionConflictEngineException;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.seqno.SequenceNumbers;
import org.elasticsearch.indices.IndexClosedException;
import org.elasticsearch.license.LicenseUtils;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentFactory;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.core.XPackField;
import org.elasticsearch.xpack.core.XPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.ScrollHelper;
import org.elasticsearch.xpack.core.security.SecurityContext;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.KeyAndTimestamp;
import org.elasticsearch.xpack.core.security.authc.TokenMetadata;
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
import org.elasticsearch.xpack.core.security.authc.support.TokensInvalidationResult;
import org.elasticsearch.xpack.security.Security;
import org.elasticsearch.xpack.security.support.FeatureNotEnabledException;
import org.elasticsearch.xpack.security.support.FeatureNotEnabledException.Feature;
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;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.time.Clock;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import static org.elasticsearch.action.support.TransportActions.isShardNotAvailableException;
import static org.elasticsearch.common.hash.MessageDigests.sha256;
import static org.elasticsearch.core.Strings.format;
import static org.elasticsearch.gateway.GatewayService.STATE_NOT_RECOVERED_BLOCK;
import static org.elasticsearch.search.SearchService.DEFAULT_KEEPALIVE_SETTING;
import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN;
import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin;
import static org.elasticsearch.xpack.security.support.SecurityIndexManager.Availability.PRIMARY_SHARDS;
import static org.elasticsearch.xpack.security.support.SecurityIndexManager.Availability.SEARCH_SHARDS;
/**
* Service responsible for the creation, validation, and other management of {@link UserToken}
* objects for authentication
*/
public class TokenService {
/**
* The parameters below are used to generate the cryptographic key that is used to encrypt the
* values returned by this service. These parameters are based off of the
* OWASP Password Storage
* Cheat Sheet and the
* NIST Digital Identity Guidelines
*/
static final int TOKEN_SERVICE_KEY_ITERATIONS = 100000;
static final int TOKENS_ENCRYPTION_KEY_ITERATIONS = 1024;
private static final String KDF_ALGORITHM = "PBKDF2withHMACSHA512";
static final int SALT_BYTES = 32;
private static final int KEY_BYTES = 64;
static final int IV_BYTES = 12;
private static final int VERSION_BYTES = 4;
private static final String ENCRYPTION_CIPHER = "AES/GCM/NoPadding";
private static final String EXPIRED_TOKEN_WWW_AUTH_VALUE = String.format(Locale.ROOT, """
Bearer realm="%s", error="invalid_token", error_description="The access token expired\"""", XPackField.SECURITY);
private static final BackoffPolicy DEFAULT_BACKOFF = BackoffPolicy.exponentialBackoff();
public static final String THREAD_POOL_NAME = XPackField.SECURITY + "-token-key";
public static final Setting TOKEN_EXPIRATION = Setting.timeSetting(
"xpack.security.authc.token.timeout",
TimeValue.timeValueMinutes(20L),
TimeValue.timeValueSeconds(1L),
TimeValue.timeValueHours(1L),
Property.NodeScope
);
public static final Setting DELETE_INTERVAL = Setting.timeSetting(
"xpack.security.authc.token.delete.interval",
TimeValue.timeValueMinutes(30L),
Property.NodeScope
);
public static final Setting DELETE_TIMEOUT = Setting.timeSetting(
"xpack.security.authc.token.delete.timeout",
TimeValue.MINUS_ONE,
Property.NodeScope
);
static final String TOKEN_DOC_TYPE = "token";
static final int RAW_TOKEN_BYTES_LENGTH = 16;
static final int RAW_TOKEN_DOC_ID_BYTES_LENGTH = 8;
static final int RAW_TOKEN_BYTES_TOTAL_LENGTH = RAW_TOKEN_BYTES_LENGTH + RAW_TOKEN_DOC_ID_BYTES_LENGTH;
// 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;
private static final Logger logger = LogManager.getLogger(TokenService.class);
private final SecureRandom secureRandom = new SecureRandom();
private final Settings settings;
private final ClusterService clusterService;
private final Clock clock;
private final TimeValue expirationDelay;
private final TimeValue deleteInterval;
private final Client client;
private final SecurityIndexManager securityMainIndex;
private final SecurityIndexManager securityTokensIndex;
private final ExpiredTokenRemover expiredTokenRemover;
private final boolean enabled;
private final XPackLicenseState licenseState;
private final SecurityContext securityContext;
private volatile TokenKeys keyCache;
private volatile long lastExpirationRunMs;
private final AtomicLong createdTimeStamps = new AtomicLong(-1);
/**
* Creates a new token service
*/
@SuppressWarnings("this-escape")
public TokenService(
Settings settings,
Clock clock,
Client client,
XPackLicenseState licenseState,
SecurityContext securityContext,
SecurityIndexManager securityMainIndex,
SecurityIndexManager securityTokensIndex,
ClusterService clusterService
) throws GeneralSecurityException {
byte[] saltArr = new byte[SALT_BYTES];
secureRandom.nextBytes(saltArr);
final SecureString tokenPassphrase = generateTokenKey();
this.settings = settings;
this.clock = clock.withZone(ZoneOffset.UTC);
this.expirationDelay = TOKEN_EXPIRATION.get(settings);
this.client = client;
this.licenseState = licenseState;
this.securityContext = securityContext;
this.securityMainIndex = securityMainIndex;
this.securityTokensIndex = securityTokensIndex;
this.lastExpirationRunMs = client.threadPool().relativeTimeInMillis();
this.deleteInterval = DELETE_INTERVAL.get(settings);
this.enabled = isTokenServiceEnabled(settings);
this.expiredTokenRemover = new ExpiredTokenRemover(settings, client, this.securityMainIndex, securityTokensIndex);
ensureEncryptionCiphersSupported();
KeyAndCache keyAndCache = new KeyAndCache(
new KeyAndTimestamp(tokenPassphrase, createdTimeStamps.incrementAndGet()),
new BytesKey(saltArr)
);
keyCache = new TokenKeys(Collections.singletonMap(keyAndCache.getKeyHash(), keyAndCache), keyAndCache.getKeyHash());
this.clusterService = clusterService;
initialize(clusterService);
getTokenMetadata();
}
/**
* 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 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,
Authentication originatingClientAuth,
Map metadata,
boolean includeRefreshToken,
ActionListener listener
) {
// the created token is compatible with the oldest node version in the cluster
final TransportVersion tokenVersion = getTokenVersionCompatibility();
Tuple newTokenBytes = getRandomTokenBytes(tokenVersion, includeRefreshToken);
createOAuth2Tokens(newTokenBytes.v1(), newTokenBytes.v2(), tokenVersion, authentication, originatingClientAuth, metadata, listener);
}
/**
* 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 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(
byte[] accessTokenBytes,
@Nullable byte[] refreshTokenBytes,
Authentication authentication,
Authentication originatingClientAuth,
Map metadata,
ActionListener listener
) {
// the created token is compatible with the oldest node version in the cluster
final TransportVersion tokenVersion = getTokenVersionCompatibility();
createOAuth2Tokens(accessTokenBytes, refreshTokenBytes, tokenVersion, authentication, originatingClientAuth, metadata, listener);
}
/**
* Create an access token and optionally a refresh token as well from predefined values, based on the provided authentication and
* metadata.
*
* @param accessTokenBytes The predefined seed value for the access token. This will then be
*
*
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 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
* @param originatingClientAuth The authentication object representing the client that called the related API
* @param metadata A map with metadata to be stored in the token document
* @param listener The listener to call upon completion with a {@link CreateTokenResult} containing the
* serialized access token, serialized refresh token and authentication for which the token is created
* as these will be returned to the client
*/
private void createOAuth2Tokens(
byte[] accessTokenBytes,
@Nullable byte[] refreshTokenBytes,
TransportVersion tokenVersion,
Authentication authentication,
Authentication originatingClientAuth,
Map metadata,
ActionListener listener
) {
ensureEnabled();
if (authentication == null) {
listener.onFailure(traceLog("create token", new IllegalArgumentException("authentication must be provided")));
} else if (originatingClientAuth == null) {
listener.onFailure(
traceLog("create token", new IllegalArgumentException("originating client authentication must be provided"))
);
} else {
final Authentication tokenAuth = authentication.token().maybeRewriteForOlderVersion(tokenVersion);
final String accessTokenToStore;
final String refreshTokenToStore;
final String refreshTokenToReturn;
final String documentId;
final BytesReference tokenDocument;
try {
final String userTokenId;
if (tokenVersion.onOrAfter(VERSION_GET_TOKEN_DOC_FOR_REFRESH)) {
assert accessTokenBytes.length == RAW_TOKEN_BYTES_TOTAL_LENGTH;
MessageDigest userTokenIdDigest = sha256();
userTokenIdDigest.update(accessTokenBytes, RAW_TOKEN_BYTES_LENGTH, RAW_TOKEN_DOC_ID_BYTES_LENGTH);
userTokenId = Base64.getUrlEncoder().withoutPadding().encodeToString(userTokenIdDigest.digest());
accessTokenToStore = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256().digest(accessTokenBytes));
if (refreshTokenBytes != null) {
assert refreshTokenBytes.length == RAW_TOKEN_BYTES_TOTAL_LENGTH;
assert Arrays.equals(
refreshTokenBytes,
RAW_TOKEN_BYTES_LENGTH,
RAW_TOKEN_BYTES_TOTAL_LENGTH,
accessTokenBytes,
RAW_TOKEN_BYTES_LENGTH,
RAW_TOKEN_BYTES_TOTAL_LENGTH
) : "the last bytes of paired access and refresh tokens should be the same";
refreshTokenToStore = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256().digest(refreshTokenBytes));
refreshTokenToReturn = prependVersionAndEncodeRefreshToken(tokenVersion, refreshTokenBytes);
} else {
refreshTokenToStore = refreshTokenToReturn = null;
}
} else if (tokenVersion.onOrAfter(VERSION_HASHED_TOKENS)) {
assert accessTokenBytes.length == RAW_TOKEN_BYTES_LENGTH;
userTokenId = hashTokenString(Base64.getUrlEncoder().withoutPadding().encodeToString(accessTokenBytes));
accessTokenToStore = null;
if (refreshTokenBytes != null) {
assert refreshTokenBytes.length == RAW_TOKEN_BYTES_LENGTH;
refreshTokenToStore = hashTokenString(Base64.getUrlEncoder().withoutPadding().encodeToString(refreshTokenBytes));
refreshTokenToReturn = prependVersionAndEncodeRefreshToken(tokenVersion, refreshTokenBytes);
} else {
refreshTokenToStore = refreshTokenToReturn = null;
}
} else {
assert accessTokenBytes.length == RAW_TOKEN_BYTES_LENGTH;
userTokenId = Base64.getUrlEncoder().withoutPadding().encodeToString(accessTokenBytes);
accessTokenToStore = null;
if (refreshTokenBytes != null) {
assert refreshTokenBytes.length == RAW_TOKEN_BYTES_LENGTH;
refreshTokenToStore = refreshTokenToReturn = Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(refreshTokenBytes);
} else {
refreshTokenToStore = refreshTokenToReturn = null;
}
}
UserToken userToken = new UserToken(userTokenId, tokenVersion, tokenAuth, getExpirationTime(), metadata);
tokenDocument = createTokenDocument(userToken, accessTokenToStore, refreshTokenToStore, originatingClientAuth);
documentId = getTokenDocumentId(userToken);
} catch (IOException e) {
logger.error("Could not encode access or refresh token", e);
listener.onFailure(traceLog("create token", e));
return;
}
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,
tokensIndex.aliasName()
)
);
final IndexRequest indexTokenRequest = client.prepareIndex(tokensIndex.aliasName())
.setId(documentId)
.setOpType(OpType.CREATE)
.setSource(tokenDocument, XContentType.JSON)
.setRefreshPolicy(tokenCreationRefreshPolicy)
.request();
tokensIndex.prepareIndexIfNeededThenExecute(
ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", documentId, ex)),
() -> executeAsyncWithOrigin(
client,
SECURITY_ORIGIN,
TransportIndexAction.TYPE,
indexTokenRequest,
ActionListener.wrap(indexResponse -> {
if (indexResponse.getResult() == Result.CREATED) {
String accessTokenToReturn = prependVersionAndEncodeAccessToken(tokenVersion, accessTokenBytes);
listener.onResponse(new CreateTokenResult(accessTokenToReturn, refreshTokenToReturn, authentication));
} else {
listener.onFailure(
traceLog("create token", new ElasticsearchException("failed to create token document [{}]", indexResponse))
);
}
}, listener::onFailure)
)
);
}
}
/**
* Hashes an access or refresh token String so that it can safely be persisted in the index. We don't salt
* the values as these are v4 UUIDs that have enough entropy by themselves.
*/
// public for testing
public static String hashTokenString(String accessTokenString) {
return new String(Hasher.SHA256.hash(new SecureString(accessTokenString.toCharArray())));
}
/**
* If the token is non-null, then it is validated, which might include authenticated decryption and
* verification that the token has not been revoked or is expired.
*/
void tryAuthenticateToken(SecureString token, ActionListener listener) {
if (shouldTryRealm() && token != null) {
decodeToken(token.toString(), true, listener.delegateResponse((l, e) -> {
if (isShardNotAvailableException(e)) {
l.onResponse(null);
} else {
l.onFailure(e);
}
}));
} else {
listener.onResponse(null);
}
}
/**
* Reads the authentication and metadata from the given token.
* This method does not validate whether the token is expired or not.
*/
public void getAuthenticationAndMetadata(String token, ActionListener>> listener) {
decodeToken(token, false, ActionListener.wrap(userToken -> {
if (userToken == null) {
listener.onFailure(new ElasticsearchSecurityException("supplied token is not valid"));
} else {
listener.onResponse(new Tuple<>(userToken.getAuthentication(), userToken.getMetadata()));
}
}, listener::onFailure));
}
/**
* Retrieves the {@code UserToken} given the {@param tokenId} and {@param tokenVersion}.
* If passed in, {@param storedAccessToken} is verified against the value stored in the token document.
* A mismatch here indicates that the token doc represents a different token from the one the client
* is referring to.
* If {@param validateUserToken} is {@code true} the {@code UserToken} is also checked to not be
* invalidated or expired.
*/
@SuppressWarnings("unchecked")
private void getAndValidateUserToken(
String tokenId,
TransportVersion tokenVersion,
@Nullable String storedAccessToken,
boolean validateUserToken,
ActionListener listener
) {
getTokenDocById(tokenId, tokenVersion, storedAccessToken, null, ActionListener.wrap(doc -> {
if (doc == null) {
listener.onResponse(null);
} else {
Map accessTokenSource = (Map) doc.sourceAsMap().get("access_token");
if (validateUserToken) {
Boolean invalidated = (Boolean) accessTokenSource.get("invalidated");
if (invalidated == null) {
listener.onFailure(new IllegalStateException("token document is missing invalidated field"));
} else if (invalidated) {
listener.onFailure(expiredTokenException());
} else {
UserToken userToken = UserToken.fromSourceMap((Map) accessTokenSource.get("user_token"));
if (clock.instant().isAfter(userToken.getExpirationTime())) {
listener.onFailure(traceLog("validate token", userToken.getId(), expiredTokenException()));
} else {
listener.onResponse(userToken);
}
}
} else {
listener.onResponse(UserToken.fromSourceMap((Map) accessTokenSource.get("user_token")));
}
}
}, listener::onFailure));
}
/**
* Retrieves a token doc given the {@param tokenId} and {@param tokenVersion}.
* If passed in, {@param storedAccessToken} and {@param storedRefreshToken} are verified against the values stored
* in the token document. A mismatch here indicates that the token doc represents a different token from the one
* that the client is referring to, even though the client knows (or guessed) the {@param tokenId}. In other words,
* the client knows only part of the secret token (some part of it is wrong).
*/
@SuppressWarnings("unchecked")
private void getTokenDocById(
String tokenId,
TransportVersion tokenVersion,
@Nullable String storedAccessToken,
@Nullable String storedRefreshToken,
ActionListener listener
) {
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, tokensIndex.aliasName());
listener.onFailure(frozenTokensIndex.getUnavailableReason(PRIMARY_SHARDS));
return;
}
final GetRequest getRequest = client.prepareGet(tokensIndex.aliasName(), getTokenDocumentId(tokenId)).request();
final Consumer onFailure = ex -> listener.onFailure(traceLog("get token from id", tokenId, ex));
tokensIndex.checkIndexVersionThenExecute(
ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", tokenId, ex)),
() -> executeAsyncWithOrigin(
client.threadPool().getThreadContext(),
SECURITY_ORIGIN,
getRequest,
ActionListener.wrap(response -> {
if (response.isExists() == false) {
// The chances of a random token string decoding to something that we can read is minimal, so
// we assume that this was a token we have created but is now expired/revoked and deleted
logger.trace("The token [{}] probably expired and has already been deleted", tokenId);
listener.onResponse(null);
return;
}
Map accessSource = (Map) response.getSource().get("access_token");
Map refreshSource = (Map) response.getSource().get("refresh_token");
boolean versionGetForRefresh = tokenVersion.onOrAfter(VERSION_GET_TOKEN_DOC_FOR_REFRESH);
if (accessSource == null) {
onFailure.accept(new IllegalStateException("token document is missing the access_token field"));
} else if (accessSource.containsKey("user_token") == false) {
onFailure.accept(new IllegalStateException("token document is missing the user_token field"));
} else if (versionGetForRefresh && accessSource.containsKey("token") == false) {
onFailure.accept(new IllegalStateException("token document is missing the user_token.token field"));
} else if (versionGetForRefresh && refreshSource != null && refreshSource.containsKey("token") == false) {
onFailure.accept(new IllegalStateException("token document is missing the refresh_token.token field"));
} else if (storedAccessToken != null && storedAccessToken.equals(accessSource.get("token")) == false) {
logger.error(
"The stored access token [{}] for token doc id [{}] could not be verified",
storedAccessToken,
tokenId
);
listener.onResponse(null);
} else if (storedRefreshToken != null
&& (refreshSource == null || storedRefreshToken.equals(refreshSource.get("token")) == false)) {
logger.error(
"The stored refresh token [{}] for token doc id [{}] could not be verified",
storedRefreshToken,
tokenId
);
listener.onResponse(null);
} else {
listener.onResponse(new Doc(response));
}
}, e -> {
// 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, tokensIndex.aliasName());
} else {
logger.error(() -> "failed to get token doc [" + tokenId + "]", e);
}
listener.onFailure(e);
}),
client::get
)
);
}
/**
* If needed, for tokens that were created in a pre {@code #VERSION_ACCESS_TOKENS_UUIDS} cluster, it asynchronously decodes the token to
* get the token document id. The process for this is asynchronous as we may need to compute a key, which can be computationally
* expensive so this should not block the current thread, which is typically a network thread. A second reason for being asynchronous is
* that we can restrain the amount of resources consumed by the key computation to a single thread. For tokens created in an after
* {@code #VERSION_ACCESS_TOKENS_UUIDS} cluster, the token is just the token document Id so this is used directly without decryption
*
*/
void decodeToken(String token, boolean validateUserToken, ActionListener listener) {
final byte[] bytes = token.getBytes(StandardCharsets.UTF_8);
try (StreamInput in = new InputStreamStreamInput(Base64.getDecoder().wrap(new ByteArrayInputStream(bytes)), bytes.length)) {
final TransportVersion version = TransportVersion.readVersion(in);
in.setTransportVersion(version);
if (version.onOrAfter(VERSION_GET_TOKEN_DOC_FOR_REFRESH)) {
byte[] accessTokenBytes = in.readByteArray();
if (accessTokenBytes.length != RAW_TOKEN_BYTES_TOTAL_LENGTH) {
logger.debug(
"invalid token, received size [{}] bytes is different from expect size [{}]",
accessTokenBytes.length,
RAW_TOKEN_BYTES_TOTAL_LENGTH
);
listener.onResponse(null);
return;
}
MessageDigest userTokenIdDigest = sha256();
userTokenIdDigest.update(accessTokenBytes, RAW_TOKEN_BYTES_LENGTH, RAW_TOKEN_DOC_ID_BYTES_LENGTH);
final String userTokenId = Base64.getUrlEncoder().withoutPadding().encodeToString(userTokenIdDigest.digest());
final String storedAccessToken = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256().digest(accessTokenBytes));
getAndValidateUserToken(userTokenId, version, storedAccessToken, validateUserToken, listener);
} else if (version.onOrAfter(VERSION_ACCESS_TOKENS_AS_UUIDS)) {
// The token was created in a > VERSION_ACCESS_TOKENS_UUIDS cluster
if (in.available() < MINIMUM_BYTES) {
logger.debug("invalid token, smaller than [{}] bytes", MINIMUM_BYTES);
listener.onResponse(null);
return;
}
final String accessToken = in.readString();
final String userTokenId = hashTokenString(accessToken);
getAndValidateUserToken(userTokenId, version, null, validateUserToken, listener);
} else {
// The token was created in a < VERSION_ACCESS_TOKENS_UUIDS cluster so we need to decrypt it to get the tokenId
if (in.available() < LEGACY_MINIMUM_BYTES) {
logger.debug("invalid token, smaller than [{}] bytes", LEGACY_MINIMUM_BYTES);
listener.onResponse(null);
return;
}
final BytesKey decodedSalt = new BytesKey(in.readByteArray());
final BytesKey passphraseHash = new BytesKey(in.readByteArray());
final byte[] iv = in.readByteArray();
final BytesStreamOutput out = new BytesStreamOutput();
Streams.copy(in, out);
final byte[] encryptedTokenId = BytesReference.toBytes(out.bytes());
final KeyAndCache keyAndCache = keyCache.get(passphraseHash);
if (keyAndCache != null) {
getKeyAsync(decodedSalt, keyAndCache, ActionListener.wrap(decodeKey -> {
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
logger.debug("built in token service unable to decode token", e);
listener.onResponse(null);
}
}
/**
* This method performs the steps necessary to invalidate an access token so that it may no longer be
* used. The process of invalidation involves performing an update to the token document and setting
* the {@code access_token.invalidated} field to {@code true}
*/
public void invalidateAccessToken(String accessToken, ActionListener listener) {
ensureEnabled();
if (Strings.isNullOrEmpty(accessToken)) {
listener.onFailure(traceLog("invalidate access token", new IllegalArgumentException("access token must be provided")));
} else {
maybeStartTokenRemover();
final Iterator backoff = DEFAULT_BACKOFF.iterator();
decodeToken(accessToken, false, ActionListener.wrap(userToken -> {
if (userToken == null) {
// The chances of a random token string decoding to something that we can read is minimal, so
// we assume that this was a token we have created but is now expired/revoked and deleted
logger.trace("The access token [{}] is expired and already deleted", accessToken);
listener.onResponse(TokensInvalidationResult.emptyResult(RestStatus.NOT_FOUND));
} else {
indexInvalidation(Collections.singleton(userToken), backoff, "access_token", null, listener);
}
}, e -> {
if (e instanceof IndexNotFoundException || e instanceof IndexClosedException) {
listener.onFailure(new ElasticsearchSecurityException("failed to invalidate token", RestStatus.BAD_REQUEST));
} else {
listener.onFailure(unableToPerformAction(e));
}
}));
}
}
/**
* This method invalidates a refresh token so that it may no longer be used. Invalidation involves performing an update to the token
* document and setting the refresh_token.invalidated field to true
*
* @param refreshToken The string representation of the refresh token
* @param listener the listener to notify upon completion
*/
public void invalidateRefreshToken(String refreshToken, ActionListener listener) {
ensureEnabled();
if (Strings.isNullOrEmpty(refreshToken)) {
logger.trace("No refresh token provided");
listener.onFailure(new IllegalArgumentException("refresh token must be provided"));
} else {
maybeStartTokenRemover();
final Iterator backoff = DEFAULT_BACKOFF.iterator();
findTokenFromRefreshToken(refreshToken, backoff, ActionListener.wrap(tokenDoc -> {
if (tokenDoc == null) {
logger.debug("could not find token document for refresh token");
listener.onResponse(TokensInvalidationResult.emptyResult(RestStatus.NOT_FOUND));
} else {
final Tuple parsedTokens = parseTokenAndRefreshStatus(tokenDoc.sourceAsMap());
final UserToken userToken = parsedTokens.v1();
final RefreshTokenStatus refresh = parsedTokens.v2();
if (refresh.isInvalidated()) {
listener.onResponse(new TokensInvalidationResult(List.of(), List.of(userToken.getId()), null, RestStatus.OK));
} else {
indexInvalidation(Collections.singletonList(userToken), backoff, "refresh_token", null, listener);
}
}
}, e -> {
if (e instanceof IndexNotFoundException || e instanceof IndexClosedException) {
listener.onFailure(new ElasticsearchSecurityException("failed to invalidate token", RestStatus.BAD_REQUEST));
} else {
listener.onFailure(unableToPerformAction(e));
}
}));
}
}
/**
* Invalidates all access and refresh tokens for a given {@code realmName} and/or of a given
* {@code username} so that they may no longer be usable.
*
* @param realmName the realm of which the tokens should be invalidated
* @param username the username for which the tokens should be invalidated
* @param filter An optional {@code Predicate} to further test and filter the tokens to invalidate.
* The predicate tests the token doc source.
* @param listener the listener to notify upon completion
*/
public void invalidateActiveTokens(
@Nullable String realmName,
@Nullable String username,
@Nullable Predicate