66package org .elasticsearch .xpack .security .authc .support ;
77
88import org .elasticsearch .action .ActionListener ;
9+ import org .elasticsearch .common .Nullable ;
910import org .elasticsearch .common .cache .Cache ;
1011import org .elasticsearch .common .cache .CacheBuilder ;
1112import org .elasticsearch .common .settings .SecureString ;
2930
3031public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm implements CachingRealm {
3132
32- private final Cache <String , ListenableFuture <UserWithHash >> cache ;
33+ private final Cache <String , ListenableFuture <CachedResult >> cache ;
3334 private final ThreadPool threadPool ;
3435 private final boolean authenticationEnabled ;
3536 final Hasher cacheHasher ;
@@ -40,7 +41,7 @@ protected CachingUsernamePasswordRealm(RealmConfig config, ThreadPool threadPool
4041 this .threadPool = threadPool ;
4142 final TimeValue ttl = this .config .getSetting (CachingUsernamePasswordRealmSettings .CACHE_TTL_SETTING );
4243 if (ttl .getNanos () > 0 ) {
43- cache = CacheBuilder .<String , ListenableFuture <UserWithHash >>builder ()
44+ cache = CacheBuilder .<String , ListenableFuture <CachedResult >>builder ()
4445 .setExpireAfterWrite (ttl )
4546 .setMaximumWeight (this .config .getSetting (CachingUsernamePasswordRealmSettings .CACHE_MAX_USERS_SETTING ))
4647 .build ();
@@ -121,58 +122,61 @@ public final void authenticate(AuthenticationToken authToken, ActionListener<Aut
121122 private void authenticateWithCache (UsernamePasswordToken token , ActionListener <AuthenticationResult > listener ) {
122123 try {
123124 final AtomicBoolean authenticationInCache = new AtomicBoolean (true );
124- final ListenableFuture <UserWithHash > listenableCacheEntry = cache .computeIfAbsent (token .principal (), k -> {
125+ final ListenableFuture <CachedResult > listenableCacheEntry = cache .computeIfAbsent (token .principal (), k -> {
125126 authenticationInCache .set (false );
126127 return new ListenableFuture <>();
127128 });
128129 if (authenticationInCache .get ()) {
129130 // there is a cached or an inflight authenticate request
130- listenableCacheEntry .addListener (ActionListener .wrap (authenticatedUserWithHash -> {
131- if (authenticatedUserWithHash != null && authenticatedUserWithHash .verify (token .credentials ())) {
132- // cached credential hash matches the credential hash for this forestalled request
133- handleCachedAuthentication (authenticatedUserWithHash .user , ActionListener .wrap (cacheResult -> {
134- if (cacheResult .isAuthenticated ()) {
135- logger .debug ("realm [{}] authenticated user [{}], with roles [{}]" ,
136- name (), token .principal (), cacheResult .getUser ().roles ());
137- } else {
138- logger .debug ("realm [{}] authenticated user [{}] from cache, but then failed [{}]" ,
139- name (), token .principal (), cacheResult .getMessage ());
140- }
141- listener .onResponse (cacheResult );
142- }, listener ::onFailure ));
131+ listenableCacheEntry .addListener (ActionListener .wrap (cachedResult -> {
132+ final boolean credsMatch = cachedResult .verify (token .credentials ());
133+ if (cachedResult .authenticationResult .isAuthenticated ()) {
134+ if (credsMatch ) {
135+ // cached credential hash matches the credential hash for this forestalled request
136+ handleCachedAuthentication (cachedResult .user , ActionListener .wrap (cacheResult -> {
137+ if (cacheResult .isAuthenticated ()) {
138+ logger .debug ("realm [{}] authenticated user [{}], with roles [{}]" ,
139+ name (), token .principal (), cacheResult .getUser ().roles ());
140+ } else {
141+ logger .debug ("realm [{}] authenticated user [{}] from cache, but then failed [{}]" ,
142+ name (), token .principal (), cacheResult .getMessage ());
143+ }
144+ listener .onResponse (cacheResult );
145+ }, listener ::onFailure ));
146+ } else {
147+ // its credential hash does not match the
148+ // hash of the credential for this forestalled request.
149+ // clear cache and try to reach the authentication source again because password
150+ // might have changed there and the local cached hash got stale
151+ cache .invalidate (token .principal (), listenableCacheEntry );
152+ authenticateWithCache (token , listener );
153+ }
154+ } else if (credsMatch ) {
155+ // not authenticated but instead of hammering reuse the result. a new
156+ // request will trigger a retried auth
157+ listener .onResponse (cachedResult .authenticationResult );
143158 } else {
144- // The inflight request has failed or its credential hash does not match the
145- // hash of the credential for this forestalled request.
146- // clear cache and try to reach the authentication source again because password
147- // might have changed there and the local cached hash got stale
148159 cache .invalidate (token .principal (), listenableCacheEntry );
149160 authenticateWithCache (token , listener );
150161 }
151- }, e -> {
152- // the inflight request failed, so try again, but first (always) make sure cache
153- // is cleared of the failed authentication
154- cache .invalidate (token .principal (), listenableCacheEntry );
155- authenticateWithCache (token , listener );
156- }), threadPool .executor (ThreadPool .Names .GENERIC ), threadPool .getThreadContext ());
162+ }, listener ::onFailure ), threadPool .executor (ThreadPool .Names .GENERIC ), threadPool .getThreadContext ());
157163 } else {
158164 // attempt authentication against the authentication source
159165 doAuthenticate (token , ActionListener .wrap (authResult -> {
160- if (authResult .isAuthenticated () && authResult .getUser ().enabled ()) {
161- // compute the credential hash of this successful authentication request
162- final UserWithHash userWithHash = new UserWithHash (authResult .getUser (), token .credentials (), cacheHasher );
163- // notify any forestalled request listeners; they will not reach to the
164- // authentication request and instead will use this hash for comparison
165- listenableCacheEntry .onResponse (userWithHash );
166- } else {
167- // notify any forestalled request listeners; they will retry the request
168- listenableCacheEntry .onResponse (null );
166+ if (authResult .isAuthenticated () == false || authResult .getUser ().enabled () == false ) {
167+ // a new request should trigger a new authentication
168+ cache .invalidate (token .principal (), listenableCacheEntry );
169169 }
170- // notify the listener of the inflight authentication request; this request is not retried
170+ // notify any forestalled request listeners; they will not reach to the
171+ // authentication request and instead will use this result if they contain
172+ // the same credentials
173+ listenableCacheEntry .onResponse (new CachedResult (authResult , cacheHasher , authResult .getUser (), token .credentials ()));
171174 listener .onResponse (authResult );
172175 }, e -> {
173- // notify any staved off listeners; they will retry the request
176+ cache .invalidate (token .principal (), listenableCacheEntry );
177+ // notify any staved off listeners; they will propagate this error
174178 listenableCacheEntry .onFailure (e );
175- // notify the listener of the inflight authentication request; this request is not retried
179+ // notify the listener of the inflight authentication request
176180 listener .onFailure (e );
177181 }));
178182 }
@@ -223,35 +227,31 @@ public final void lookupUser(String username, ActionListener<User> listener) {
223227 private void lookupWithCache (String username , ActionListener <User > listener ) {
224228 try {
225229 final AtomicBoolean lookupInCache = new AtomicBoolean (true );
226- final ListenableFuture <UserWithHash > listenableCacheEntry = cache .computeIfAbsent (username , key -> {
230+ final ListenableFuture <CachedResult > listenableCacheEntry = cache .computeIfAbsent (username , key -> {
227231 lookupInCache .set (false );
228232 return new ListenableFuture <>();
229233 });
230234 if (false == lookupInCache .get ()) {
231235 // attempt lookup against the user directory
232236 doLookupUser (username , ActionListener .wrap (user -> {
233- if (user != null ) {
234- // user found
235- final UserWithHash userWithHash = new UserWithHash (user , null , null );
236- // notify forestalled request listeners
237- listenableCacheEntry .onResponse (userWithHash );
238- } else {
237+ final CachedResult result = new CachedResult (AuthenticationResult .notHandled (), cacheHasher , user , null );
238+ if (user == null ) {
239239 // user not found, invalidate cache so that subsequent requests are forwarded to
240240 // the user directory
241241 cache .invalidate (username , listenableCacheEntry );
242- // notify forestalled request listeners
243- listenableCacheEntry .onResponse (null );
244242 }
243+ // notify forestalled request listeners
244+ listenableCacheEntry .onResponse (result );
245245 }, e -> {
246246 // the next request should be forwarded, not halted by a failed lookup attempt
247247 cache .invalidate (username , listenableCacheEntry );
248248 // notify forestalled listeners
249249 listenableCacheEntry .onFailure (e );
250250 }));
251251 }
252- listenableCacheEntry .addListener (ActionListener .wrap (userWithHash -> {
253- if (userWithHash != null ) {
254- listener .onResponse (userWithHash .user );
252+ listenableCacheEntry .addListener (ActionListener .wrap (cachedResult -> {
253+ if (cachedResult . user != null ) {
254+ listener .onResponse (cachedResult .user );
255255 } else {
256256 listener .onResponse (null );
257257 }
@@ -263,16 +263,21 @@ private void lookupWithCache(String username, ActionListener<User> listener) {
263263
264264 protected abstract void doLookupUser (String username , ActionListener <User > listener );
265265
266- private static class UserWithHash {
267- final User user ;
268- final char [] hash ;
266+ private static class CachedResult {
267+ private final AuthenticationResult authenticationResult ;
268+ private final User user ;
269+ private final char [] hash ;
269270
270- UserWithHash (User user , SecureString password , Hasher hasher ) {
271- this .user = Objects .requireNonNull (user );
271+ private CachedResult (AuthenticationResult result , Hasher hasher , @ Nullable User user , @ Nullable SecureString password ) {
272+ this .authenticationResult = Objects .requireNonNull (result );
273+ if (authenticationResult .isAuthenticated () && user == null ) {
274+ throw new IllegalArgumentException ("authentication cannot be successful with a null user" );
275+ }
276+ this .user = user ;
272277 this .hash = password == null ? null : hasher .hash (password );
273278 }
274279
275- boolean verify (SecureString password ) {
280+ private boolean verify (SecureString password ) {
276281 return hash != null && Hasher .verifyHash (password , hash );
277282 }
278283 }
0 commit comments