shield: do not throw exception if authorization header is not a basic token

Custom realms may enable the use of other authorization schemes than just basic authentication
and these schemes should work in addition to our built in realms. However, our built in realms use
the UsernamePasswordToken class to parse the Authorization header, which had a check to ensure
the token was for basic authentication and if not, an exception was thrown. The throwing of the
exception stops the authentication process and prevents custom realms from evaluating the header
if they come later in the ordering of realms.

This change removes the throwing of the exception unless the header starts with 'Basic ' and is invalid.

Original commit: elastic/x-pack-elasticsearch@fd438ded95
This commit is contained in:
jaymode 2016-02-11 08:07:09 -05:00
parent 2fdfec0ae8
commit 95a8f77146
3 changed files with 22 additions and 59 deletions

View File

@ -22,7 +22,7 @@ public abstract class UsernamePasswordRealm extends Realm<UsernamePasswordToken>
@Override
public UsernamePasswordToken token(ThreadContext threadContext) {
return UsernamePasswordToken.extractToken(threadContext, null);
return UsernamePasswordToken.extractToken(threadContext);
}
public boolean supports(AuthenticationToken token) {

View File

@ -7,15 +7,12 @@ package org.elasticsearch.shield.authc.support;
import org.elasticsearch.common.Base64;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.shield.authc.AuthenticationToken;
import java.io.IOException;
import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.elasticsearch.shield.support.Exceptions.authenticationError;
@ -25,7 +22,7 @@ import static org.elasticsearch.shield.support.Exceptions.authenticationError;
public class UsernamePasswordToken implements AuthenticationToken {
public static final String BASIC_AUTH_HEADER = "Authorization";
private static final Pattern BASIC_AUTH_PATTERN = Pattern.compile("Basic\\s(.+)");
private static final String BASIC_AUTH_PREFIX = "Basic ";
private final String username;
private final SecuredString password;
@ -66,34 +63,30 @@ public class UsernamePasswordToken implements AuthenticationToken {
return Objects.hash(username, password.hashCode());
}
public static UsernamePasswordToken extractToken(ThreadContext context, UsernamePasswordToken defaultToken) {
public static UsernamePasswordToken extractToken(ThreadContext context) {
String authStr = context.getHeader(BASIC_AUTH_HEADER);
if (authStr == null) {
return defaultToken;
return null;
}
return extractToken(authStr);
}
public static UsernamePasswordToken extractToken(RestRequest request, UsernamePasswordToken defaultToken) {
String authStr = request.header(BASIC_AUTH_HEADER);
if (authStr == null) {
return defaultToken;
private static UsernamePasswordToken extractToken(String headerValue) {
if (headerValue.startsWith(BASIC_AUTH_PREFIX) == false) {
// the header does not start with 'Basic ' so we cannot use it, but it may be valid for another realm
return null;
}
return extractToken(authStr);
}
static UsernamePasswordToken extractToken(String token) {
Matcher matcher = BASIC_AUTH_PATTERN.matcher(token.trim());
if (!matcher.matches()) {
// if there is nothing after the prefix, the header is bad
if (headerValue.length() == BASIC_AUTH_PREFIX.length()) {
throw authenticationError("invalid basic authentication header value");
}
char[] userpasswd;
try {
userpasswd = CharArrays.utf8BytesToChars(Base64.decode(matcher.group(1)));
} catch (IllegalArgumentException|IOException e) {
userpasswd = CharArrays.utf8BytesToChars(Base64.decode(headerValue.substring(BASIC_AUTH_PREFIX.length()).trim()));
} catch (IllegalArgumentException | IOException e) {
throw authenticationError("invalid basic authentication header encoding", e);
}

View File

@ -9,7 +9,6 @@ import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.common.Base64;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.test.ESTestCase;
import org.junit.Rule;
import org.junit.rules.ExpectedException;
@ -21,8 +20,6 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
*
@ -51,19 +48,19 @@ public class UsernamePasswordTokenTests extends ESTestCase {
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
String header = "Basic " + Base64.encodeBytes("user1:test123".getBytes(StandardCharsets.UTF_8));
threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header);
UsernamePasswordToken token = UsernamePasswordToken.extractToken(threadContext, null);
UsernamePasswordToken token = UsernamePasswordToken.extractToken(threadContext);
assertThat(token, notNullValue());
assertThat(token.principal(), equalTo("user1"));
assertThat(new String(token.credentials().internalChars()), equalTo("test123"));
}
public void testExtractTokenInvalid() throws Exception {
String[] invalidValues = { "Basic", "Basic ", "Basic f" };
String[] invalidValues = { "Basic ", "Basic f" };
for (String value : invalidValues) {
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, value);
try {
UsernamePasswordToken.extractToken(threadContext, null);
UsernamePasswordToken.extractToken(threadContext);
fail("Expected an authentication exception for invalid basic auth token [" + value + "]");
} catch (ElasticsearchSecurityException e) {
// expected
@ -72,44 +69,17 @@ public class UsernamePasswordTokenTests extends ESTestCase {
}
}
public void testThatAuthenticationExceptionContainsResponseHeaders() {
public void testHeaderNotMatchingReturnsNull() {
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
String header = "BasicBroken";
String header = randomFrom("BasicBroken", "invalid", "Basic");
threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header);
try {
UsernamePasswordToken.extractToken(threadContext, null);
fail("Expected exception but did not happen");
} catch (ElasticsearchSecurityException e) {
assertAuthenticationException(e);
}
UsernamePasswordToken extracted = UsernamePasswordToken.extractToken(threadContext);
assertThat(extracted, nullValue());
}
public void testExtractTokenRest() throws Exception {
RestRequest request = mock(RestRequest.class);
UsernamePasswordToken token = new UsernamePasswordToken("username", SecuredStringTests.build("changeme"));
when(request.header(UsernamePasswordToken.BASIC_AUTH_HEADER)).thenReturn(UsernamePasswordToken.basicAuthHeaderValue("username",
SecuredStringTests.build("changeme")));
assertThat(UsernamePasswordToken.extractToken(request, null), equalTo(token));
}
public void testExtractTokenRestMissing() throws Exception {
RestRequest request = mock(RestRequest.class);
when(request.header(UsernamePasswordToken.BASIC_AUTH_HEADER)).thenReturn(null);
assertThat(UsernamePasswordToken.extractToken(request, null), nullValue());
}
public void testExtractTokenRestWithInvalidToken1() throws Exception {
thrown.expect(ElasticsearchSecurityException.class);
RestRequest request = mock(RestRequest.class);
when(request.header(UsernamePasswordToken.BASIC_AUTH_HEADER)).thenReturn("invalid");
UsernamePasswordToken.extractToken(request, null);
}
public void testExtractTokenRestWithInvalidToken2() throws Exception {
thrown.expect(ElasticsearchSecurityException.class);
RestRequest request = mock(RestRequest.class);
when(request.header(UsernamePasswordToken.BASIC_AUTH_HEADER)).thenReturn("Basic");
UsernamePasswordToken.extractToken(request, null);
public void testExtractTokenMissing() throws Exception {
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
assertThat(UsernamePasswordToken.extractToken(threadContext), nullValue());
}
public void testEqualsWithDifferentPasswords() {