Skip to content

Commit

Permalink
Add an authentication cache for API keys (#38469) (#38480)
Browse files Browse the repository at this point in the history
This commit adds an authentication cache for API keys that caches the
hash of an API key with a faster hash. This will enable better
performance when API keys are used for bulk or heavy searching.
  • Loading branch information
jaymode authored and bizybot committed Feb 6, 2019
1 parent 3a0c896 commit a45429c
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,8 @@ Collection<Object> createComponents(Client client, ThreadPool threadPool, Cluste
rolesProviders.addAll(extension.getRolesProviders(settings, resourceWatcherService));
}

final ApiKeyService apiKeyService = new ApiKeyService(settings, Clock.systemUTC(), client, securityIndex.get(), clusterService);
final ApiKeyService apiKeyService = new ApiKeyService(settings, Clock.systemUTC(), client, securityIndex.get(), clusterService,
threadPool);
components.add(apiKeyService);
final CompositeRolesStore allRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore,
reservedRolesStore, privilegeStore, rolesProviders, threadPool.getThreadContext(), getLicenseState(), fieldPermissionsCache,
Expand Down Expand Up @@ -707,6 +708,9 @@ public static List<Setting<?>> getSettings(boolean transportClientMode, List<Sec
settingsList.add(ApiKeyService.PASSWORD_HASHING_ALGORITHM);
settingsList.add(ApiKeyService.DELETE_TIMEOUT);
settingsList.add(ApiKeyService.DELETE_INTERVAL);
settingsList.add(ApiKeyService.CACHE_HASH_ALGO_SETTING);
settingsList.add(ApiKeyService.CACHE_MAX_KEYS_SETTING);
settingsList.add(ApiKeyService.CACHE_TTL_SETTING);

// hide settings
settingsList.add(Setting.listSetting(SecurityField.setting("hide_settings"), Collections.emptyList(), Function.identity(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.cache.Cache;
import org.elasticsearch.common.cache.CacheBuilder;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.logging.LoggerMessageFormat;
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.unit.TimeValue;
import org.elasticsearch.common.util.concurrent.FutureUtils;
import org.elasticsearch.common.util.concurrent.ListenableFuture;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.xcontent.DeprecationHandler;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
Expand All @@ -50,6 +54,7 @@
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.ScrollHelper;
import org.elasticsearch.xpack.core.security.action.ApiKey;
Expand Down Expand Up @@ -81,6 +86,9 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -118,6 +126,12 @@ public class ApiKeyService {
TimeValue.MINUS_ONE, Property.NodeScope);
public static final Setting<TimeValue> DELETE_INTERVAL = Setting.timeSetting("xpack.security.authc.api_key.delete.interval",
TimeValue.timeValueHours(24L), Property.NodeScope);
public static final Setting<String> CACHE_HASH_ALGO_SETTING = Setting.simpleString("xpack.security.authc.api_key.cache.hash_algo",
"ssha256", Setting.Property.NodeScope);
public static final Setting<TimeValue> CACHE_TTL_SETTING = Setting.timeSetting("xpack.security.authc.api_key.cache.ttl",
TimeValue.timeValueHours(24L), Property.NodeScope);
public static final Setting<Integer> CACHE_MAX_KEYS_SETTING = Setting.intSetting("xpack.security.authc.api_key.cache.max_keys",
10000, Property.NodeScope);

private final Clock clock;
private final Client client;
Expand All @@ -128,11 +142,14 @@ public class ApiKeyService {
private final Settings settings;
private final ExpiredApiKeysRemover expiredApiKeysRemover;
private final TimeValue deleteInterval;
private final Cache<String, ListenableFuture<CachedApiKeyHashResult>> apiKeyAuthCache;
private final Hasher cacheHasher;
private final ThreadPool threadPool;

private volatile long lastExpirationRunMs;

public ApiKeyService(Settings settings, Clock clock, Client client, SecurityIndexManager securityIndex,
ClusterService clusterService) {
public ApiKeyService(Settings settings, Clock clock, Client client, SecurityIndexManager securityIndex, ClusterService clusterService,
ThreadPool threadPool) {
this.clock = clock;
this.client = client;
this.securityIndex = securityIndex;
Expand All @@ -142,6 +159,17 @@ public ApiKeyService(Settings settings, Clock clock, Client client, SecurityInde
this.settings = settings;
this.deleteInterval = DELETE_INTERVAL.get(settings);
this.expiredApiKeysRemover = new ExpiredApiKeysRemover(settings, client);
this.threadPool = threadPool;
this.cacheHasher = Hasher.resolve(CACHE_HASH_ALGO_SETTING.get(settings));
final TimeValue ttl = CACHE_TTL_SETTING.get(settings);
if (ttl.getNanos() > 0) {
this.apiKeyAuthCache = CacheBuilder.<String, ListenableFuture<CachedApiKeyHashResult>>builder()
.setExpireAfterWrite(ttl)
.setMaximumWeight(CACHE_MAX_KEYS_SETTING.get(settings))
.build();
} else {
this.apiKeyAuthCache = null;
}
}

/**
Expand Down Expand Up @@ -364,8 +392,8 @@ private List<RoleDescriptor> parseRoleDescriptors(final String apiKeyId, final M
* @param credentials the credentials provided by the user
* @param listener the listener to notify after verification
*/
static void validateApiKeyCredentials(Map<String, Object> source, ApiKeyCredentials credentials, Clock clock,
ActionListener<AuthenticationResult> listener) {
void validateApiKeyCredentials(Map<String, Object> source, ApiKeyCredentials credentials, Clock clock,
ActionListener<AuthenticationResult> listener) {
final Boolean invalidated = (Boolean) source.get("api_key_invalidated");
if (invalidated == null) {
listener.onResponse(AuthenticationResult.terminate("api key document is missing invalidated field", null));
Expand All @@ -376,33 +404,87 @@ static void validateApiKeyCredentials(Map<String, Object> source, ApiKeyCredenti
if (apiKeyHash == null) {
throw new IllegalStateException("api key hash is missing");
}
final boolean verified = verifyKeyAgainstHash(apiKeyHash, credentials);

if (verified) {
final Long expirationEpochMilli = (Long) source.get("expiration_time");
if (expirationEpochMilli == null || Instant.ofEpochMilli(expirationEpochMilli).isAfter(clock.instant())) {
final Map<String, Object> creator = Objects.requireNonNull((Map<String, Object>) source.get("creator"));
final String principal = Objects.requireNonNull((String) creator.get("principal"));
final Map<String, Object> metadata = (Map<String, Object>) creator.get("metadata");
final Map<String, Object> roleDescriptors = (Map<String, Object>) source.get("role_descriptors");
final Map<String, Object> limitedByRoleDescriptors = (Map<String, Object>) source.get("limited_by_role_descriptors");
final String[] roleNames = (roleDescriptors != null) ? roleDescriptors.keySet().toArray(Strings.EMPTY_ARRAY)
: limitedByRoleDescriptors.keySet().toArray(Strings.EMPTY_ARRAY);
final User apiKeyUser = new User(principal, roleNames, null, null, metadata, true);
final Map<String, Object> authResultMetadata = new HashMap<>();
authResultMetadata.put(API_KEY_ROLE_DESCRIPTORS_KEY, roleDescriptors);
authResultMetadata.put(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, limitedByRoleDescriptors);
authResultMetadata.put(API_KEY_ID_KEY, credentials.getId());
listener.onResponse(AuthenticationResult.success(apiKeyUser, authResultMetadata));

if (apiKeyAuthCache != null) {
final AtomicBoolean valueAlreadyInCache = new AtomicBoolean(true);
final ListenableFuture<CachedApiKeyHashResult> listenableCacheEntry;
try {
listenableCacheEntry = apiKeyAuthCache.computeIfAbsent(credentials.getId(),
k -> {
valueAlreadyInCache.set(false);
return new ListenableFuture<>();
});
} catch (ExecutionException e) {
listener.onFailure(e);
return;
}

if (valueAlreadyInCache.get()) {
listenableCacheEntry.addListener(ActionListener.wrap(result -> {
if (result.success) {
if (result.verify(credentials.getKey())) {
// move on
validateApiKeyExpiration(source, credentials, clock, listener);
} else {
listener.onResponse(AuthenticationResult.unsuccessful("invalid credentials", null));
}
} else if (result.verify(credentials.getKey())) { // same key, pass the same result
listener.onResponse(AuthenticationResult.unsuccessful("invalid credentials", null));
} else {
apiKeyAuthCache.invalidate(credentials.getId(), listenableCacheEntry);
validateApiKeyCredentials(source, credentials, clock, listener);
}
}, listener::onFailure),
threadPool.generic(), threadPool.getThreadContext());
} else {
listener.onResponse(AuthenticationResult.terminate("api key is expired", null));
final boolean verified = verifyKeyAgainstHash(apiKeyHash, credentials);
listenableCacheEntry.onResponse(new CachedApiKeyHashResult(verified, credentials.getKey()));
if (verified) {
// move on
validateApiKeyExpiration(source, credentials, clock, listener);
} else {
listener.onResponse(AuthenticationResult.unsuccessful("invalid credentials", null));
}
}
} else {
listener.onResponse(AuthenticationResult.unsuccessful("invalid credentials", null));
final boolean verified = verifyKeyAgainstHash(apiKeyHash, credentials);
if (verified) {
// move on
validateApiKeyExpiration(source, credentials, clock, listener);
} else {
listener.onResponse(AuthenticationResult.unsuccessful("invalid credentials", null));
}
}
}
}

// pkg private for testing
CachedApiKeyHashResult getFromCache(String id) {
return apiKeyAuthCache == null ? null : FutureUtils.get(apiKeyAuthCache.get(id), 0L, TimeUnit.MILLISECONDS);
}

private void validateApiKeyExpiration(Map<String, Object> source, ApiKeyCredentials credentials, Clock clock,
ActionListener<AuthenticationResult> listener) {
final Long expirationEpochMilli = (Long) source.get("expiration_time");
if (expirationEpochMilli == null || Instant.ofEpochMilli(expirationEpochMilli).isAfter(clock.instant())) {
final Map<String, Object> creator = Objects.requireNonNull((Map<String, Object>) source.get("creator"));
final String principal = Objects.requireNonNull((String) creator.get("principal"));
final Map<String, Object> metadata = (Map<String, Object>) creator.get("metadata");
final Map<String, Object> roleDescriptors = (Map<String, Object>) source.get("role_descriptors");
final Map<String, Object> limitedByRoleDescriptors = (Map<String, Object>) source.get("limited_by_role_descriptors");
final String[] roleNames = (roleDescriptors != null) ? roleDescriptors.keySet().toArray(Strings.EMPTY_ARRAY)
: limitedByRoleDescriptors.keySet().toArray(Strings.EMPTY_ARRAY);
final User apiKeyUser = new User(principal, roleNames, null, null, metadata, true);
final Map<String, Object> authResultMetadata = new HashMap<>();
authResultMetadata.put(API_KEY_ROLE_DESCRIPTORS_KEY, roleDescriptors);
authResultMetadata.put(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, limitedByRoleDescriptors);
authResultMetadata.put(API_KEY_ID_KEY, credentials.getId());
listener.onResponse(AuthenticationResult.success(apiKeyUser, authResultMetadata));
} else {
listener.onResponse(AuthenticationResult.terminate("api key is expired", null));
}
}

/**
* Gets the API Key from the <code>Authorization</code> header if the header begins with
* <code>ApiKey </code>
Expand Down Expand Up @@ -858,4 +940,17 @@ public void getApiKeyForApiKeyName(String apiKeyName, ActionListener<GetApiKeyRe
}
}

final class CachedApiKeyHashResult {
final boolean success;
final char[] hash;

CachedApiKeyHashResult(boolean success, SecureString apiKey) {
this.success = success;
this.hash = cacheHasher.hash(apiKey);
}

private boolean verify(SecureString password) {
return hash != null && cacheHasher.verify(password, hash);
}
}
}
Loading

0 comments on commit a45429c

Please sign in to comment.