/*
 *  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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;

import org.junit.Assert;
import org.junit.Test;

import org.apache.catalina.Context;
import org.apache.catalina.Session;
import org.apache.catalina.session.ManagerBase;
import org.apache.catalina.startup.TesterServletEncodeUrl;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;

/**
 * Test BasicAuthenticator and NonLoginAuthenticator when a SingleSignOn Valve is active.
 * <p>
 * In the absence of SSO support, a webapp using NonLoginAuthenticator simply cannot access protected resources. These
 * tests exercise the the way successfully authenticating a different webapp under the BasicAuthenticator triggers the
 * additional SSO logic for both webapps.
 * <p>
 * The two Authenticators are thoroughly exercised by two other unit test classes: TestBasicAuthParser and
 * TestNonLoginAndBasicAuthenticator. This class mainly examines the way the Single SignOn Valve interacts with two
 * webapps when the second cannot be authenticated directly, but needs to inherit its authentication via the other.
 * <p>
 * When the server and client can both use cookies, the authentication is preserved through the exchange of a
 * JSSOSESSIONID cookie, which is different to the individual and unique JSESSIONID cookies assigned separately to the
 * two webapp sessions.
 * <p>
 * The other situation examined is where the server returns authentication cookies, but the client is configured to
 * ignore them. The Tomcat documentation clearly states that SSO <i>requires</i> the client to support cookies, so
 * access to resources in other webapp containers receives no SSO assistance.
 */
public class TestSSOnonLoginAndBasicAuthenticator extends TomcatBaseTest {

    protected static final boolean USE_COOKIES = true;
    protected static final boolean NO_COOKIES = !USE_COOKIES;

    private static final String USER = "user";
    private static final String PWD = "pwd";
    private static final String ROLE = "role";
    private static final String NICE_METHOD = "Basic";

    private static final String HTTP_PREFIX = "http://localhost:";
    private static final String CONTEXT_PATH_NOLOGIN = "/nologin";
    private static final String CONTEXT_PATH_LOGIN = "/login";
    private static final String URI_PROTECTED = "/protected";
    private static final String URI_PUBLIC = "/anyoneCanAccess";

    // session expiry in web.xml is defined in minutes
    private static final int SHORT_SESSION_TIMEOUT_MINS = 1;
    private static final int LONG_SESSION_TIMEOUT_MINS = 2;

    // we don't change the expiry scan interval - just the iteration count
    private static final int MANAGER_SCAN_INTERVAL_SECS = 10;
    private static final int MANAGER_EXPIRE_SESSIONS_FAST = 1;

    // now compute some delays - beware of the units!
    private static final int EXTRA_DELAY_SECS = 5;
    private static final int TIMEOUT_WAIT_SECS =
            EXTRA_DELAY_SECS + (MANAGER_SCAN_INTERVAL_SECS * MANAGER_EXPIRE_SESSIONS_FAST) * 5;

    private static final String CLIENT_AUTH_HEADER = "authorization";
    private static final String SERVER_AUTH_HEADER = "WWW-Authenticate";
    private static final String SERVER_COOKIE_HEADER = "Set-Cookie";
    private static final String CLIENT_COOKIE_HEADER = "Cookie";
    private static final String ENCODE_SESSION_PARAM = "jsessionid";
    private static final String ENCODE_SSOSESSION_PARAM = "jssosessionid";

    private static final TestSSOnonLoginAndBasicAuthenticator.BasicCredentials NO_CREDENTIALS = null;
    private static final TestSSOnonLoginAndBasicAuthenticator.BasicCredentials GOOD_CREDENTIALS =
            new TestSSOnonLoginAndBasicAuthenticator.BasicCredentials(NICE_METHOD, USER, PWD);

    private Tomcat tomcat;
    private Context basicContext;
    private Context nonloginContext;
    private List<String> cookies;
    private String encodedURL;

    /*
     * Run some checks without an established SSO session to make sure the test environment is correct.
     */
    @Test
    public void testEssentialEnvironment() throws Exception {

        // should be permitted to access an unprotected resource.
        doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC, USE_COOKIES, HttpServletResponse.SC_OK);

        // should not be permitted to access a protected resource
        // with the two Authenticators used in the remaining tests.
        doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, USE_COOKIES, HttpServletResponse.SC_FORBIDDEN);
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, USE_COOKIES,
                HttpServletResponse.SC_UNAUTHORIZED);
    }

    @Test
    public void testEssentialEnvironmentWithoutCookies() throws Exception {

        // should be permitted to access an unprotected resource.
        doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC, NO_COOKIES, HttpServletResponse.SC_OK);

        // should not be permitted to access a protected resource
        // with the two Authenticators used in the remaining tests.
        doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, NO_COOKIES, HttpServletResponse.SC_FORBIDDEN);
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, NO_COOKIES,
                HttpServletResponse.SC_UNAUTHORIZED);
    }

    /*
     * Logon to access a protected resource using BASIC authentication, which will establish an SSO session. Wait until
     * the SSO session times-out, then try to re-access the resource. This should be rejected with SC_FORBIDDEN 401
     * status.
     *
     * Note: this test should run for ~10 seconds.
     */
    @Test
    public void testBasicAccessAndSessionTimeout() throws Exception {

        setRapidSessionTimeoutDetection();

        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, USE_COOKIES,
                HttpServletResponse.SC_UNAUTHORIZED);
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, USE_COOKIES, HttpServletResponse.SC_OK);

        // verify the SSOID exists as a cookie
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, USE_COOKIES, HttpServletResponse.SC_OK);

        // make the session time out and lose authentication
        doImminentSessionTimeout(basicContext);

        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, USE_COOKIES,
                HttpServletResponse.SC_UNAUTHORIZED);
    }


    /*
     * Logon to access a protected resource using BASIC authentication, which will establish an SSO session. Immediately
     * try to access a protected resource in the NonLogin webapp while providing the SSO session cookie received from
     * the first webapp. This should be successful with SC_OK 200 status.
     */
    @Test
    public void testBasicLoginThenAcceptWithCookies() throws Exception {

        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, NO_COOKIES,
                HttpServletResponse.SC_UNAUTHORIZED);
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, USE_COOKIES, HttpServletResponse.SC_OK);

        // send the cookie which proves we have an authenticated SSO session
        doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, USE_COOKIES, HttpServletResponse.SC_OK);
    }

    /*
     * Logon to access a protected resource using BASIC authentication, which will establish an SSO session. Immediately
     * try to access a protected resource in the NonLogin webapp, but without sending the SSO session cookie. This
     * should be rejected with SC_FORBIDDEN 403 status.
     */
    @Test
    public void testBasicLoginThenRejectWithoutCookie() throws Exception {

        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, USE_COOKIES,
                HttpServletResponse.SC_UNAUTHORIZED);
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, USE_COOKIES, HttpServletResponse.SC_OK);

        // fail to send the authentication cookie to the other webapp.
        doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, NO_COOKIES, HttpServletResponse.SC_FORBIDDEN);
    }

    /*
     * Logon to access a protected resource using BASIC authentication, which will establish an SSO session. Then try to
     * access a protected resource in the NonLogin webapp by sending the JSESSIONID from the redirect header. The access
     * request should be rejected because the Basic webapp's sessionID is not valid for any other container.
     */
    @Test
    public void testBasicAccessThenAcceptAuthWithUri() throws Exception {

        setAlwaysUseSession();

        // first, fail to access the protected resource without credentials
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, NO_COOKIES,
                HttpServletResponse.SC_UNAUTHORIZED);

        // now, access the protected resource with good credentials
        // to establish the session
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, NO_COOKIES, HttpServletResponse.SC_OK);

        // next, access it again to harvest the session id url parameter
        String forwardParam = "?nextUrl=" + CONTEXT_PATH_LOGIN + URI_PROTECTED;
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED + forwardParam, GOOD_CREDENTIALS, NO_COOKIES,
                HttpServletResponse.SC_OK);

        // verify the sessionID was encoded in the absolute URL
        String firstEncodedURL = encodedURL;
        Assert.assertTrue(firstEncodedURL.contains(ENCODE_SESSION_PARAM));

        // access the protected resource with the encoded url (with session id)
        doTestBasic(firstEncodedURL + forwardParam, NO_CREDENTIALS, NO_COOKIES, HttpServletResponse.SC_OK);

        // verify the sessionID has not changed
        // verify the SSO sessionID was not encoded
        String secondEncodedURL = encodedURL;
        Assert.assertEquals(firstEncodedURL, secondEncodedURL);
        Assert.assertFalse(firstEncodedURL.contains(ENCODE_SSOSESSION_PARAM));

        // extract the first container's session ID
        int ix = secondEncodedURL.indexOf(ENCODE_SESSION_PARAM);
        String sessionId = secondEncodedURL.substring(ix);

        // expect to fail using that sessionID in a different container
        doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED + ";" + sessionId, NO_COOKIES,
                HttpServletResponse.SC_FORBIDDEN);
    }

    /*
     * Logon to access a protected resource using BASIC authentication, which will establish an SSO session. Immediately
     * try to access a protected resource in the NonLogin webapp while providing the SSO session cookie received from
     * the first webapp. This should be successful with SC_OK 200 status.
     *
     * Then, wait long enough for the BASIC session to expire. (The SSO session should remain active because the
     * NonLogin session has not yet expired). Try to access the protected resource again, before the SSO session has
     * expired. This should be successful with SC_OK 200 status.
     *
     * Finally, wait for the non-login session to expire and try again.. This should be rejected with SC_FORBIDDEN 403
     * status.
     *
     * (see bugfix https://bz.apache.org/bugzilla/show_bug.cgi?id=52303)
     *
     * Note: this test should run for ~20 seconds.
     */
    @Test
    public void testBasicExpiredAcceptProtectedWithCookies() throws Exception {

        setRapidSessionTimeoutDetection();

        // begin with a repeat of testBasicLoginAcceptProtectedWithCookies
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, USE_COOKIES,
                HttpServletResponse.SC_UNAUTHORIZED);
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, USE_COOKIES, HttpServletResponse.SC_OK);
        doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, USE_COOKIES, HttpServletResponse.SC_OK);

        // wait long enough for the BASIC session to expire,
        // but not long enough for the NonLogin session expiry.
        doImminentSessionTimeout(basicContext);

        // this successful NonLogin access should replenish the
        // the individual session expiry time and keep the SSO session alive
        doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, USE_COOKIES, HttpServletResponse.SC_OK);

        // wait long enough for the NonLogin session to expire,
        // which will also tear down the SSO session at the same time.
        doImminentSessionTimeout(nonloginContext);

        doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, USE_COOKIES, HttpServletResponse.SC_FORBIDDEN);
        doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, USE_COOKIES,
                HttpServletResponse.SC_UNAUTHORIZED);

    }


    public void doTestNonLogin(String uri, boolean useCookie, int expectedRC) throws Exception {

        Map<String,List<String>> reqHeaders = new HashMap<>();
        Map<String,List<String>> respHeaders = new HashMap<>();

        if (useCookie && (cookies != null)) {
            addCookies(reqHeaders);
        }

        ByteChunk bc = new ByteChunk();
        int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, respHeaders);

        if (expectedRC != HttpServletResponse.SC_OK) {
            Assert.assertEquals(expectedRC, rc);
            Assert.assertTrue(bc.getLength() > 0);
        } else {
            Assert.assertEquals("OK", bc.toString());
        }
    }

    private void doTestBasic(String uri, TestSSOnonLoginAndBasicAuthenticator.BasicCredentials credentials,
            boolean useCookie, int expectedRC) throws Exception {

        Map<String,List<String>> reqHeaders = new HashMap<>();
        Map<String,List<String>> respHeaders = new HashMap<>();

        if (useCookie && (cookies != null)) {
            addCookies(reqHeaders);
        } else {
            if (credentials != null) {
                List<String> auth = new ArrayList<>();
                auth.add(credentials.getCredentials());
                reqHeaders.put(CLIENT_AUTH_HEADER, auth);
            }
        }

        ByteChunk bc = new ByteChunk();
        int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, respHeaders);

        Assert.assertEquals("Unexpected Return Code", expectedRC, rc);
        if (expectedRC != HttpServletResponse.SC_OK) {
            Assert.assertTrue(bc.getLength() > 0);
            if (expectedRC == HttpServletResponse.SC_UNAUTHORIZED) {
                // The server should identify the acceptable method(s)
                boolean methodFound = false;
                List<String> authHeaders = respHeaders.get(SERVER_AUTH_HEADER);
                for (String authHeader : authHeaders) {
                    if (authHeader.contains(NICE_METHOD)) {
                        methodFound = true;
                        break;
                    }
                }
                Assert.assertTrue(methodFound);
            }
        } else {
            String thePage = bc.toString();
            Assert.assertNotNull(thePage);
            Assert.assertTrue(thePage.startsWith("OK"));
            if (useCookie) {
                List<String> newCookies = respHeaders.get(SERVER_COOKIE_HEADER);
                if (newCookies != null) {
                    // harvest cookies whenever the server sends some new ones
                    cookies = newCookies;
                }
            } else {
                encodedURL = "";
                final String start = "<a href=\"";
                final String end = "\">";
                int iStart = thePage.indexOf(start);
                int iEnd = 0;
                if (iStart > -1) {
                    iStart += start.length();
                    iEnd = thePage.indexOf(end, iStart);
                    if (iEnd > -1) {
                        encodedURL = thePage.substring(iStart, iEnd);
                    }
                }
            }
        }
    }


    /*
     * setup two webapps for every test
     *
     * note: the super class tearDown method will stop tomcat
     */
    @Override
    public void setUp() throws Exception {

        super.setUp();

        // create a tomcat server using the default in-memory Realm
        tomcat = getTomcatInstance();

        // associate the SingeSignOn Valve before the Contexts
        SingleSignOn sso = new SingleSignOn();
        tomcat.getHost().getPipeline().addValve(sso);

        // add the test user and role to the Realm
        tomcat.addUser(USER, PWD);
        tomcat.addRole(USER, ROLE);

        // setup both NonLogin and Login webapps
        setUpNonLogin();
        setUpLogin();

        tomcat.start();
    }

    @Override
    public void tearDown() throws Exception {

        tomcat.stop();
    }

    private void setUpNonLogin() throws Exception {

        // Must have a real docBase for webapps - just use temp
        nonloginContext = tomcat.addContext(CONTEXT_PATH_NOLOGIN, System.getProperty("java.io.tmpdir"));
        nonloginContext.setSessionTimeout(LONG_SESSION_TIMEOUT_MINS);

        // Add protected servlet to the context
        Tomcat.addServlet(nonloginContext, "TesterServlet1", new TesterServletEncodeUrl());
        nonloginContext.addServletMappingDecoded(URI_PROTECTED, "TesterServlet1");

        SecurityCollection collection1 = new SecurityCollection();
        collection1.addPatternDecoded(URI_PROTECTED);
        SecurityConstraint sc1 = new SecurityConstraint();
        sc1.addAuthRole(ROLE);
        sc1.addCollection(collection1);
        nonloginContext.addConstraint(sc1);

        // Add unprotected servlet to the context
        Tomcat.addServlet(nonloginContext, "TesterServlet2", new TesterServletEncodeUrl());
        nonloginContext.addServletMappingDecoded(URI_PUBLIC, "TesterServlet2");

        SecurityCollection collection2 = new SecurityCollection();
        collection2.addPatternDecoded(URI_PUBLIC);
        SecurityConstraint sc2 = new SecurityConstraint();
        // do not add a role - which signals access permitted without one
        sc2.addCollection(collection2);
        nonloginContext.addConstraint(sc2);

        // Configure the authenticator and inherit the Realm from Engine
        LoginConfig lc = new LoginConfig();
        lc.setAuthMethod("NONE");
        nonloginContext.setLoginConfig(lc);
        AuthenticatorBase nonloginAuthenticator = new NonLoginAuthenticator();
        nonloginContext.getPipeline().addValve(nonloginAuthenticator);
    }

    private void setUpLogin() throws Exception {

        // Must have a real docBase for webapps - just use temp
        basicContext = tomcat.addContext(CONTEXT_PATH_LOGIN, System.getProperty("java.io.tmpdir"));
        basicContext.setSessionTimeout(SHORT_SESSION_TIMEOUT_MINS);

        // Add protected servlet to the context
        Tomcat.addServlet(basicContext, "TesterServlet3", new TesterServletEncodeUrl());
        basicContext.addServletMappingDecoded(URI_PROTECTED, "TesterServlet3");
        SecurityCollection collection = new SecurityCollection();
        collection.addPatternDecoded(URI_PROTECTED);
        SecurityConstraint sc = new SecurityConstraint();
        sc.addAuthRole(ROLE);
        sc.addCollection(collection);
        basicContext.addConstraint(sc);

        // Add unprotected servlet to the context
        Tomcat.addServlet(basicContext, "TesterServlet4", new TesterServletEncodeUrl());
        basicContext.addServletMappingDecoded(URI_PUBLIC, "TesterServlet4");
        SecurityCollection collection2 = new SecurityCollection();
        collection2.addPatternDecoded(URI_PUBLIC);
        SecurityConstraint sc2 = new SecurityConstraint();
        // do not add a role - which signals access permitted without one
        sc2.addCollection(collection2);
        basicContext.addConstraint(sc2);

        // Configure the authenticator and inherit the Realm from Engine
        LoginConfig lc = new LoginConfig();
        lc.setAuthMethod("BASIC");
        basicContext.setLoginConfig(lc);
        AuthenticatorBase basicAuthenticator = new BasicAuthenticator();
        basicContext.getPipeline().addValve(basicAuthenticator);
    }

    /*
     * extract and save the server cookies from the incoming response
     */
    protected void saveCookies(Map<String,List<String>> respHeaders) {
        // we only save the Cookie values, not header prefix
        List<String> cookieHeaders = respHeaders.get(SERVER_COOKIE_HEADER);
        if (cookieHeaders == null) {
            cookies = null;
        } else {
            cookies = new ArrayList<>(cookieHeaders.size());
            for (String cookieHeader : cookieHeaders) {
                cookies.add(cookieHeader.substring(0, cookieHeader.indexOf(';')));
            }
        }
    }

    /*
     * add all saved cookies to the outgoing request
     */
    protected void addCookies(Map<String,List<String>> reqHeaders) {
        if ((cookies != null) && (cookies.size() > 0)) {
            StringBuilder cookieHeader = new StringBuilder();
            boolean first = true;
            for (String cookie : cookies) {
                if (!first) {
                    cookieHeader.append(';');
                } else {
                    first = false;
                }
                cookieHeader.append(cookie);
            }
            List<String> cookieHeaderList = new ArrayList<>(1);
            cookieHeaderList.add(cookieHeader.toString());
            reqHeaders.put(CLIENT_COOKIE_HEADER, cookieHeaderList);
        }
    }

    /*
     * Force non-default behaviour for both Authenticators. The session id will not be regenerated after authentication,
     * which is less secure but needed for browsers that will not handle cookies.
     */
    private void setAlwaysUseSession() {

        ((AuthenticatorBase) basicContext.getAuthenticator()).setAlwaysUseSession(true);
        ((AuthenticatorBase) nonloginContext.getAuthenticator()).setAlwaysUseSession(true);
    }

    /*
     * Force faster timeout for an active Container than can be defined in web.xml. By getting to the active Session we
     * can choose seconds instead of minutes. Note: shamelessly cloned from ManagerBase - beware of synch issues on the
     * underlying sessions.
     */
    private void doImminentSessionTimeout(Context activeContext) {

        ManagerBase manager = (ManagerBase) activeContext.getManager();
        Session[] sessions = manager.findSessions();
        for (Session session : sessions) {
            if (session != null && session.isValid()) {
                session.setMaxInactiveInterval(EXTRA_DELAY_SECS);
                // leave it to be expired by the manager
            }
        }


        try {
            Thread.sleep(EXTRA_DELAY_SECS * 1000);
        } catch (InterruptedException ie) {
            // ignored
        }

        // Paranoid verification that active sessions have now gone
        int count = 0;
        sessions = manager.findSessions();
        while (sessions.length != 0 && count < TIMEOUT_WAIT_SECS) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // Ignore
            }
            sessions = manager.findSessions();
            count++;
        }

        sessions = manager.findSessions();
        Assert.assertTrue(sessions.length == 0);
    }

    /*
     * Force rapid timeout scanning for both webapps The StandardManager default service cycle time is 10 seconds, with
     * a session expiry scan every 6 cycles.
     */
    private void setRapidSessionTimeoutDetection() {

        ((ManagerBase) basicContext.getManager()).setProcessExpiresFrequency(MANAGER_EXPIRE_SESSIONS_FAST);
        ((ManagerBase) nonloginContext.getManager()).setProcessExpiresFrequency(MANAGER_EXPIRE_SESSIONS_FAST);
    }

    /*
     * Encapsulate the logic to generate an HTTP header for BASIC Authentication. Note: only used internally, so no need
     * to validate arguments.
     */
    private static final class BasicCredentials {

        private final String method;
        private final String username;
        private final String password;
        private final String credentials;

        private BasicCredentials(String aMethod, String aUsername, String aPassword) {
            method = aMethod;
            username = aUsername;
            password = aPassword;
            String userCredentials = username + ":" + password;
            byte[] credentialsBytes = userCredentials.getBytes(StandardCharsets.ISO_8859_1);
            String base64auth = Base64.getEncoder().encodeToString(credentialsBytes);
            credentials = method + " " + base64auth;
        }

        private String getCredentials() {
            return credentials;
        }
    }
}