blob: 2cc7634d40e39b2df1a52d0e563cf3823422d454 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina.authenticator;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.Assert;
import org.junit.Test;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.codec.binary.Base64;
/**
* Test the BasicAuthenticator's BasicCredentials inner class and the
* associated Base64 decoder.
*/
public class TestBasicAuthParser {
private static final String NICE_METHOD = "Basic";
private static final String USER_NAME = "userid";
private static final String PASSWORD = "secret";
/*
* test cases with good BASIC Auth credentials - Base64 strings
* can have zero, one or two trailing pad characters
*/
@Test
public void testGoodCredentials() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
@Test
public void testGoodCredentialsNoPassword() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, null);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertNull(credentials.getPassword());
}
@Test
public void testGoodCrib() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA==";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
@Test
public void testGoodCribUserOnly() throws Exception {
final String BASE64_CRIB = "dXNlcmlk";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertNull(credentials.getPassword());
}
@Test
public void testGoodCribOnePad() throws Exception {
final String PASSWORD1 = "secrets";
final String BASE64_CRIB = "dXNlcmlkOnNlY3JldHM=";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD1, credentials.getPassword());
}
/*
* RFC 2045 says the Base64 encoded string should be represented
* as lines of no more than 76 characters. However, RFC 2617
* says a base64-user-pass token is not limited to 76 char/line.
* It also says all line breaks, including mandatory ones,
* should be ignored during decoding.
* This test case has a line break in the Base64 string.
* (See also testGoodCribBase64Big below).
*/
@Test
public void testGoodCribLineWrap() throws Exception {
final String USER_LONG = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz0123456789+/AAAABBBBCCCC"
+ "DDDD"; // 80 characters
final String BASE64_CRIB = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldY"
+ "WVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0"
+ "\n" + "NTY3ODkrL0FBQUFCQkJCQ0NDQ0REREQ=";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_LONG, credentials.getUsername());
}
/*
* RFC 2045 says the Base64 encoded string should be represented
* as lines of no more than 76 characters. However, RFC 2617
* says a base64-user-pass token is not limited to 76 char/line.
*/
@Test
public void testGoodCribBase64Big() throws Exception {
// Our decoder accepts a long token without complaint.
final String USER_LONG = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz0123456789+/AAAABBBBCCCC"
+ "DDDD"; // 80 characters
final String BASE64_CRIB = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldY"
+ "WVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0"
+ "NTY3ODkrL0FBQUFCQkJCQ0NDQ0REREQ="; // no new line
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_LONG, credentials.getUsername());
}
/*
* verify the parser follows RFC2617 by treating the auth-scheme
* token as case-insensitive.
*/
@Test
public void testAuthMethodCaseBasic() throws Exception {
final String METHOD = "bAsIc";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(METHOD, USER_NAME, PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* Confirm the Basic parser rejects an invalid authentication method.
*/
@Test
public void testAuthMethodBadMethod() throws Exception {
final String METHOD = "BadMethod";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(METHOD, USER_NAME, PASSWORD);
@SuppressWarnings("unused")
BasicAuthenticator.BasicCredentials credentials = null;
try {
credentials = new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.fail("IllegalArgumentException expected");
}
catch (Exception e) {
Assert.assertTrue(e instanceof IllegalArgumentException);
Assert.assertTrue(e.getMessage().contains("header method"));
}
}
/*
* Confirm the Basic parser tolerates excess white space after
* the authentication method.
*
* RFC2617 does not define the separation syntax between the auth-scheme
* and basic-credentials tokens. Tomcat tolerates any amount of white
* (within the limits of HTTP header sizes).
*/
@Test
public void testAuthMethodExtraLeadingSpace() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD + " ", USER_NAME, PASSWORD);
final BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* invalid decoded credentials cases
*/
@Test
public void testWrongPassword() throws Exception {
final String PWD_WRONG = "wrong";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PWD_WRONG);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertNotSame(PASSWORD, credentials.getPassword());
}
@Test
public void testMissingUsername() throws Exception {
final String EMPTY_USER_NAME = "";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, EMPTY_USER_NAME, PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(EMPTY_USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
@Test
public void testShortUsername() throws Exception {
final String SHORT_USER_NAME = "a";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, SHORT_USER_NAME, PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(SHORT_USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
@Test
public void testShortPassword() throws Exception {
final String SHORT_PASSWORD = "a";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, SHORT_PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(SHORT_PASSWORD, credentials.getPassword());
}
@Test
public void testPasswordHasSpaceEmbedded() throws Exception {
final String PASSWORD_SPACE = "abc def";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_SPACE);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD_SPACE, credentials.getPassword());
}
@Test
public void testPasswordHasColonEmbedded() throws Exception {
final String PASSWORD_COLON = "abc:def";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_COLON);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD_COLON, credentials.getPassword());
}
@Test
public void testPasswordHasColonLeading() throws Exception {
final String PASSWORD_COLON = ":abcdef";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_COLON);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD_COLON, credentials.getPassword());
}
@Test
public void testPasswordHasColonTrailing() throws Exception {
final String PASSWORD_COLON = "abcdef:";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_COLON);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD_COLON, credentials.getPassword());
}
/*
* Confirm the Basic parser tolerates excess white space after
* the base64 blob.
*
* RFC2617 does not define this case, but asks servers to be
* tolerant of this kind of client deviation.
*/
@Test
public void testAuthMethodExtraTrailingSpace() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD, " ");
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* Confirm the Basic parser tolerates excess white space around
* the username inside the base64 blob.
*
* RFC2617 does not define the separation syntax between the auth-scheme
* and basic-credentials tokens. Tomcat should tolerate any reasonable
* amount of white space.
*/
@Test
public void testUserExtraSpace() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, " " + USER_NAME + " ", PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* Confirm the Basic parser tolerates excess white space around
* the username within the base64 blob.
*
* RFC2617 does not define the separation syntax between the auth-scheme
* and basic-credentials tokens. Tomcat should tolerate any reasonable
* amount of white space.
*/
@Test
public void testPasswordExtraSpace() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, " " + PASSWORD + " ");
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* invalid base64 string tests
*
* Refer to RFC2045 section 6.8.
*/
/*
* non-trailing "=" should trigger premature termination of the
* decoder, returning a truncated string that will eventually
* result in an authentication Assert.failure.
*/
@Test
public void testBadBase64InlineEquals() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNlY3J=dAo=";
final String TRUNCATED_PWD = "secr";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertNotSame(PASSWORD, credentials.getPassword());
Assert.assertEquals(TRUNCATED_PWD, credentials.getPassword());
}
/*
* "-" is not a legal base64 character. The RFC says it must be
* ignored by the decoder. This will scramble the decoded string
* and eventually result in an authentication Assert.failure.
*/
@Test
public void testBadBase64Char() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNl-3JldHM=";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertNotSame(PASSWORD, credentials.getPassword());
}
/*
* "-" is not a legal base64 character. The RFC says it must be
* ignored by the decoder. This is a very strange case because the
* next character is a pad, which terminates the string normally.
* It is likely (but not certain) the decoded password will be
* damaged and subsequent authentication will fail.
*/
@Test
public void testBadBase64LastChar() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA-=";
final String POSSIBLY_DAMAGED_PWD = "secret";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(POSSIBLY_DAMAGED_PWD, credentials.getPassword());
}
/*
* The trailing third "=" is illegal. However, the RFC says the decoder
* must terminate as soon as the first pad is detected, so no error
* will be detected unless the payload has been damaged in some way.
*/
@Test
public void testBadBase64TooManyEquals() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA===";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* there should be a multiple of 4 encoded characters. However,
* the RFC says the decoder should pad the input string with
* zero bits out to the next boundary. An error will not be detected
* unless the payload has been damaged in some way - this
* particular crib has no damage.
*/
@Test
public void testBadBase64BadLength() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader());
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* Encapsulate the logic to generate an HTTP header
* for BASIC Authentication.
* Note: only used internally, so no need to validate arguments.
*/
private final class BasicAuthHeader {
private final String HTTP_AUTH = "authorization: ";
private final byte[] HEADER =
HTTP_AUTH.getBytes(StandardCharsets.ISO_8859_1);
private ByteChunk authHeader;
private int initialOffset = 0;
/*
* This method creates a valid base64 blob
*/
private BasicAuthHeader(String method, String username,
String password) {
this(method, username, password, null);
}
/*
* This method creates valid base64 blobs with optional trailing data
*/
private BasicAuthHeader(String method, String username,
String password, String extraBlob) {
prefix(method);
String userCredentials =
((password == null) || (password.length() < 1))
? username
: username + ":" + password;
byte[] credentialsBytes =
userCredentials.getBytes(StandardCharsets.ISO_8859_1);
String base64auth = Base64.encodeBase64String(credentialsBytes);
byte[] base64Bytes =
base64auth.getBytes(StandardCharsets.ISO_8859_1);
byte[] extraBytes =
((extraBlob == null) || (extraBlob.length() < 1))
? null :
extraBlob.getBytes(StandardCharsets.ISO_8859_1);
try {
authHeader.append(base64Bytes, 0, base64Bytes.length);
if (extraBytes != null) {
authHeader.append(extraBytes, 0, extraBytes.length);
}
}
catch (IOException ioe) {
throw new IllegalStateException("unable to extend ByteChunk:"
+ ioe.getMessage());
}
// emulate tomcat server - offset points to method in header
authHeader.setOffset(initialOffset);
}
/*
* This method allows injection of cribbed base64 blobs,
* without any validation of the contents
*/
private BasicAuthHeader(String method, String fakeBase64) {
prefix(method);
byte[] fakeBytes = fakeBase64.getBytes(StandardCharsets.ISO_8859_1);
try {
authHeader.append(fakeBytes, 0, fakeBytes.length);
}
catch (IOException ioe) {
throw new IllegalStateException("unable to extend ByteChunk:"
+ ioe.getMessage());
}
// emulate tomcat server - offset points to method in header
authHeader.setOffset(initialOffset);
}
/*
* construct the common authorization header
*/
private void prefix(String method) {
authHeader = new ByteChunk();
authHeader.setBytes(HEADER, 0, HEADER.length);
initialOffset = HEADER.length;
String methodX = method + " ";
byte[] methodBytes = methodX.getBytes(StandardCharsets.ISO_8859_1);
try {
authHeader.append(methodBytes, 0, methodBytes.length);
}
catch (IOException ioe) {
throw new IllegalStateException("unable to extend ByteChunk:"
+ ioe.getMessage());
}
}
private ByteChunk getHeader() {
return authHeader;
}
}
}