OAuth2Error.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2014-2016 ForgeRock AS.
 */

package org.forgerock.http.oauth2;

import static org.forgerock.http.header.HeaderUtil.parseParameters;
import static org.forgerock.http.header.HeaderUtil.quote;
import static org.forgerock.http.header.HeaderUtil.split;
import static org.forgerock.util.Utils.joinAsString;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.forgerock.http.protocol.Form;
import org.forgerock.http.protocol.Status;
import org.forgerock.json.JsonValue;
import org.forgerock.json.JsonValueException;
import org.forgerock.util.Reject;

/**
 * Describes an error which occurred during an OAuth 2.0 authorization request
 * or when performing an authorized request. More specifically, errors are
 * communicated:
 * <ul>
 * <li>as query parameters in a failed authorization call-back. These errors are
 * defined in RFC 6749 # 4.1.2 and comprise of an error code, optional error
 * description, and optional error URI
 * <li>as JSON encoded content in a failed access token request or failed
 * refresh token request. These errors are defined in RFC 6749 # 5.2 and
 * comprise of an error code, optional error description, and optional error URI
 * <li>using the {@code WWW-Authenticate} response header in response to a
 * failed attempt to access an OAuth 2.0 protected resource on a resource
 * server. These errors are defined in RFC 6750 # 3.1 and comprise of an
 * optional error code, optional error description, optional error URI, optional
 * list of required scopes, and optional realm.
 * </ul>
 *
 * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749 #
 *      4.1.2 - The OAuth 2.0 Authorization Framework</a>
 * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 # 5.2
 *      - The OAuth 2.0 Authorization Framework</a>
 * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 - The
 *      OAuth 2.0 Authorization Framework: Bearer Token Usage</a>
 */
public final class OAuth2Error {
    /**
     * The resource owner or authorization server denied the request.
     *
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
     */
    public static final String E_ACCESS_DENIED = "access_denied";
    /**
     * The request requires higher privileges than provided by the access token.
     * The resource server SHOULD respond with the HTTP 403 (Forbidden) status
     * code and MAY include the "scope" attribute with the scope necessary to
     * access the protected resource.
     *
     * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 -
     *      The OAuth 2.0 Authorization Framework: Bearer Token Usage</a>
     */
    public static final String E_INSUFFICIENT_SCOPE = "insufficient_scope";
    /**
     * Client authentication failed (e.g., unknown client, no client
     * authentication included, or unsupported authentication method). The
     * authorization server MAY return an HTTP 401 (Unauthorized) status code to
     * indicate which HTTP authentication schemes are supported. If the client
     * attempted to authenticate via the "Authorization" request header field,
     * the authorization server MUST respond with an HTTP 401 (Unauthorized)
     * status code and include the "WWW-Authenticate" response header field
     * matching the authentication scheme used by the client.
     *
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
     *      5.2 - The OAuth 2.0 Authorization Framework</a>
     */
    public static final String E_INVALID_CLIENT = "invalid_client";
    /**
     * The provided authorization grant (e.g., authorization code, resource
     * owner credentials) or refresh token is invalid, expired, revoked, does
     * not match the redirection URI used in the authorization request, or was
     * issued to another client.
     *
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
     *      5.2 - The OAuth 2.0 Authorization Framework</a>
     */
    public static final String E_INVALID_GRANT = "invalid_grant";
    /**
     * The request is missing a required parameter, includes an unsupported
     * parameter value (other than grant type), repeats a parameter, includes
     * multiple credentials, utilizes more than one mechanism for authenticating
     * the client, or is otherwise malformed. The resource server SHOULD respond
     * with the HTTP 400 (Bad Request) status code.
     *
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
     *      5.2 - The OAuth 2.0 Authorization Framework</a>
     * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 -
     *      The OAuth 2.0 Authorization Framework: Bearer Token Usage</a>
     */
    public static final String E_INVALID_REQUEST = "invalid_request";
    /**
     * The requested scope is invalid, unknown, malformed, or exceeds the scope
     * granted by the resource owner.
     *
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
     *      5.2 - The OAuth 2.0 Authorization Framework</a>
     */
    public static final String E_INVALID_SCOPE = "invalid_scope";
    /**
     * The access token provided is expired, revoked, malformed, or invalid for
     * other reasons. The resource SHOULD respond with the HTTP 401
     * (Unauthorized) status code. The client MAY request a new access token and
     * retry the protected resource request.
     *
     * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 -
     *      The OAuth 2.0 Authorization Framework: Bearer Token Usage</a>
     */
    public static final String E_INVALID_TOKEN = "invalid_token";
    /**
     * The authorization server encountered an unexpected condition that
     * prevented it from fulfilling the request. (This error code is needed
     * because a 500 Internal Server Error HTTP status code cannot be returned
     * to the client via an HTTP redirect.)
     *
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
     */
    public static final String E_SERVER_ERROR = "server_error";
    /**
     * The authorization server is currently unable to handle the request due to
     * a temporary overloading or maintenance of the server. (This error code is
     * needed because a 503 Service Unavailable HTTP status code cannot be
     * returned to the client via an HTTP redirect.)
     *
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
     */
    public static final String E_TEMPORARILY_UNAVAILABLE = "temporarily_unavailable";
    /**
     * The authenticated client is not authorized to use this authorization
     * grant type.
     *
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
     *      5.2 - The OAuth 2.0 Authorization Framework</a>
     */
    public static final String E_UNAUTHORIZED_CLIENT = "unauthorized_client";
    /**
     * The authorization grant type is not supported by the authorization
     * server.
     *
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
     *      5.2 - The OAuth 2.0 Authorization Framework</a>
     */
    public static final String E_UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type";
    /**
     * The authorization server does not support obtaining an authorization code
     * using this method.
     *
     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
     */
    public static final String E_UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type";

    /** The name of the field which communicates the error code. */
    public static final String F_ERROR = "error";
    /** The name of the field which communicates the error description. */
    public static final String F_ERROR_DESCRIPTION = "error_description";
    /** The name of the field which communicates the error uri. */
    public static final String F_ERROR_URI = "error_uri";
    /** The name of the field which communicates the realm. */
    public static final String F_REALM = "realm";
    /** The name of the field which communicates the scope. */
    public static final String F_SCOPE = "scope";

    /** The WWW-Authenticate header prefix, 'Bearer'. */
    public static final String H_BEARER = "Bearer";

    /** Singleton instance used for empty WWW-Authenticate headers. */
    private static final OAuth2Error EMPTY = new OAuth2Error(null, null, null, null, null);

    /** The WWW-Authenticate header prefix including trailing space separator. */
    private static final String H_BEARER_WITH_SPACE = H_BEARER + " ";

    /**
     * Returns an OAuth 2.0 resource server error whose values are determined on
     * a best-effort basis from the provided incomplete error and HTTP status
     * code.
     *
     * @param status
     *            The HTTP status code.
     * @param incomplete
     *            The incomplete and possibly {@code null} error.
     * @return A non-{@code null} error whose error code has been determined
     *         from the HTTP status code.
     */
    public static OAuth2Error bestEffortResourceServerError(final Status status,
                                                            final OAuth2Error incomplete) {
        if (incomplete != null && incomplete.error != null) {
            // Seems ok.
            return incomplete;
        }
        final String error = mapStatusToError(status);
        if (incomplete == null) {
            return new OAuth2Error(null, null, error, null, null);
        } else {
            return new OAuth2Error(incomplete.getRealm(),
                                   incomplete.getScope(),
                                   error,
                                   incomplete.getErrorDescription(),
                                   incomplete.getErrorUri());
        }
    }

    private static String mapStatusToError(final Status status) {
        if (Status.BAD_REQUEST.equals(status)) {
            return E_INVALID_REQUEST;
        } else if (Status.UNAUTHORIZED.equals(status)) {
            return E_INVALID_TOKEN;
        } else if (Status.FORBIDDEN.equals(status)) {
            return E_INVALID_SCOPE;
        } else if (Status.METHOD_NOT_ALLOWED.equals(status)) {
            return E_INVALID_REQUEST;
        } else if (Status.INTERNAL_SERVER_ERROR.equals(status)) {
            return E_SERVER_ERROR;
        } else if (Status.SERVICE_UNAVAILABLE.equals(status)) {
            return E_TEMPORARILY_UNAVAILABLE;
        } else {
            return E_SERVER_ERROR; // No idea
        }
    }

    /**
     * Returns an OAuth 2.0 error suitable for inclusion in authorization
     * call-back responses and access token and refresh token responses.
     *
     * @param error
     *            The error code specifying the cause of the failure.
     * @param errorDescription
     *            The human-readable ASCII text providing additional
     *            information, or {@code null}.
     * @return The OAuth 2.0 error.
     * @throws NullPointerException
     *             If {@code error} was {@code null}.
     */
    public static OAuth2Error newAuthorizationServerError(final String error,
            final String errorDescription) {
        Reject.ifNull(error);
        return new OAuth2Error(null, null, error, errorDescription, null);
    }

    /**
     * Returns an OAuth 2.0 error suitable for inclusion in authorization
     * call-back responses and access token and refresh token responses.
     *
     * @param error
     *            The error code specifying the cause of the failure.
     * @param errorDescription
     *            The human-readable ASCII text providing additional
     *            information, or {@code null}.
     * @param errorUri
     *            A URI identifying a human-readable web page with information
     *            about the error, or {@code null}.
     * @return The OAuth 2.0 error.
     * @throws NullPointerException
     *             If {@code error} was {@code null}.
     */
    public static OAuth2Error newAuthorizationServerError(final String error,
            final String errorDescription, final String errorUri) {
        Reject.ifNull(error);
        return new OAuth2Error(null, null, error, errorDescription, errorUri);
    }

    /**
     * Returns an OAuth 2.0 error suitable for inclusion in resource server
     * WWW-Authenticate response headers.
     *
     * @param realm
     *            The scope of protection required to access the protected
     *            resource, or {@code null}.
     * @param scope
     *            The required scope(s) of the access token for accessing the
     *            requested resource, or {@code null}.
     * @param error
     *            The error code specifying the cause of the failure, or
     *            {@code null}.
     * @param errorDescription
     *            The human-readable ASCII text providing additional
     *            information, or {@code null}.
     * @param errorUri
     *            A URI identifying a human-readable web page with information
     *            about the error, or {@code null}.
     * @return The OAuth 2.0 error.
     */
    public static OAuth2Error newResourceServerError(final String realm, final List<String> scope,
            final String error, final String errorDescription, final String errorUri) {
        return new OAuth2Error(realm, scope, error, errorDescription, errorUri);
    }

    /**
     * Parses the provided {@link #toString()} representation as an OAuth 2.0
     * error.
     *
     * @param s
     *            The string to parse.
     * @return The parsed OAuth 2.0 error.
     */
    public static OAuth2Error valueOf(final String s) {
        final List<String> attributes = split(s, ',');
        final Map<String, String> map = parseParameters(attributes);
        final String realm = map.get("realm");
        final String scopeString = map.get("scope");
        final List<String> scopes = scopeString != null
                ? Arrays.asList(scopeString.trim().split("\\s+"))
                : null;
        final String error = map.get("error");
        final String errorDescription = map.get("error_description");
        final String errorUri = map.get("error_uri");
        return new OAuth2Error(realm, scopes, error, errorDescription, errorUri);
    }

    /**
     * Parses the Form representation of an authorization call-back error as an
     * OAuth 2.0 error. Only the error, error description, and error URI fields
     * will be included.
     *
     * @param form
     *            The Form representation of an authorization call-back error.
     * @return The parsed OAuth 2.0 error.
     */
    public static OAuth2Error valueOfForm(final Form form) {
        return new OAuth2Error(null, null, form.getFirst(F_ERROR),
                form.getFirst(F_ERROR_DESCRIPTION), form.getFirst(F_ERROR_URI));
    }

    /**
     * Parses the JSON representation of an access token error response as an
     * OAuth 2.0 error. Only the error, error description, and error URI fields
     * will be included.
     *
     * @param json
     *            The JSON representation of an access token error response.
     * @return The parsed OAuth 2.0 error.
     * @throws IllegalArgumentException
     *             If the JSON content was malformed.
     */
    public static OAuth2Error valueOfJsonContent(final Map<String, Object> json) {
        final JsonValue jv = new JsonValue(json);
        try {
            return new OAuth2Error(null, null, jv.get(F_ERROR).asString(),
                    jv.get(F_ERROR_DESCRIPTION).asString(), jv.get(F_ERROR_URI).asString());
        } catch (final JsonValueException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Parses the provided WWW-Authenticate header content as an OAuth 2.0
     * error.
     *
     * @param s
     *            The string containing the WWW-Authenticate header content.
     * @return The parsed OAuth 2.0 error.
     * @throws IllegalArgumentException
     *             If the header value was malformed.
     */
    public static OAuth2Error valueOfWWWAuthenticateHeader(final String s) {
        if (H_BEARER.equals(s)) {
            return EMPTY;
        } else if (s.startsWith(H_BEARER_WITH_SPACE)) {
            return valueOf(s.substring(H_BEARER_WITH_SPACE.length()));
        } else {
            throw new IllegalArgumentException("Malformed WWW-Authenticate header '" + s + "'");
        }
    }

    private final String error;
    private final String errorDescription;
    private final String errorUri;
    private final String realm;
    private final List<String> scope;
    private transient String stringValue;

    private OAuth2Error(final String realm, final List<String> scope, final String error,
            final String errorDescription, final String errorUri) {
        this.realm = realm;
        this.scope = scope != null
                ? Collections.unmodifiableList(scope)
                : Collections.<String> emptyList();
        this.error = error;
        this.errorDescription = errorDescription;
        this.errorUri = errorUri;
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        } else if (obj instanceof OAuth2Error) {
            return toString().equals(obj.toString());
        } else {
            return false;
        }
    }

    /**
     * Returns the error code specifying the cause of the failure.
     *
     * @return The error code specifying the cause of the failure, or
     *         {@code null} if no error code was provided (which may be the case
     *         for WWW-Authenticate headers).
     */
    public String getError() {
        return error;
    }

    /**
     * Returns the human-readable ASCII text providing additional information,
     * used to assist the client developer in understanding the error that
     * occurred.
     *
     * @return The human-readable ASCII text providing additional information,
     *         or {@code null} if no description was provided.
     */
    public String getErrorDescription() {
        return errorDescription;
    }

    /**
     * Returns a URI identifying a human-readable web page with information
     * about the error, used to provide the client developer with additional
     * information about the error.
     *
     * @return A URI identifying a human-readable web page with information
     *         about the error, or {@code null} if no error URI was provided.
     */
    public String getErrorUri() {
        return errorUri;
    }

    /**
     * Returns the scope of protection required to access the protected
     * resource. The realm is only included with {@code WWW-Authenticate}
     * headers in response to a failure to access a protected resource.
     *
     * @return The scope of protection required to access the protected
     *         resource, or {@code null} if no realm was provided (which will
     *         always be the case for authorization call-back failures and
     *         access/refresh token requests).
     */
    public String getRealm() {
        return realm;
    }

    /**
     * Returns the required scope of the access token for accessing the
     * requested resource. The scope is only included with
     * {@code WWW-Authenticate} headers in response to a failure to access a
     * protected resource.
     *
     * @return The required scope of the access token for accessing the
     *         requested resource, which may be empty (never {@code null}) if no
     *         scope was provided (which will always be the case for
     *         authorization call-back failures and access/refresh token
     *         requests).
     */
    public List<String> getScope() {
        return scope;
    }

    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    /**
     * Returns {@code true} if this error includes an error code and it matches
     * the provided error code.
     *
     * @param error
     *            The error code.
     * @return {@code true} if this error includes an error code and it matches
     *         the provided error code.
     */
    public boolean is(final String error) {
        return error.equalsIgnoreCase(this.error);
    }

    /**
     * Returns the form representation of this error suitable for inclusion in
     * an authorization call-back query. Only the error, error description, and
     * error URI fields will be included.
     *
     * @return The form representation of this error suitable for inclusion in
     *         an authorization call-back query.
     */
    public Form toForm() {
        final Form form = new Form();
        if (error != null) {
            form.add(F_ERROR, error);
        }
        if (errorDescription != null) {
            form.add(F_ERROR_DESCRIPTION, errorDescription);
        }
        if (errorUri != null) {
            form.add(F_ERROR_URI, errorUri);
        }
        return form;
    }

    /**
     * Returns the JSON representation of this error formatted as an access
     * token error response. Only the error, error description, and error URI
     * fields will be included.
     *
     * @return The JSON representation of this error formatted as an access
     *         token error response.
     */
    public Map<String, Object> toJsonContent() {
        final Map<String, Object> json = new LinkedHashMap<>(3);
        if (error != null) {
            json.put(F_ERROR, error);
        }
        if (errorDescription != null) {
            json.put(F_ERROR_DESCRIPTION, errorDescription);
        }
        if (errorUri != null) {
            json.put(F_ERROR_URI, errorUri);
        }
        return json;
    }

    @Override
    public String toString() {
        // Use lazy initialization: minor race conditions don't matter.
        if (stringValue == null) {
            final StringBuilder builder = new StringBuilder();
            appendAttribute(builder, F_REALM, realm);
            appendAttribute(builder, F_SCOPE, scope.isEmpty() ? null : joinAsString(" ", scope));
            appendAttribute(builder, F_ERROR, error);
            appendAttribute(builder, F_ERROR_DESCRIPTION, errorDescription);
            appendAttribute(builder, F_ERROR_URI, errorUri);
            stringValue = builder.toString();
        }
        return stringValue;
    }

    /**
     * Returns the string representation of this error formatted as a
     * {@code WWW-Authenticate} header.
     *
     * @return The string representation of this error formatted as a
     *         {@code WWW-Authenticate} header.
     */
    public String toWWWAuthenticateHeader() {
        final String stringValue = toString();
        return stringValue.isEmpty() ? H_BEARER : H_BEARER_WITH_SPACE + stringValue;
    }

    private void addSeparator(final StringBuilder builder) {
        if (builder.length() > 0) {
            builder.append(", ");
        }
    }

    private void appendAttribute(final StringBuilder builder, final String key, final String value) {
        if (value != null) {
            addSeparator(builder);
            builder.append(key).append('=').append(quote(value));
        }
    }
}