[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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
Loading…
Reference in New Issue