3333import org .elasticsearch .common .Strings ;
3434import org .elasticsearch .common .UUIDs ;
3535import org .elasticsearch .common .bytes .BytesReference ;
36+ import org .elasticsearch .common .cache .Cache ;
37+ import org .elasticsearch .common .cache .CacheBuilder ;
3638import org .elasticsearch .common .logging .DeprecationLogger ;
3739import org .elasticsearch .common .settings .SecureString ;
3840import org .elasticsearch .common .settings .Setting ;
3941import org .elasticsearch .common .settings .Setting .Property ;
4042import org .elasticsearch .common .settings .Settings ;
4143import org .elasticsearch .common .unit .TimeValue ;
44+ import org .elasticsearch .common .util .concurrent .FutureUtils ;
45+ import org .elasticsearch .common .util .concurrent .ListenableFuture ;
4246import org .elasticsearch .common .util .concurrent .ThreadContext ;
4347import org .elasticsearch .common .xcontent .DeprecationHandler ;
4448import org .elasticsearch .common .xcontent .NamedXContentRegistry ;
4953import org .elasticsearch .index .query .BoolQueryBuilder ;
5054import org .elasticsearch .index .query .QueryBuilders ;
5155import org .elasticsearch .search .SearchHit ;
56+ import org .elasticsearch .threadpool .ThreadPool ;
5257import org .elasticsearch .xpack .core .XPackSettings ;
5358import org .elasticsearch .xpack .core .security .ScrollHelper ;
5459import org .elasticsearch .xpack .core .security .action .ApiKey ;
8186import java .util .Map ;
8287import java .util .Objects ;
8388import java .util .Set ;
89+ import java .util .concurrent .ExecutionException ;
90+ import java .util .concurrent .TimeUnit ;
91+ import java .util .concurrent .atomic .AtomicBoolean ;
8492import java .util .function .Function ;
8593import java .util .stream .Collectors ;
8694
@@ -96,7 +104,6 @@ public class ApiKeyService {
96104 static final String API_KEY_ID_KEY = "_security_api_key_id" ;
97105 static final String API_KEY_ROLE_DESCRIPTORS_KEY = "_security_api_key_role_descriptors" ;
98106 static final String API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY = "_security_api_key_limited_by_role_descriptors" ;
99- static final String API_KEY_ROLE_KEY = "_security_api_key_role" ;
100107
101108 public static final Setting <String > PASSWORD_HASHING_ALGORITHM = new Setting <>(
102109 "xpack.security.authc.api_key_hashing.algorithm" , "pbkdf2" , Function .identity (), v -> {
@@ -117,6 +124,12 @@ public class ApiKeyService {
117124 TimeValue .MINUS_ONE , Property .NodeScope );
118125 public static final Setting <TimeValue > DELETE_INTERVAL = Setting .timeSetting ("xpack.security.authc.api_key.delete.interval" ,
119126 TimeValue .timeValueHours (24L ), Property .NodeScope );
127+ public static final Setting <String > CACHE_HASH_ALGO_SETTING = Setting .simpleString ("xpack.security.authc.api_key.cache.hash_algo" ,
128+ "ssha256" , Setting .Property .NodeScope );
129+ public static final Setting <TimeValue > CACHE_TTL_SETTING = Setting .timeSetting ("xpack.security.authc.api_key.cache.ttl" ,
130+ TimeValue .timeValueHours (24L ), Property .NodeScope );
131+ public static final Setting <Integer > CACHE_MAX_KEYS_SETTING = Setting .intSetting ("xpack.security.authc.api_key.cache.max_keys" ,
132+ 10000 , Property .NodeScope );
120133
121134 private final Clock clock ;
122135 private final Client client ;
@@ -127,11 +140,14 @@ public class ApiKeyService {
127140 private final Settings settings ;
128141 private final ExpiredApiKeysRemover expiredApiKeysRemover ;
129142 private final TimeValue deleteInterval ;
143+ private final Cache <String , ListenableFuture <CachedApiKeyHashResult >> apiKeyAuthCache ;
144+ private final Hasher cacheHasher ;
145+ private final ThreadPool threadPool ;
130146
131147 private volatile long lastExpirationRunMs ;
132148
133- public ApiKeyService (Settings settings , Clock clock , Client client , SecurityIndexManager securityIndex ,
134- ClusterService clusterService ) {
149+ public ApiKeyService (Settings settings , Clock clock , Client client , SecurityIndexManager securityIndex , ClusterService clusterService ,
150+ ThreadPool threadPool ) {
135151 this .clock = clock ;
136152 this .client = client ;
137153 this .securityIndex = securityIndex ;
@@ -141,6 +157,17 @@ public ApiKeyService(Settings settings, Clock clock, Client client, SecurityInde
141157 this .settings = settings ;
142158 this .deleteInterval = DELETE_INTERVAL .get (settings );
143159 this .expiredApiKeysRemover = new ExpiredApiKeysRemover (settings , client );
160+ this .threadPool = threadPool ;
161+ this .cacheHasher = Hasher .resolve (CACHE_HASH_ALGO_SETTING .get (settings ));
162+ final TimeValue ttl = CACHE_TTL_SETTING .get (settings );
163+ if (ttl .getNanos () > 0 ) {
164+ this .apiKeyAuthCache = CacheBuilder .<String , ListenableFuture <CachedApiKeyHashResult >>builder ()
165+ .setExpireAfterWrite (ttl )
166+ .setMaximumWeight (CACHE_MAX_KEYS_SETTING .get (settings ))
167+ .build ();
168+ } else {
169+ this .apiKeyAuthCache = null ;
170+ }
144171 }
145172
146173 /**
@@ -363,8 +390,8 @@ private List<RoleDescriptor> parseRoleDescriptors(final String apiKeyId, final M
363390 * @param credentials the credentials provided by the user
364391 * @param listener the listener to notify after verification
365392 */
366- static void validateApiKeyCredentials (Map <String , Object > source , ApiKeyCredentials credentials , Clock clock ,
367- ActionListener <AuthenticationResult > listener ) {
393+ void validateApiKeyCredentials (Map <String , Object > source , ApiKeyCredentials credentials , Clock clock ,
394+ ActionListener <AuthenticationResult > listener ) {
368395 final Boolean invalidated = (Boolean ) source .get ("api_key_invalidated" );
369396 if (invalidated == null ) {
370397 listener .onResponse (AuthenticationResult .terminate ("api key document is missing invalidated field" , null ));
@@ -375,33 +402,87 @@ static void validateApiKeyCredentials(Map<String, Object> source, ApiKeyCredenti
375402 if (apiKeyHash == null ) {
376403 throw new IllegalStateException ("api key hash is missing" );
377404 }
378- final boolean verified = verifyKeyAgainstHash (apiKeyHash , credentials );
379-
380- if (verified ) {
381- final Long expirationEpochMilli = (Long ) source .get ("expiration_time" );
382- if (expirationEpochMilli == null || Instant .ofEpochMilli (expirationEpochMilli ).isAfter (clock .instant ())) {
383- final Map <String , Object > creator = Objects .requireNonNull ((Map <String , Object >) source .get ("creator" ));
384- final String principal = Objects .requireNonNull ((String ) creator .get ("principal" ));
385- final Map <String , Object > metadata = (Map <String , Object >) creator .get ("metadata" );
386- final Map <String , Object > roleDescriptors = (Map <String , Object >) source .get ("role_descriptors" );
387- final Map <String , Object > limitedByRoleDescriptors = (Map <String , Object >) source .get ("limited_by_role_descriptors" );
388- final String [] roleNames = (roleDescriptors != null ) ? roleDescriptors .keySet ().toArray (Strings .EMPTY_ARRAY )
389- : limitedByRoleDescriptors .keySet ().toArray (Strings .EMPTY_ARRAY );
390- final User apiKeyUser = new User (principal , roleNames , null , null , metadata , true );
391- final Map <String , Object > authResultMetadata = new HashMap <>();
392- authResultMetadata .put (API_KEY_ROLE_DESCRIPTORS_KEY , roleDescriptors );
393- authResultMetadata .put (API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY , limitedByRoleDescriptors );
394- authResultMetadata .put (API_KEY_ID_KEY , credentials .getId ());
395- listener .onResponse (AuthenticationResult .success (apiKeyUser , authResultMetadata ));
405+
406+ if (apiKeyAuthCache != null ) {
407+ final AtomicBoolean valueAlreadyInCache = new AtomicBoolean (true );
408+ final ListenableFuture <CachedApiKeyHashResult > listenableCacheEntry ;
409+ try {
410+ listenableCacheEntry = apiKeyAuthCache .computeIfAbsent (credentials .getId (),
411+ k -> {
412+ valueAlreadyInCache .set (false );
413+ return new ListenableFuture <>();
414+ });
415+ } catch (ExecutionException e ) {
416+ listener .onFailure (e );
417+ return ;
418+ }
419+
420+ if (valueAlreadyInCache .get ()) {
421+ listenableCacheEntry .addListener (ActionListener .wrap (result -> {
422+ if (result .success ) {
423+ if (result .verify (credentials .getKey ())) {
424+ // move on
425+ validateApiKeyExpiration (source , credentials , clock , listener );
426+ } else {
427+ listener .onResponse (AuthenticationResult .unsuccessful ("invalid credentials" , null ));
428+ }
429+ } else if (result .verify (credentials .getKey ())) { // same key, pass the same result
430+ listener .onResponse (AuthenticationResult .unsuccessful ("invalid credentials" , null ));
431+ } else {
432+ apiKeyAuthCache .invalidate (credentials .getId (), listenableCacheEntry );
433+ validateApiKeyCredentials (source , credentials , clock , listener );
434+ }
435+ }, listener ::onFailure ),
436+ threadPool .generic (), threadPool .getThreadContext ());
396437 } else {
397- listener .onResponse (AuthenticationResult .terminate ("api key is expired" , null ));
438+ final boolean verified = verifyKeyAgainstHash (apiKeyHash , credentials );
439+ listenableCacheEntry .onResponse (new CachedApiKeyHashResult (verified , credentials .getKey ()));
440+ if (verified ) {
441+ // move on
442+ validateApiKeyExpiration (source , credentials , clock , listener );
443+ } else {
444+ listener .onResponse (AuthenticationResult .unsuccessful ("invalid credentials" , null ));
445+ }
398446 }
399447 } else {
400- listener .onResponse (AuthenticationResult .unsuccessful ("invalid credentials" , null ));
448+ final boolean verified = verifyKeyAgainstHash (apiKeyHash , credentials );
449+ if (verified ) {
450+ // move on
451+ validateApiKeyExpiration (source , credentials , clock , listener );
452+ } else {
453+ listener .onResponse (AuthenticationResult .unsuccessful ("invalid credentials" , null ));
454+ }
401455 }
402456 }
403457 }
404458
459+ // pkg private for testing
460+ CachedApiKeyHashResult getFromCache (String id ) {
461+ return apiKeyAuthCache == null ? null : FutureUtils .get (apiKeyAuthCache .get (id ), 0L , TimeUnit .MILLISECONDS );
462+ }
463+
464+ private void validateApiKeyExpiration (Map <String , Object > source , ApiKeyCredentials credentials , Clock clock ,
465+ ActionListener <AuthenticationResult > listener ) {
466+ final Long expirationEpochMilli = (Long ) source .get ("expiration_time" );
467+ if (expirationEpochMilli == null || Instant .ofEpochMilli (expirationEpochMilli ).isAfter (clock .instant ())) {
468+ final Map <String , Object > creator = Objects .requireNonNull ((Map <String , Object >) source .get ("creator" ));
469+ final String principal = Objects .requireNonNull ((String ) creator .get ("principal" ));
470+ final Map <String , Object > metadata = (Map <String , Object >) creator .get ("metadata" );
471+ final Map <String , Object > roleDescriptors = (Map <String , Object >) source .get ("role_descriptors" );
472+ final Map <String , Object > limitedByRoleDescriptors = (Map <String , Object >) source .get ("limited_by_role_descriptors" );
473+ final String [] roleNames = (roleDescriptors != null ) ? roleDescriptors .keySet ().toArray (Strings .EMPTY_ARRAY )
474+ : limitedByRoleDescriptors .keySet ().toArray (Strings .EMPTY_ARRAY );
475+ final User apiKeyUser = new User (principal , roleNames , null , null , metadata , true );
476+ final Map <String , Object > authResultMetadata = new HashMap <>();
477+ authResultMetadata .put (API_KEY_ROLE_DESCRIPTORS_KEY , roleDescriptors );
478+ authResultMetadata .put (API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY , limitedByRoleDescriptors );
479+ authResultMetadata .put (API_KEY_ID_KEY , credentials .getId ());
480+ listener .onResponse (AuthenticationResult .success (apiKeyUser , authResultMetadata ));
481+ } else {
482+ listener .onResponse (AuthenticationResult .terminate ("api key is expired" , null ));
483+ }
484+ }
485+
405486 /**
406487 * Gets the API Key from the <code>Authorization</code> header if the header begins with
407488 * <code>ApiKey </code>
@@ -851,4 +932,17 @@ public void getApiKeyForApiKeyName(String apiKeyName, ActionListener<GetApiKeyRe
851932 }
852933 }
853934
935+ final class CachedApiKeyHashResult {
936+ final boolean success ;
937+ final char [] hash ;
938+
939+ CachedApiKeyHashResult (boolean success , SecureString apiKey ) {
940+ this .success = success ;
941+ this .hash = cacheHasher .hash (apiKey );
942+ }
943+
944+ private boolean verify (SecureString password ) {
945+ return hash != null && cacheHasher .verify (password , hash );
946+ }
947+ }
854948}
0 commit comments