[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;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.util.concurrent.ThreadContext;
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_HEADER = "Authorization";
// authorization scheme check is case-insensitive
private static final boolean IGNORE_CASE_AUTH_HEADER_MATCH = true;
private final String username;
private final SecureString password;
@ -79,15 +82,15 @@ public class UsernamePasswordToken implements AuthenticationToken {
public static UsernamePasswordToken extractToken(ThreadContext context) {
String authStr = context.getHeader(BASIC_AUTH_HEADER);
if (authStr == null) {
return null;
}
return extractToken(authStr);
}
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
return null;
}

View File

@ -1007,7 +1007,7 @@ public final class TokenService extends AbstractComponent {
*/
private String getFromHeader(ThreadContext threadContext) {
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()) {
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.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
@ -162,7 +163,7 @@ public class TokenServiceTests extends ESTestCase {
mockGetTokenFromId(token);
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)) {
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 {
TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService);
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 {
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);
UsernamePasswordToken token = UsernamePasswordToken.extractToken(threadContext);
assertThat(token, notNullValue());
@ -54,7 +55,7 @@ public class UsernamePasswordTokenTests extends ESTestCase {
}
public void testExtractTokenInvalid() throws Exception {
String[] invalidValues = { "Basic ", "Basic f" };
final String[] invalidValues = { "Basic ", "Basic f", "basic " };
for (String value : invalidValues) {
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, value);
@ -70,7 +71,7 @@ public class UsernamePasswordTokenTests extends ESTestCase {
public void testHeaderNotMatchingReturnsNull() {
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);
UsernamePasswordToken extracted = UsernamePasswordToken.extractToken(threadContext);
assertThat(extracted, nullValue());