HttpAuthenticationFeature.java

/*
 * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.client.authentication;

import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;

/**
 * Features that provides Http Basic and Digest client authentication (based on RFC 2617).
 * <p>
 * The feature can work in following modes:
 * <ul>
 *  <li><b>BASIC:</b> Basic preemptive authentication. In preemptive mode the authentication information
 *  is send always with each HTTP request. This mode is more usual than the following non-preemptive mode
 *  (if you require BASIC authentication you will probably use this preemptive mode). This mode must
 *  be combined with usage of SSL/TLS as the password is send only BASE64 encoded.</li>
 *  <li><i>BASIC NON-PREEMPTIVE:</i> Basic non-preemptive authentication. In non-preemptive mode the
 *  authentication information is added only when server refuses the request with {@code 401} status code and
 *  then the request is repeated with authentication information. This mode has negative impact on the performance.
 *  The advantage is that it does not send credentials when they are not needed. This mode must
 *  be combined with usage of SSL/TLS as the password is send only BASE64 encoded.
 *  <p/>
 *  Please note that when you use non-preemptive authentication, Jersey client will make 2 requests to a resource,
 *  which also means that all registered filters will be invoked twice.
 *  </li>
 *  <li><b>DIGEST:</b> Http digest authentication. Does not require usage of SSL/TLS.</li>
 *  <li><b>UNIVERSAL:</b> Combination of basic and digest authentication. The feature works in non-preemptive
 *  mode which means that it sends requests without authentication information. If {@code 401} status
 *  code is returned, the request is repeated and an appropriate authentication is used based on the
 *  authentication requested in the response (defined in {@code WWW-Authenticate} HTTP header. The feature
 *  remembers which authentication requests were successful for given URI and next time tries to preemptively
 *  authenticate against this URI with latest successful authentication method.
 *  </li>
 * </ul>
 * </p>
 * <p>
 * To initialize the feature use static method of this feature.
 * </p>
 * <p>
 * Example of building the feature in
 * Basic authentication mode:
 * <pre>
 * HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic("user", "superSecretPassword");
 * </pre>
 * </p>
 * <p>
 * Example of building the feature in basic non-preemptive mode:
 * <pre>
 * HttpAuthenticationFeature feature = HttpAuthenticationFeature.basicBuilder()
 *     .nonPreemptive().credentials("user", "superSecretPassword").build();
 * </pre>
 * </p>
 * <p>
 * Example of building the feature in universal mode:
 * <pre>
 * HttpAuthenticationFeature feature = HttpAuthenticationFeature.universal("user", "superSecretPassword");
 * </pre>
 * </p>
 * <p>
 * Example of building the feature in universal mode with different credentials for basic and digest:
 * <pre>
 * HttpAuthenticationFeature feature = HttpAuthenticationFeature.universalBuilder()
 *      .credentialsForBasic("user", "123456")
 *      .credentials("adminuser", "hello")
 *      .build();
 * </pre>
 * </p>
 * Example of building the feature in basic preemptive mode with no default credentials. Credentials will have
 * to be supplied with each request using request properties (see below):
 * <pre>
 * HttpAuthenticationFeature feature = HttpAuthenticationFeature.basicBuilder().build();
 * </pre>
 * </p>
 * <p>
 * Once the feature is built it needs to be registered into the {@link javax.ws.rs.client.Client},
 * {@link javax.ws.rs.client.WebTarget} or other client configurable object. Example:
 * <pre>
 * final Client client = ClientBuilder.newClient();
 * client.register(feature);
 * </pre>
 * </p>
 *
 * Then you invoke requests as usual and authentication will be handled by the feature.
 * You can change the credentials for each request using properties
 * {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_USERNAME} and
 * {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_PASSWORD}. Example:
 * <pre>
 * final Response response = client.target("http://localhost:8080/rest/homer/contact").request()
 *    .property(HTTP_AUTHENTICATION_BASIC_USERNAME, "homer")
 *    .property(HTTP_AUTHENTICATION_BASIC_PASSWORD, "p1swd745").get();
 * </pre>
 * <p>
 * This class also contains property key definitions for overriding only specific basic or digest credentials:
 * <ul>
 *     <li>
 *      {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_BASIC_USERNAME} and
 *      {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_BASIC_PASSWORD}
 *      </li>
 *     <li>
 *      {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_DIGEST_USERNAME} and
 *      {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_DIGEST_PASSWORD}.
 *      </li>
 * </ul>
 * </p>
 *
 * @author Miroslav Fuksa
 *
 * @since 2.5
 */
public class HttpAuthenticationFeature implements Feature {

    /**
     * Feature authentication mode.
     */
    static enum Mode {
        /**
         * Basic preemptive.
         **/
        BASIC_PREEMPTIVE,
        /**
         * Basic non preemptive
         */
        BASIC_NON_PREEMPTIVE,
        /**
         * Digest.
         */
        DIGEST,
        /**
         * Universal.
         */
        UNIVERSAL
    }

    /**
     * Builder that creates instances of {@link HttpAuthenticationFeature}.
     */
    public static interface Builder {

        /**
         * Set credentials.
         *
         * @param username Username.
         * @param password Password as byte array.
         * @return This builder.
         */
        public Builder credentials(String username, byte[] password);

        /**
         * Set credentials.
         *
         * @param username Username.
         * @param password Password as {@link String}.
         * @return This builder.
         */
        public Builder credentials(String username, String password);

        /**
         * Build the feature.
         *
         * @return Http authentication feature configured from this builder.
         */
        public HttpAuthenticationFeature build();
    }

    /**
     * Extension of {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature.Builder}
     * that builds the http authentication feature configured for basic authentication.
     */
    public static interface BasicBuilder extends Builder {

        /**
         * Configure the builder to create features in non-preemptive basic authentication mode.
         *
         * @return This builder.
         */
        public BasicBuilder nonPreemptive();
    }

    /**
     * Extension of {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature.Builder}
     * that builds the http authentication feature configured in universal mode that supports
     * basic and digest authentication.
     */
    public static interface UniversalBuilder extends Builder {

        /**
         * Set credentials that will be used for basic authentication only.
         *
         * @param username Username.
         * @param password Password as {@link String}.
         * @return This builder.
         */
        public UniversalBuilder credentialsForBasic(String username, String password);

        /**
         * Set credentials that will be used for basic authentication only.
         *
         * @param username Username.
         * @param password Password as {@code byte array}.
         * @return This builder.
         */
        public UniversalBuilder credentialsForBasic(String username, byte[] password);

        /**
         * Set credentials that will be used for digest authentication only.
         *
         * @param username Username.
         * @param password Password as {@link String}.
         * @return This builder.
         */
        public UniversalBuilder credentialsForDigest(String username, String password);

        /**
         * Set credentials that will be used for digest authentication only.
         *
         * @param username Username.
         * @param password Password as {@code byte array}.
         * @return This builder.
         */
        public UniversalBuilder credentialsForDigest(String username, byte[] password);
    }

    /**
     * Implementation of all authentication builders.
     */
    static class BuilderImpl implements UniversalBuilder, BasicBuilder {

        private String usernameBasic;
        private byte[] passwordBasic;
        private String usernameDigest;
        private byte[] passwordDigest;
        private Mode mode;

        /**
         * Create a new builder.
         *
         * @param mode Mode in which the final authentication feature should work.
         */
        public BuilderImpl(Mode mode) {
            this.mode = mode;
        }

        @Override
        public Builder credentials(String username, String password) {
            return credentials(username, password == null ? null : password.getBytes(HttpAuthenticationFilter.CHARACTER_SET));
        }

        @Override
        public Builder credentials(String username, byte[] password) {
            credentialsForBasic(username, password);
            credentialsForDigest(username, password);
            return this;
        }

        @Override
        public UniversalBuilder credentialsForBasic(String username, String password) {
            return credentialsForBasic(username,
                    password == null ? null : password.getBytes(HttpAuthenticationFilter.CHARACTER_SET));
        }

        @Override
        public UniversalBuilder credentialsForBasic(String username, byte[] password) {
            this.usernameBasic = username;
            this.passwordBasic = password;
            return this;
        }

        @Override
        public UniversalBuilder credentialsForDigest(String username, String password) {
            return credentialsForDigest(username,
                    password == null ? null : password.getBytes(HttpAuthenticationFilter.CHARACTER_SET));
        }

        @Override
        public UniversalBuilder credentialsForDigest(String username, byte[] password) {
            this.usernameDigest = username;
            this.passwordDigest = password;
            return this;
        }

        @Override
        public HttpAuthenticationFeature build() {
            return new HttpAuthenticationFeature(mode,
                    usernameBasic == null ? null
                            : new HttpAuthenticationFilter.Credentials(usernameBasic, passwordBasic),
                    usernameDigest == null ? null
                            : new HttpAuthenticationFilter.Credentials(usernameDigest, passwordDigest));
        }

        @Override
        public BasicBuilder nonPreemptive() {
            if (mode == Mode.BASIC_PREEMPTIVE) {
                this.mode = Mode.BASIC_NON_PREEMPTIVE;
            }
            return this;
        }
    }

    /**
     * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request}
     * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override
     * the username for http authentication feature for the request.
     * <p>
     * Example:
     * <pre>
     * Response response = client.target("http://localhost:8080/rest/joe/orders").request()
     *      .property(HTTP_AUTHENTICATION_USERNAME, "joe")
     *      .property(HTTP_AUTHENTICATION_PASSWORD, "p1swd745").get();
     * </pre>
     * </p>
     * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_PASSWORD} property
     * (as shown in the example). This property pair overrides all password settings of the authentication
     * feature for the current request.
     * <p>
     * The default value must be instance of {@link String}.
     * </p>
     * <p>
     * The name of the configuration property is <tt>{@value}</tt>.
     * </p>
     */
    public static final String HTTP_AUTHENTICATION_USERNAME = "jersey.config.client.http.auth.username";
    /**
     * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request}
     * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override
     * the password for http authentication feature for the request.
     * <p>
     * Example:
     * <pre>
     * Response response = client.target("http://localhost:8080/rest/joe/orders").request()
     *      .property(HTTP_AUTHENTICATION_USERNAME, "joe")
     *      .property(HTTP_AUTHENTICATION_PASSWORD, "p1swd745").get();
     * </pre>
     * </p>
     * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_USERNAME} property
     * (as shown in the example). This property pair overrides all password settings of the authentication
     * feature for the current request.
     * <p>
     * The value must be instance of {@link String} or {@code byte} array ({@code byte[]}).
     * </p>
     * <p>
     * The name of the configuration property is <tt>{@value}</tt>.
     * </p>
     */
    public static final String HTTP_AUTHENTICATION_PASSWORD = "jersey.config.client.http.auth.password";

    /**
     * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request}
     * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override
     * the username for http basic authentication feature for the request.
     * <p>
     * Example:
     * <pre>
     * Response response = client.target("http://localhost:8080/rest/joe/orders").request()
     *      .property(HTTP_AUTHENTICATION_BASIC_USERNAME, "joe")
     *      .property(HTTP_AUTHENTICATION_BASIC_PASSWORD, "p1swd745").get();
     * </pre>
     * </p>
     * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_PASSWORD} property
     * (as shown in the example). The property pair influence only credentials used during basic authentication.
     *
     * <p>
     * The value must be instance of {@link String}.
     * </p>
     * <p>
     * The name of the configuration property is <tt>{@value}</tt>.
     * </p>

     */
    public static final String HTTP_AUTHENTICATION_BASIC_USERNAME = "jersey.config.client.http.auth.basic.username";

    /**
     * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request}
     * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override
     * the password for http basic authentication feature for the request.
     * <p>
     * Example:
     * <pre>
     * Response response = client.target("http://localhost:8080/rest/joe/orders").request()
     *      .property(HTTP_AUTHENTICATION_BASIC_USERNAME, "joe")
     *      .property(HTTP_AUTHENTICATION_BASIC_PASSWORD, "p1swd745").get();
     * </pre>
     * </p>
     * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_USERNAME} property
     * (as shown in the example). The property pair influence only credentials used during basic authentication.
     * <p>
     * The value must be instance of {@link String} or {@code byte} array ({@code byte[]}).
     * </p>
     * <p>
     * The name of the configuration property is <tt>{@value}</tt>.
     * </p>
     */
    public static final String HTTP_AUTHENTICATION_BASIC_PASSWORD = "jersey.config.client.http.auth.basic.password";

    /**
     * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request}
     * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override
     * the username for http digest authentication feature for the request.
     * <p>
     * Example:
     * <pre>
     * Response response = client.target("http://localhost:8080/rest/joe/orders").request()
     *      .property(HTTP_AUTHENTICATION_DIGEST_USERNAME, "joe")
     *      .property(HTTP_AUTHENTICATION_DIGEST_PASSWORD, "p1swd745").get();
     * </pre>
     * </p>
     * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_PASSWORD} property
     * (as shown in the example). The property pair influence only credentials used during digest authentication.
     * <p>
     * The value must be instance of {@link String}.
     * </p>
     * <p>
     * The name of the configuration property is <tt>{@value}</tt>.
     * </p>
     */
    public static final String HTTP_AUTHENTICATION_DIGEST_USERNAME = "jersey.config.client.http.auth.digest.username";

    /**
     * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request}
     * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override
     * the password for http digest authentication feature for the request.
     * <p>
     * Example:
     * <pre>
     * Response response = client.target("http://localhost:8080/rest/joe/orders").request()
     *      .property(HTTP_AUTHENTICATION_DIGEST_USERNAME, "joe")
     *      .property(HTTP_AUTHENTICATION_DIGEST_PASSWORD, "p1swd745").get();
     * </pre>
     * </p>
     * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_PASSWORD} property
     * (as shown in the example). The property pair influence only credentials used during digest authentication.
     * <p>
     * The value must be instance of {@link String} or {@code byte} array ({@code byte[]}).
     * </p>
     * <p>
     * The name of the configuration property is <tt>{@value}</tt>.
     * </p>
     */
    public static final String HTTP_AUTHENTICATION_DIGEST_PASSWORD = "jersey.config.client.http.auth.digest.password";

    /**
     * Create the builder of the http authentication feature working in basic authentication mode. The builder
     * can build preemptive and non-preemptive basic authentication features.
     *
     * @return Basic http authentication builder.
     */
    public static BasicBuilder basicBuilder() {
        return new BuilderImpl(Mode.BASIC_PREEMPTIVE);
    }

    /**
     * Create the http authentication feature in basic preemptive authentication mode initialized with credentials.
     *
     * @param username Username.
     * @param password Password as {@code byte array}.
     * @return Http authentication feature configured in basic mode.
     */
    public static HttpAuthenticationFeature basic(String username, byte[] password) {
        return build(Mode.BASIC_PREEMPTIVE, username, password);
    }

    /**
     * Create the http authentication feature in basic preemptive authentication mode initialized with credentials.
     *
     * @param username Username.
     * @param password Password as {@link String}.
     * @return Http authentication feature configured in basic mode.
     */
    public static HttpAuthenticationFeature basic(String username, String password) {
        return build(Mode.BASIC_PREEMPTIVE, username, password);
    }

    /**
     * Create the http authentication feature in digest authentication mode initialized without default
     * credentials. Credentials will have to be supplied using request properties for each request.
     *
     * @return Http authentication feature configured in digest mode.
     */
    public static HttpAuthenticationFeature digest() {
        return build(Mode.DIGEST);
    }

    /**
     * Create the http authentication feature in digest authentication mode initialized with credentials.
     *
     * @param username Username.
     * @param password Password as {@code byte array}.
     * @return Http authentication feature configured in digest mode.
     */
    public static HttpAuthenticationFeature digest(String username, byte[] password) {
        return build(Mode.DIGEST, username, password);
    }

    /**
     * Create the http authentication feature in digest authentication mode initialized with credentials.
     *
     * @param username Username.
     * @param password Password as {@link String}.
     * @return Http authentication feature configured in digest mode.
     */
    public static HttpAuthenticationFeature digest(String username, String password) {
        return build(Mode.DIGEST, username, password);
    }

    /**
     * Create the builder that builds http authentication feature in combined mode supporting both,
     * basic and digest authentication.
     *
     * @return Universal builder.
     */
    public static UniversalBuilder universalBuilder() {
        return new BuilderImpl(Mode.UNIVERSAL);
    }

    /**
     * Create the http authentication feature in combined mode supporting both,
     * basic and digest authentication.
     *
     * @param username Username.
     * @param password Password as {@code byte array}.
     * @return Http authentication feature configured in digest mode.
     */
    public static HttpAuthenticationFeature universal(String username, byte[] password) {
        return build(Mode.UNIVERSAL, username, password);
    }

    /**
     * Create the http authentication feature in combined mode supporting both,
     * basic and digest authentication.
     *
     * @param username Username.
     * @param password Password as {@link String}.
     * @return Http authentication feature configured in digest mode.
     */
    public static HttpAuthenticationFeature universal(String username, String password) {
        return build(Mode.UNIVERSAL, username, password);
    }

    private static HttpAuthenticationFeature build(Mode mode) {
        return new BuilderImpl(mode).build();
    }

    private static HttpAuthenticationFeature build(Mode mode, String username, byte[] password) {
        return new BuilderImpl(mode).credentials(username, password).build();
    }

    private static HttpAuthenticationFeature build(Mode mode, String username, String password) {
        return new BuilderImpl(mode).credentials(username, password).build();
    }

    private final Mode mode;
    private final HttpAuthenticationFilter.Credentials basicCredentials;
    private final HttpAuthenticationFilter.Credentials digestCredentials;

    private HttpAuthenticationFeature(Mode mode, HttpAuthenticationFilter.Credentials basicCredentials,
                                      HttpAuthenticationFilter.Credentials digestCredentials) {
        this.mode = mode;
        this.basicCredentials = basicCredentials;

        this.digestCredentials = digestCredentials;
    }

    @Override
    public boolean configure(FeatureContext context) {
        context.register(new HttpAuthenticationFilter(mode, basicCredentials, digestCredentials, context.getConfiguration()));
        return true;
    }
}