[Security] Check auth scheme case insensitively (#31490)

According to RFC 7617, the Basic authentication scheme name
should not be case sensitive.
Case insensitive comparisons are also applicable for the bearer
tokens where Bearer authentication scheme is used as per
RFC 6750 and RFC 7235

Some Http clients may send authentication scheme names in
different case types for eg. Basic, basic, BASIC, BEARER etc.,
so the lack of case-insensitive check is an issue when these
clients try to authenticate with elasticsearch.

This commit adds case-insensitive checks for Basic and Bearer
authentication schemes.

Closes #31486
This commit is contained in:
Yogesh Gaikwad 2018-06-22 10:15:38 +10:00 committed by GitHub
parent 3b7225e9d1
commit 724438a0b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 30 additions and 10 deletions

View File

@ -5,6 +5,7 @@
*/ */
package org.elasticsearch.xpack.core.security.authc.support; package org.elasticsearch.xpack.core.security.authc.support;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
@ -20,6 +21,8 @@ public class UsernamePasswordToken implements AuthenticationToken {
public static final String BASIC_AUTH_PREFIX = "Basic "; public static final String BASIC_AUTH_PREFIX = "Basic ";
public static final String BASIC_AUTH_HEADER = "Authorization"; public static final String BASIC_AUTH_HEADER = "Authorization";
// authorization scheme check is case-insensitive
private static final boolean IGNORE_CASE_AUTH_HEADER_MATCH = true;
private final String username; private final String username;
private final SecureString password; private final SecureString password;
@ -79,15 +82,15 @@ public class UsernamePasswordToken implements AuthenticationToken {
public static UsernamePasswordToken extractToken(ThreadContext context) { public static UsernamePasswordToken extractToken(ThreadContext context) {
String authStr = context.getHeader(BASIC_AUTH_HEADER); String authStr = context.getHeader(BASIC_AUTH_HEADER);
if (authStr == null) {
return null;
}
return extractToken(authStr); return extractToken(authStr);
} }
private static UsernamePasswordToken extractToken(String headerValue) { private static UsernamePasswordToken extractToken(String headerValue) {
if (headerValue.startsWith(BASIC_AUTH_PREFIX) == false) { if (Strings.isNullOrEmpty(headerValue)) {
return null;
}
if (headerValue.regionMatches(IGNORE_CASE_AUTH_HEADER_MATCH, 0, BASIC_AUTH_PREFIX, 0,
BASIC_AUTH_PREFIX.length()) == false) {
// the header does not start with 'Basic ' so we cannot use it, but it may be valid for another realm // the header does not start with 'Basic ' so we cannot use it, but it may be valid for another realm
return null; return null;
} }

View File

@ -1007,7 +1007,7 @@ public final class TokenService extends AbstractComponent {
*/ */
private String getFromHeader(ThreadContext threadContext) { private String getFromHeader(ThreadContext threadContext) {
String header = threadContext.getHeader("Authorization"); String header = threadContext.getHeader("Authorization");
if (Strings.hasLength(header) && header.startsWith("Bearer ") if (Strings.hasText(header) && header.regionMatches(true, 0, "Bearer ", 0, "Bearer ".length())
&& header.length() > "Bearer ".length()) { && header.length() > "Bearer ".length()) {
return header.substring("Bearer ".length()); return header.substring("Bearer ".length());
} }

View File

@ -71,6 +71,7 @@ import static java.time.Clock.systemUTC;
import static org.elasticsearch.repositories.ESBlobStoreTestCase.randomBytes; import static org.elasticsearch.repositories.ESBlobStoreTestCase.randomBytes;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
@ -162,7 +163,7 @@ public class TokenServiceTests extends ESTestCase {
mockGetTokenFromId(token); mockGetTokenFromId(token);
ThreadContext requestContext = new ThreadContext(Settings.EMPTY); ThreadContext requestContext = new ThreadContext(Settings.EMPTY);
requestContext.putHeader("Authorization", "Bearer " + tokenService.getUserTokenString(token)); requestContext.putHeader("Authorization", randomFrom("Bearer ", "BEARER ", "bearer ") + tokenService.getUserTokenString(token));
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
PlainActionFuture<UserToken> future = new PlainActionFuture<>(); PlainActionFuture<UserToken> future = new PlainActionFuture<>();
@ -183,6 +184,21 @@ public class TokenServiceTests extends ESTestCase {
} }
} }
public void testInvalidAuthorizationHeader() throws Exception {
TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService);
ThreadContext requestContext = new ThreadContext(Settings.EMPTY);
String token = randomFrom("", " ");
String authScheme = randomFrom("Bearer ", "BEARER ", "bearer ", "Basic ");
requestContext.putHeader("Authorization", authScheme + token);
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
tokenService.getAndValidateToken(requestContext, future);
UserToken serialized = future.get();
assertThat(serialized, nullValue());
}
}
public void testRotateKey() throws Exception { public void testRotateKey() throws Exception {
TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService); TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService);
Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null);

View File

@ -45,7 +45,8 @@ public class UsernamePasswordTokenTests extends ESTestCase {
public void testExtractToken() throws Exception { public void testExtractToken() throws Exception {
ThreadContext threadContext = new ThreadContext(Settings.EMPTY); ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
String header = "Basic " + Base64.getEncoder().encodeToString("user1:test123".getBytes(StandardCharsets.UTF_8)); final String header = randomFrom("Basic ", "basic ", "BASIC ")
+ Base64.getEncoder().encodeToString("user1:test123".getBytes(StandardCharsets.UTF_8));
threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header); threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header);
UsernamePasswordToken token = UsernamePasswordToken.extractToken(threadContext); UsernamePasswordToken token = UsernamePasswordToken.extractToken(threadContext);
assertThat(token, notNullValue()); assertThat(token, notNullValue());
@ -54,7 +55,7 @@ public class UsernamePasswordTokenTests extends ESTestCase {
} }
public void testExtractTokenInvalid() throws Exception { public void testExtractTokenInvalid() throws Exception {
String[] invalidValues = { "Basic ", "Basic f" }; final String[] invalidValues = { "Basic ", "Basic f", "basic " };
for (String value : invalidValues) { for (String value : invalidValues) {
ThreadContext threadContext = new ThreadContext(Settings.EMPTY); ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, value); threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, value);
@ -70,7 +71,7 @@ public class UsernamePasswordTokenTests extends ESTestCase {
public void testHeaderNotMatchingReturnsNull() { public void testHeaderNotMatchingReturnsNull() {
ThreadContext threadContext = new ThreadContext(Settings.EMPTY); ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
String header = randomFrom("BasicBroken", "invalid", "Basic"); final String header = randomFrom("Basic", "BasicBroken", "invalid", " basic ");
threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header); threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header);
UsernamePasswordToken extracted = UsernamePasswordToken.extractToken(threadContext); UsernamePasswordToken extracted = UsernamePasswordToken.extractToken(threadContext);
assertThat(extracted, nullValue()); assertThat(extracted, nullValue());