Skip to content

Commit 9ef45d3

Browse files
committed
Don't fallback to anonymous for tokens/apikeys (elastic#51042)
This commit changes our behavior so that when we receive a request with an invalid/expired/wrong access token or API Key we do not fallback to authenticating as the anonymous user even if anonymous access is enabled for Elasticsearch.
1 parent 790cac2 commit 9ef45d3

File tree

2 files changed

+70
-1
lines changed

2 files changed

+70
-1
lines changed

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

+16-1
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ private List<Realm> getRealmList(String principal) {
460460
* <ul>
461461
* <li>this is an initial request from a client without preemptive authentication, so we must return an authentication
462462
* challenge</li>
463+
* <li>this is a request that contained an Authorization Header that we can't validate </li>
463464
* <li>this is a request made internally within a node and there is a fallback user, which is typically the
464465
* {@link SystemUser}</li>
465466
* <li>anonymous access is enabled and this will be considered an anonymous request</li>
@@ -475,7 +476,7 @@ void handleNullToken() {
475476
RealmRef authenticatedBy = new RealmRef("__fallback", "__fallback", nodeName);
476477
authentication = new Authentication(fallbackUser, authenticatedBy, null, Version.CURRENT, AuthenticationType.INTERNAL,
477478
Collections.emptyMap());
478-
} else if (isAnonymousUserEnabled) {
479+
} else if (isAnonymousUserEnabled && shouldFallbackToAnonymous()) {
479480
logger.trace("No valid credentials found in request [{}], using anonymous [{}]", request, anonymousUser.principal());
480481
RealmRef authenticatedBy = new RealmRef("__anonymous", "__anonymous", nodeName);
481482
authentication = new Authentication(anonymousUser, authenticatedBy, null, Version.CURRENT, AuthenticationType.ANONYMOUS,
@@ -499,6 +500,20 @@ void handleNullToken() {
499500
action.run();
500501
}
501502

503+
/**
504+
* When an API Key or an Elasticsearch Token Service token is used for authentication and authentication fails (as indicated by
505+
* a null AuthenticationToken) we should not fallback to the anonymous user.
506+
*/
507+
boolean shouldFallbackToAnonymous(){
508+
String header = threadContext.getHeader("Authorization");
509+
if (Strings.hasText(header) &&
510+
((header.regionMatches(true, 0, "Bearer ", 0, "Bearer ".length()) && header.length() > "Bearer ".length()) ||
511+
(header.regionMatches(true, 0, "ApiKey ", 0, "ApiKey ".length()) && header.length() > "ApiKey ".length()))) {
512+
return false;
513+
}
514+
return true;
515+
}
516+
502517
/**
503518
* Consumes the {@link User} that resulted from attempting to authenticate a token against the {@link Realms}. When the user is
504519
* {@code null}, authentication fails and does not proceed. When there is a user, the request is inspected to see if the run as

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java

+54
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
import java.util.concurrent.atomic.AtomicBoolean;
100100
import java.util.function.Consumer;
101101

102+
import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_PRIMARY_TERM;
102103
import static org.elasticsearch.test.SecurityTestsUtils.assertAuthenticationException;
103104
import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError;
104105
import static org.elasticsearch.xpack.security.authc.TokenServiceTests.mockGetTokenFromId;
@@ -744,6 +745,59 @@ public void testAuthenticateTamperedUser() throws Exception {
744745
}
745746
}
746747

748+
public void testWrongTokenDoesNotFallbackToAnonymous() {
749+
String username = randomBoolean() ? AnonymousUser.DEFAULT_ANONYMOUS_USERNAME : "user1";
750+
Settings.Builder builder = Settings.builder()
751+
.putList(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3");
752+
if (username.equals(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME) == false) {
753+
builder.put(AnonymousUser.USERNAME_SETTING.getKey(), username);
754+
}
755+
Settings anonymousEnabledSettings = builder.build();
756+
final AnonymousUser anonymousUser = new AnonymousUser(anonymousEnabledSettings);
757+
service = new AuthenticationService(anonymousEnabledSettings, realms, auditTrail,
758+
new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, tokenService, apiKeyService);
759+
760+
try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
761+
final String reqId = AuditUtil.getOrGenerateRequestId(threadContext);
762+
threadContext.putHeader("Authorization", "Bearer thisisaninvalidtoken");
763+
ElasticsearchSecurityException e =
764+
expectThrows(ElasticsearchSecurityException.class, () -> authenticateBlocking("_action", message, null));
765+
verify(auditTrail).anonymousAccessDenied(reqId, "_action", message);
766+
verifyNoMoreInteractions(auditTrail);
767+
assertAuthenticationException(e);
768+
}
769+
}
770+
771+
public void testWrongApiKeyDoesNotFallbackToAnonymous() {
772+
String username = randomBoolean() ? AnonymousUser.DEFAULT_ANONYMOUS_USERNAME : "user1";
773+
Settings.Builder builder = Settings.builder()
774+
.putList(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3");
775+
if (username.equals(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME) == false) {
776+
builder.put(AnonymousUser.USERNAME_SETTING.getKey(), username);
777+
}
778+
Settings anonymousEnabledSettings = builder.build();
779+
final AnonymousUser anonymousUser = new AnonymousUser(anonymousEnabledSettings);
780+
service = new AuthenticationService(anonymousEnabledSettings, realms, auditTrail,
781+
new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, tokenService, apiKeyService);
782+
doAnswer(invocationOnMock -> {
783+
final GetRequest request = (GetRequest) invocationOnMock.getArguments()[0];
784+
final ActionListener<GetResponse> listener = (ActionListener<GetResponse>) invocationOnMock.getArguments()[1];
785+
listener.onResponse(new GetResponse(new GetResult(request.index(), request.id(),
786+
SequenceNumbers.UNASSIGNED_SEQ_NO, UNASSIGNED_PRIMARY_TERM, -1L, false, null,
787+
Collections.emptyMap(), Collections.emptyMap())));
788+
return Void.TYPE;
789+
}).when(client).get(any(GetRequest.class), any(ActionListener.class));
790+
try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
791+
final String reqId = AuditUtil.getOrGenerateRequestId(threadContext);
792+
threadContext.putHeader("Authorization", "ApiKey dGhpc2lzYW5pbnZhbGlkaWQ6dGhpc2lzYW5pbnZhbGlkc2VjcmV0");
793+
ElasticsearchSecurityException e =
794+
expectThrows(ElasticsearchSecurityException.class, () -> authenticateBlocking("_action", message, null));
795+
verify(auditTrail).anonymousAccessDenied(reqId, "_action", message);
796+
verifyNoMoreInteractions(auditTrail);
797+
assertAuthenticationException(e);
798+
}
799+
}
800+
747801
public void testAnonymousUserRest() throws Exception {
748802
String username = randomBoolean() ? AnonymousUser.DEFAULT_ANONYMOUS_USERNAME : "user1";
749803
Settings.Builder builder = Settings.builder()

0 commit comments

Comments
 (0)