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 .logging .LoggerMessageFormat ;
3840import org .elasticsearch .common .settings .SecureString ;
3941import org .elasticsearch .common .settings .Setting ;
4042import org .elasticsearch .common .settings .Setting .Property ;
4143import org .elasticsearch .common .settings .Settings ;
4244import org .elasticsearch .common .unit .TimeValue ;
45+ import org .elasticsearch .common .util .concurrent .FutureUtils ;
46+ import org .elasticsearch .common .util .concurrent .ListenableFuture ;
4347import org .elasticsearch .common .util .concurrent .ThreadContext ;
4448import org .elasticsearch .common .xcontent .DeprecationHandler ;
4549import org .elasticsearch .common .xcontent .NamedXContentRegistry ;
5054import org .elasticsearch .index .query .BoolQueryBuilder ;
5155import org .elasticsearch .index .query .QueryBuilders ;
5256import org .elasticsearch .search .SearchHit ;
57+ import org .elasticsearch .threadpool .ThreadPool ;
5358import org .elasticsearch .xpack .core .XPackSettings ;
5459import org .elasticsearch .xpack .core .security .ScrollHelper ;
5560import 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
@@ -118,6 +126,12 @@ public class ApiKeyService {
118126 TimeValue .MINUS_ONE , Property .NodeScope );
119127 public static final Setting <TimeValue > DELETE_INTERVAL = Setting .timeSetting ("xpack.security.authc.api_key.delete.interval" ,
120128 TimeValue .timeValueHours (24L ), Property .NodeScope );
129+ public static final Setting <String > CACHE_HASH_ALGO_SETTING = Setting .simpleString ("xpack.security.authc.api_key.cache.hash_algo" ,
130+ "ssha256" , Setting .Property .NodeScope );
131+ public static final Setting <TimeValue > CACHE_TTL_SETTING = Setting .timeSetting ("xpack.security.authc.api_key.cache.ttl" ,
132+ TimeValue .timeValueHours (24L ), Property .NodeScope );
133+ public static final Setting <Integer > CACHE_MAX_KEYS_SETTING = Setting .intSetting ("xpack.security.authc.api_key.cache.max_keys" ,
134+ 10000 , Property .NodeScope );
121135
122136 private final Clock clock ;
123137 private final Client client ;
@@ -128,11 +142,14 @@ public class ApiKeyService {
128142 private final Settings settings ;
129143 private final ExpiredApiKeysRemover expiredApiKeysRemover ;
130144 private final TimeValue deleteInterval ;
145+ private final Cache <String , ListenableFuture <CachedApiKeyHashResult >> apiKeyAuthCache ;
146+ private final Hasher cacheHasher ;
147+ private final ThreadPool threadPool ;
131148
132149 private volatile long lastExpirationRunMs ;
133150
134- public ApiKeyService (Settings settings , Clock clock , Client client , SecurityIndexManager securityIndex ,
135- ClusterService clusterService ) {
151+ public ApiKeyService (Settings settings , Clock clock , Client client , SecurityIndexManager securityIndex , ClusterService clusterService ,
152+ ThreadPool threadPool ) {
136153 this .clock = clock ;
137154 this .client = client ;
138155 this .securityIndex = securityIndex ;
@@ -142,6 +159,17 @@ public ApiKeyService(Settings settings, Clock clock, Client client, SecurityInde
142159 this .settings = settings ;
143160 this .deleteInterval = DELETE_INTERVAL .get (settings );
144161 this .expiredApiKeysRemover = new ExpiredApiKeysRemover (settings , client );
162+ this .threadPool = threadPool ;
163+ this .cacheHasher = Hasher .resolve (CACHE_HASH_ALGO_SETTING .get (settings ));
164+ final TimeValue ttl = CACHE_TTL_SETTING .get (settings );
165+ if (ttl .getNanos () > 0 ) {
166+ this .apiKeyAuthCache = CacheBuilder .<String , ListenableFuture <CachedApiKeyHashResult >>builder ()
167+ .setExpireAfterWrite (ttl )
168+ .setMaximumWeight (CACHE_MAX_KEYS_SETTING .get (settings ))
169+ .build ();
170+ } else {
171+ this .apiKeyAuthCache = null ;
172+ }
145173 }
146174
147175 /**
@@ -364,8 +392,8 @@ private List<RoleDescriptor> parseRoleDescriptors(final String apiKeyId, final M
364392 * @param credentials the credentials provided by the user
365393 * @param listener the listener to notify after verification
366394 */
367- static void validateApiKeyCredentials (Map <String , Object > source , ApiKeyCredentials credentials , Clock clock ,
368- ActionListener <AuthenticationResult > listener ) {
395+ void validateApiKeyCredentials (Map <String , Object > source , ApiKeyCredentials credentials , Clock clock ,
396+ ActionListener <AuthenticationResult > listener ) {
369397 final Boolean invalidated = (Boolean ) source .get ("api_key_invalidated" );
370398 if (invalidated == null ) {
371399 listener .onResponse (AuthenticationResult .terminate ("api key document is missing invalidated field" , null ));
@@ -376,33 +404,87 @@ static void validateApiKeyCredentials(Map<String, Object> source, ApiKeyCredenti
376404 if (apiKeyHash == null ) {
377405 throw new IllegalStateException ("api key hash is missing" );
378406 }
379- final boolean verified = verifyKeyAgainstHash (apiKeyHash , credentials );
380-
381- if (verified ) {
382- final Long expirationEpochMilli = (Long ) source .get ("expiration_time" );
383- if (expirationEpochMilli == null || Instant .ofEpochMilli (expirationEpochMilli ).isAfter (clock .instant ())) {
384- final Map <String , Object > creator = Objects .requireNonNull ((Map <String , Object >) source .get ("creator" ));
385- final String principal = Objects .requireNonNull ((String ) creator .get ("principal" ));
386- final Map <String , Object > metadata = (Map <String , Object >) creator .get ("metadata" );
387- final Map <String , Object > roleDescriptors = (Map <String , Object >) source .get ("role_descriptors" );
388- final Map <String , Object > limitedByRoleDescriptors = (Map <String , Object >) source .get ("limited_by_role_descriptors" );
389- final String [] roleNames = (roleDescriptors != null ) ? roleDescriptors .keySet ().toArray (Strings .EMPTY_ARRAY )
390- : limitedByRoleDescriptors .keySet ().toArray (Strings .EMPTY_ARRAY );
391- final User apiKeyUser = new User (principal , roleNames , null , null , metadata , true );
392- final Map <String , Object > authResultMetadata = new HashMap <>();
393- authResultMetadata .put (API_KEY_ROLE_DESCRIPTORS_KEY , roleDescriptors );
394- authResultMetadata .put (API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY , limitedByRoleDescriptors );
395- authResultMetadata .put (API_KEY_ID_KEY , credentials .getId ());
396- listener .onResponse (AuthenticationResult .success (apiKeyUser , authResultMetadata ));
407+
408+ if (apiKeyAuthCache != null ) {
409+ final AtomicBoolean valueAlreadyInCache = new AtomicBoolean (true );
410+ final ListenableFuture <CachedApiKeyHashResult > listenableCacheEntry ;
411+ try {
412+ listenableCacheEntry = apiKeyAuthCache .computeIfAbsent (credentials .getId (),
413+ k -> {
414+ valueAlreadyInCache .set (false );
415+ return new ListenableFuture <>();
416+ });
417+ } catch (ExecutionException e ) {
418+ listener .onFailure (e );
419+ return ;
420+ }
421+
422+ if (valueAlreadyInCache .get ()) {
423+ listenableCacheEntry .addListener (ActionListener .wrap (result -> {
424+ if (result .success ) {
425+ if (result .verify (credentials .getKey ())) {
426+ // move on
427+ validateApiKeyExpiration (source , credentials , clock , listener );
428+ } else {
429+ listener .onResponse (AuthenticationResult .unsuccessful ("invalid credentials" , null ));
430+ }
431+ } else if (result .verify (credentials .getKey ())) { // same key, pass the same result
432+ listener .onResponse (AuthenticationResult .unsuccessful ("invalid credentials" , null ));
433+ } else {
434+ apiKeyAuthCache .invalidate (credentials .getId (), listenableCacheEntry );
435+ validateApiKeyCredentials (source , credentials , clock , listener );
436+ }
437+ }, listener ::onFailure ),
438+ threadPool .generic (), threadPool .getThreadContext ());
397439 } else {
398- listener .onResponse (AuthenticationResult .terminate ("api key is expired" , null ));
440+ final boolean verified = verifyKeyAgainstHash (apiKeyHash , credentials );
441+ listenableCacheEntry .onResponse (new CachedApiKeyHashResult (verified , credentials .getKey ()));
442+ if (verified ) {
443+ // move on
444+ validateApiKeyExpiration (source , credentials , clock , listener );
445+ } else {
446+ listener .onResponse (AuthenticationResult .unsuccessful ("invalid credentials" , null ));
447+ }
399448 }
400449 } else {
401- listener .onResponse (AuthenticationResult .unsuccessful ("invalid credentials" , null ));
450+ final boolean verified = verifyKeyAgainstHash (apiKeyHash , credentials );
451+ if (verified ) {
452+ // move on
453+ validateApiKeyExpiration (source , credentials , clock , listener );
454+ } else {
455+ listener .onResponse (AuthenticationResult .unsuccessful ("invalid credentials" , null ));
456+ }
402457 }
403458 }
404459 }
405460
461+ // pkg private for testing
462+ CachedApiKeyHashResult getFromCache (String id ) {
463+ return apiKeyAuthCache == null ? null : FutureUtils .get (apiKeyAuthCache .get (id ), 0L , TimeUnit .MILLISECONDS );
464+ }
465+
466+ private void validateApiKeyExpiration (Map <String , Object > source , ApiKeyCredentials credentials , Clock clock ,
467+ ActionListener <AuthenticationResult > listener ) {
468+ final Long expirationEpochMilli = (Long ) source .get ("expiration_time" );
469+ if (expirationEpochMilli == null || Instant .ofEpochMilli (expirationEpochMilli ).isAfter (clock .instant ())) {
470+ final Map <String , Object > creator = Objects .requireNonNull ((Map <String , Object >) source .get ("creator" ));
471+ final String principal = Objects .requireNonNull ((String ) creator .get ("principal" ));
472+ final Map <String , Object > metadata = (Map <String , Object >) creator .get ("metadata" );
473+ final Map <String , Object > roleDescriptors = (Map <String , Object >) source .get ("role_descriptors" );
474+ final Map <String , Object > limitedByRoleDescriptors = (Map <String , Object >) source .get ("limited_by_role_descriptors" );
475+ final String [] roleNames = (roleDescriptors != null ) ? roleDescriptors .keySet ().toArray (Strings .EMPTY_ARRAY )
476+ : limitedByRoleDescriptors .keySet ().toArray (Strings .EMPTY_ARRAY );
477+ final User apiKeyUser = new User (principal , roleNames , null , null , metadata , true );
478+ final Map <String , Object > authResultMetadata = new HashMap <>();
479+ authResultMetadata .put (API_KEY_ROLE_DESCRIPTORS_KEY , roleDescriptors );
480+ authResultMetadata .put (API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY , limitedByRoleDescriptors );
481+ authResultMetadata .put (API_KEY_ID_KEY , credentials .getId ());
482+ listener .onResponse (AuthenticationResult .success (apiKeyUser , authResultMetadata ));
483+ } else {
484+ listener .onResponse (AuthenticationResult .terminate ("api key is expired" , null ));
485+ }
486+ }
487+
406488 /**
407489 * Gets the API Key from the <code>Authorization</code> header if the header begins with
408490 * <code>ApiKey </code>
@@ -858,4 +940,17 @@ public void getApiKeyForApiKeyName(String apiKeyName, ActionListener<GetApiKeyRe
858940 }
859941 }
860942
943+ final class CachedApiKeyHashResult {
944+ final boolean success ;
945+ final char [] hash ;
946+
947+ CachedApiKeyHashResult (boolean success , SecureString apiKey ) {
948+ this .success = success ;
949+ this .hash = cacheHasher .hash (apiKey );
950+ }
951+
952+ private boolean verify (SecureString password ) {
953+ return hash != null && cacheHasher .verify (password , hash );
954+ }
955+ }
861956}
0 commit comments