Skip to content

Commit 901c64e

Browse files
authored
Add Debug/Trace logging for authentication (#49619)
Authentication has grown more complex with the addition of new realm types and authentication methods. When user authentication does not behave as expected it can be difficult to determine where and why it failed. This commit adds DEBUG and TRACE logging at key points in the authentication flow so that it is possible to gain addition insight into the operation of the system. Backport of: #49575
1 parent e9ad1a7 commit 901c64e

File tree

3 files changed

+82
-29
lines changed

3 files changed

+82
-29
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java

+18
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,19 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
214214
return builder.endObject();
215215
}
216216

217+
@Override
218+
public String toString() {
219+
StringBuilder builder = new StringBuilder("Authentication[")
220+
.append(user)
221+
.append(",type=").append(type)
222+
.append(",by=").append(authenticatedBy);
223+
if (lookedUpBy != null) {
224+
builder.append(",lookup=").append(lookedUpBy);
225+
}
226+
builder.append("]");
227+
return builder.toString();
228+
}
229+
217230
public static class RealmRef {
218231

219232
private final String nodeName;
@@ -269,6 +282,11 @@ public int hashCode() {
269282
result = 31 * result + type.hashCode();
270283
return result;
271284
}
285+
286+
@Override
287+
public String toString() {
288+
return "{Realm[" + type + "." + name + "] on Node[" + nodeName + "]}";
289+
}
272290
}
273291

274292
public enum AuthenticationType {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java

+58-29
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public AuthenticationService(Settings settings, Realms realms, AuditTrailService
116116
* a user was indeed associated with the request and the credentials were verified to be valid), the method returns
117117
* the user and that user is then "attached" to the request's context.
118118
*
119-
* @param request The request to be authenticated
119+
* @param request The request to be authenticated
120120
*/
121121
public void authenticate(RestRequest request, ActionListener<Authentication> authenticationListener) {
122122
createAuthenticator(request, authenticationListener).authenticateAsync();
@@ -128,12 +128,12 @@ public void authenticate(RestRequest request, ActionListener<Authentication> aut
128128
* the user and that user is then "attached" to the message's context. If no user was found to be attached to the given
129129
* message, then the given fallback user will be returned instead.
130130
*
131-
* @param action The action of the message
132-
* @param message The message to be authenticated
133-
* @param fallbackUser The default user that will be assumed if no other user is attached to the message. Can be
134-
* {@code null}, in which case there will be no fallback user and the success/failure of the
135-
* authentication will be based on the whether there's an attached user to in the message and
136-
* if there is, whether its credentials are valid.
131+
* @param action The action of the message
132+
* @param message The message to be authenticated
133+
* @param fallbackUser The default user that will be assumed if no other user is attached to the message. Can be
134+
* {@code null}, in which case there will be no fallback user and the success/failure of the
135+
* authentication will be based on the whether there's an attached user to in the message and
136+
* if there is, whether its credentials are valid.
137137
*/
138138
public void authenticate(String action, TransportMessage message, User fallbackUser, ActionListener<Authentication> listener) {
139139
createAuthenticator(action, message, fallbackUser, listener).authenticateAsync();
@@ -226,23 +226,25 @@ private Authenticator(AuditableRequest auditableRequest, User fallbackUser, Acti
226226
* these operations are:
227227
*
228228
* <ol>
229-
* <li>look for existing authentication {@link #lookForExistingAuthentication(Consumer)}</li>
230-
* <li>look for a user token</li>
231-
* <li>token extraction {@link #extractToken(Consumer)}</li>
232-
* <li>token authentication {@link #consumeToken(AuthenticationToken)}</li>
233-
* <li>user lookup for run as if necessary {@link #consumeUser(User, Map)} and
234-
* {@link #lookupRunAsUser(User, String, Consumer)}</li>
235-
* <li>write authentication into the context {@link #finishAuthentication(User)}</li>
229+
* <li>look for existing authentication {@link #lookForExistingAuthentication(Consumer)}</li>
230+
* <li>look for a user token</li>
231+
* <li>token extraction {@link #extractToken(Consumer)}</li>
232+
* <li>token authentication {@link #consumeToken(AuthenticationToken)}</li>
233+
* <li>user lookup for run as if necessary {@link #consumeUser(User, Map)} and
234+
* {@link #lookupRunAsUser(User, String, Consumer)}</li>
235+
* <li>write authentication into the context {@link #finishAuthentication(User)}</li>
236236
* </ol>
237237
*/
238238
private void authenticateAsync() {
239239
if (defaultOrderedRealmList.isEmpty()) {
240240
// this happens when the license state changes between the call to authenticate and the actual invocation
241241
// to get the realm list
242+
logger.debug("No realms available, failing authentication");
242243
listener.onResponse(null);
243244
} else {
244245
lookForExistingAuthentication((authentication) -> {
245246
if (authentication != null) {
247+
logger.trace("Found existing authentication [{}] in request [{}]", authentication, request);
246248
listener.onResponse(authentication);
247249
} else {
248250
tokenService.getAndValidateToken(threadContext, ActionListener.wrap(userToken -> {
@@ -252,6 +254,7 @@ private void authenticateAsync() {
252254
checkForApiKey();
253255
}
254256
}, e -> {
257+
logger.debug(new ParameterizedMessage("Failed to validate token authentication for request [{}]", request), e);
255258
if (e instanceof ElasticsearchSecurityException &&
256259
tokenService.isExpiredTokenException((ElasticsearchSecurityException) e) == false) {
257260
// intentionally ignore the returned exception; we call this primarily
@@ -275,6 +278,7 @@ private void checkForApiKey() {
275278
} else if (authResult.getStatus() == AuthenticationResult.Status.TERMINATE) {
276279
Exception e = (authResult.getException() != null) ? authResult.getException()
277280
: Exceptions.authenticationError(authResult.getMessage());
281+
logger.debug(new ParameterizedMessage("API key service terminated authentication for request [{}]", request), e);
278282
listener.onFailure(e);
279283
} else {
280284
if (authResult.getMessage() != null) {
@@ -308,7 +312,7 @@ private void lookForExistingAuthentication(Consumer<Authentication> authenticati
308312
} catch (Exception e) {
309313
logger.error((Supplier<?>)
310314
() -> new ParameterizedMessage("caught exception while trying to read authentication from request [{}]", request),
311-
e);
315+
e);
312316
action = () -> listener.onFailure(request.tamperedRequest());
313317
}
314318

@@ -332,6 +336,8 @@ void extractToken(Consumer<AuthenticationToken> consumer) {
332336
for (Realm realm : defaultOrderedRealmList) {
333337
final AuthenticationToken token = realm.token(threadContext);
334338
if (token != null) {
339+
logger.trace("Found authentication credentials [{}] for principal [{}] in request [{}]",
340+
token.getClass().getName(), token.principal(), request);
335341
action = () -> consumer.accept(token);
336342
break;
337343
}
@@ -358,12 +364,17 @@ private void consumeToken(AuthenticationToken token) {
358364
} else {
359365
authenticationToken = token;
360366
final List<Realm> realmsList = getRealmList(authenticationToken.principal());
367+
logger.trace("Checking token of type [{}] against [{}] realm(s)", token.getClass().getName(), realmsList.size());
361368
final long startInvalidation = numInvalidation.get();
362369
final Map<Realm, Tuple<String, Exception>> messages = new LinkedHashMap<>();
363370
final BiConsumer<Realm, ActionListener<User>> realmAuthenticatingConsumer = (realm, userListener) -> {
364371
if (realm.supports(authenticationToken)) {
372+
logger.trace("Trying to authenticate [{}] using realm [{}] with token [{}] ",
373+
token.principal(), realm, token.getClass().getName());
365374
realm.authenticate(authenticationToken, ActionListener.wrap((result) -> {
366375
assert result != null : "Realm " + realm + " produced a null authentication result";
376+
logger.debug("Authentication of [{}] using realm [{}] with token [{}] was [{}]",
377+
token.principal(), realm, token.getClass().getSimpleName(), result);
367378
if (result.getStatus() == AuthenticationResult.Status.SUCCESS) {
368379
// user was authenticated, populate the authenticated by information
369380
authenticatedBy = new RealmRef(realm.name(), realm.type(), nodeName);
@@ -377,9 +388,9 @@ private void consumeToken(AuthenticationToken token) {
377388
request.realmAuthenticationFailed(authenticationToken, realm.name());
378389
if (result.getStatus() == AuthenticationResult.Status.TERMINATE) {
379390
logger.info("Authentication of [{}] was terminated by realm [{}] - {}",
380-
authenticationToken.principal(), realm.name(), result.getMessage());
391+
authenticationToken.principal(), realm.name(), result.getMessage());
381392
Exception e = (result.getException() != null) ? result.getException()
382-
: Exceptions.authenticationError(result.getMessage());
393+
: Exceptions.authenticationError(result.getMessage());
383394
userListener.onFailure(e);
384395
} else {
385396
if (result.getMessage() != null) {
@@ -390,8 +401,8 @@ private void consumeToken(AuthenticationToken token) {
390401
}
391402
}, (ex) -> {
392403
logger.warn(new ParameterizedMessage(
393-
"An error occurred while attempting to authenticate [{}] against realm [{}]",
394-
authenticationToken.principal(), realm.name()), ex);
404+
"An error occurred while attempting to authenticate [{}] against realm [{}]",
405+
authenticationToken.principal(), realm.name()), ex);
395406
userListener.onFailure(ex);
396407
}));
397408
} else {
@@ -407,6 +418,8 @@ private void consumeToken(AuthenticationToken token) {
407418
try {
408419
authenticatingListener.run();
409420
} catch (Exception e) {
421+
logger.debug(new ParameterizedMessage("Authentication of [{}] with token [{}] failed",
422+
token.principal(), token.getClass().getName()), e);
410423
listener.onFailure(request.exceptionProcessingRequest(e, token));
411424
}
412425
}
@@ -445,23 +458,25 @@ private List<Realm> getRealmList(String principal) {
445458
* Handles failed extraction of an authentication token. This can happen in a few different scenarios:
446459
*
447460
* <ul>
448-
* <li>this is an initial request from a client without preemptive authentication, so we must return an authentication
449-
* challenge</li>
450-
* <li>this is a request made internally within a node and there is a fallback user, which is typically the
451-
* {@link SystemUser}</li>
452-
* <li>anonymous access is enabled and this will be considered an anonymous request</li>
461+
* <li>this is an initial request from a client without preemptive authentication, so we must return an authentication
462+
* challenge</li>
463+
* <li>this is a request made internally within a node and there is a fallback user, which is typically the
464+
* {@link SystemUser}</li>
465+
* <li>anonymous access is enabled and this will be considered an anonymous request</li>
453466
* </ul>
454-
*
467+
* <p>
455468
* Regardless of the scenario, this method will call the listener with either failure or success.
456469
*/
457470
// pkg-private for tests
458471
void handleNullToken() {
459472
final Authentication authentication;
460473
if (fallbackUser != null) {
474+
logger.trace("No valid credentials found in request [{}], using fallback [{}]", request, fallbackUser.principal());
461475
RealmRef authenticatedBy = new RealmRef("__fallback", "__fallback", nodeName);
462476
authentication = new Authentication(fallbackUser, authenticatedBy, null, Version.CURRENT, AuthenticationType.INTERNAL,
463477
Collections.emptyMap());
464478
} else if (isAnonymousUserEnabled) {
479+
logger.trace("No valid credentials found in request [{}], using anonymous [{}]", request, anonymousUser.principal());
465480
RealmRef authenticatedBy = new RealmRef("__anonymous", "__anonymous", nodeName);
466481
authentication = new Authentication(anonymousUser, authenticatedBy, null, Version.CURRENT, AuthenticationType.ANONYMOUS,
467482
Collections.emptyMap());
@@ -473,7 +488,10 @@ void handleNullToken() {
473488
if (authentication != null) {
474489
action = () -> writeAuthToContext(authentication);
475490
} else {
476-
action = () -> listener.onFailure(request.anonymousAccessDenied());
491+
action = () -> {
492+
logger.debug("No valid credentials found in request [{}], rejecting", request);
493+
listener.onFailure(request.anonymousAccessDenied());
494+
};
477495
}
478496

479497
// we assign the listener call to an action to avoid calling the listener within a try block and auditing the wrong thing when
@@ -501,6 +519,7 @@ private void consumeUser(User user, Map<Realm, Tuple<String, Exception>> message
501519
Strings.collectionToCommaDelimitedString(defaultOrderedRealmList),
502520
Strings.collectionToCommaDelimitedString(unlicensedRealms));
503521
}
522+
logger.trace("Failed to authenticate request [{}]", request);
504523
listener.onFailure(request.authenticationFailed(authenticationToken));
505524
} else {
506525
threadContext.putTransient(AuthenticationResult.THREAD_CONTEXT_KEY, authenticationResult);
@@ -514,7 +533,7 @@ private void consumeUser(User user, Map<Realm, Tuple<String, Exception>> message
514533
assert runAsUsername.isEmpty() : "the run as username may not be empty";
515534
logger.debug("user [{}] attempted to runAs with an empty username", user.principal());
516535
listener.onFailure(request.runAsDenied(
517-
new Authentication(new User(runAsUsername, null, user), authenticatedBy, lookedupBy), authenticationToken));
536+
new Authentication(new User(runAsUsername, null, user), authenticatedBy, lookedupBy), authenticationToken));
518537
}
519538
} else {
520539
finishAuthentication(user);
@@ -528,10 +547,12 @@ private void consumeUser(User user, Map<Realm, Tuple<String, Exception>> message
528547
* names of users that exist using a timing attack
529548
*/
530549
private void lookupRunAsUser(final User user, String runAsUsername, Consumer<User> userConsumer) {
550+
logger.trace("Looking up run-as user [{}] for authenticated user [{}]", runAsUsername, user.principal());
531551
final RealmUserLookup lookup = new RealmUserLookup(getRealmList(runAsUsername), threadContext);
532552
final long startInvalidationNum = numInvalidation.get();
533553
lookup.lookup(runAsUsername, ActionListener.wrap(tuple -> {
534554
if (tuple == null) {
555+
logger.debug("Cannot find run-as user [{}] for authenticated user [{}]", runAsUsername, user.principal());
535556
// the user does not exist, but we still create a User object, which will later be rejected by authz
536557
userConsumer.accept(new User(runAsUsername, null, user));
537558
} else {
@@ -543,6 +564,7 @@ private void lookupRunAsUser(final User user, String runAsUsername, Consumer<Use
543564
// this might provide a valid hint
544565
lastSuccessfulAuthCache.computeIfAbsent(runAsUsername, s -> realm);
545566
}
567+
logger.trace("Using run-as user [{}] with authenticated user [{}]", foundUser, user.principal());
546568
userConsumer.accept(new User(foundUser, user));
547569
}
548570
}, exception -> listener.onFailure(request.exceptionProcessingRequest(exception, authenticationToken))));
@@ -569,11 +591,18 @@ void finishAuthentication(User finalUser) {
569591
*/
570592
void writeAuthToContext(Authentication authentication) {
571593
request.authenticationSuccess(authentication.getAuthenticatedBy().getName(), authentication.getUser());
572-
Runnable action = () -> listener.onResponse(authentication);
594+
Runnable action = () -> {
595+
logger.trace("Established authentication [{}] for request [{}]", authentication, request);
596+
listener.onResponse(authentication);
597+
};
573598
try {
574599
authentication.writeToContext(threadContext);
575600
} catch (Exception e) {
576-
action = () -> listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken));
601+
action = () -> {
602+
logger.debug(
603+
new ParameterizedMessage("Failed to store authentication [{}] for request [{}]", authentication, request), e);
604+
listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken));
605+
};
577606
}
578607

579608
// we assign the listener call to an action to avoid calling the listener within a try block and auditing the wrong thing

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java

+6
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,15 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c
5454
}
5555
service.authenticate(maybeWrapRestRequest(request), ActionListener.wrap(
5656
authentication -> {
57+
if (authentication == null) {
58+
logger.trace("No authentication available for REST request [{}]", request.uri());
59+
} else {
60+
logger.trace("Authenticated REST request [{}] as {}", request.uri(), authentication);
61+
}
5762
RemoteHostHeader.process(request, threadContext);
5863
restHandler.handleRequest(request, channel, client);
5964
}, e -> {
65+
logger.debug(new ParameterizedMessage("Authentication failed for REST request [{}]", request.uri()), e);
6066
try {
6167
channel.sendResponse(new BytesRestResponse(channel, e));
6268
} catch (Exception inner) {

0 commit comments

Comments
 (0)