/* * 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; } } }