[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:
parent
3b7225e9d1
commit
724438a0b0
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in New Issue