001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2014-2015 ForgeRock AS.
015 */
016
017package org.forgerock.openig.filter.oauth2.client;
018
019import static org.forgerock.http.header.HeaderUtil.parseParameters;
020import static org.forgerock.http.header.HeaderUtil.quote;
021import static org.forgerock.http.header.HeaderUtil.split;
022import static org.forgerock.util.Utils.joinAsString;
023
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.LinkedHashMap;
027import java.util.List;
028import java.util.Map;
029
030import org.forgerock.http.protocol.Form;
031import org.forgerock.http.protocol.Status;
032import org.forgerock.json.JsonValue;
033import org.forgerock.json.JsonValueException;
034import org.forgerock.util.Reject;
035
036/**
037 * Describes an error which occurred during an OAuth 2.0 authorization request
038 * or when performing an authorized request. More specifically, errors are
039 * communicated:
040 * <ul>
041 * <li>as query parameters in a failed authorization call-back. These errors are
042 * defined in RFC 6749 # 4.1.2 and comprise of an error code, optional error
043 * description, and optional error URI
044 * <li>as JSON encoded content in a failed access token request or failed
045 * refresh token request. These errors are defined in RFC 6749 # 5.2 and
046 * comprise of an error code, optional error description, and optional error URI
047 * <li>using the {@code WWW-Authenticate} response header in response to a
048 * failed attempt to access an OAuth 2.0 protected resource on a resource
049 * server. These errors are defined in RFC 6750 # 3.1 and comprise of an
050 * optional error code, optional error description, optional error URI, optional
051 * list of required scopes, and optional realm.
052 * </ul>
053 *
054 * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749 #
055 *      4.1.2 - The OAuth 2.0 Authorization Framework</a>
056 * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 # 5.2
057 *      - The OAuth 2.0 Authorization Framework</a>
058 * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 - The
059 *      OAuth 2.0 Authorization Framework: Bearer Token Usage</a>
060 */
061public final class OAuth2Error {
062    /**
063     * The resource owner or authorization server denied the request.
064     *
065     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
066     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
067     */
068    public static final String E_ACCESS_DENIED = "access_denied";
069
070    /**
071     * The request requires higher privileges than provided by the access token.
072     * The resource server SHOULD respond with the HTTP 403 (Forbidden) status
073     * code and MAY include the "scope" attribute with the scope necessary to
074     * access the protected resource.
075     *
076     * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 -
077     *      The OAuth 2.0 Authorization Framework: Bearer Token Usage</a>
078     */
079    public static final String E_INSUFFICIENT_SCOPE = "insufficient_scope";
080
081    /**
082     * Client authentication failed (e.g., unknown client, no client
083     * authentication included, or unsupported authentication method). The
084     * authorization server MAY return an HTTP 401 (Unauthorized) status code to
085     * indicate which HTTP authentication schemes are supported. If the client
086     * attempted to authenticate via the "Authorization" request header field,
087     * the authorization server MUST respond with an HTTP 401 (Unauthorized)
088     * status code and include the "WWW-Authenticate" response header field
089     * matching the authentication scheme used by the client.
090     *
091     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
092     *      5.2 - The OAuth 2.0 Authorization Framework</a>
093     */
094    public static final String E_INVALID_CLIENT = "invalid_client";
095
096    /**
097     * The provided authorization grant (e.g., authorization code, resource
098     * owner credentials) or refresh token is invalid, expired, revoked, does
099     * not match the redirection URI used in the authorization request, or was
100     * issued to another client.
101     *
102     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
103     *      5.2 - The OAuth 2.0 Authorization Framework</a>
104     */
105    public static final String E_INVALID_GRANT = "invalid_grant";
106
107    /**
108     * The request is missing a required parameter, includes an unsupported
109     * parameter value (other than grant type), repeats a parameter, includes
110     * multiple credentials, utilizes more than one mechanism for authenticating
111     * the client, or is otherwise malformed. The resource server SHOULD respond
112     * with the HTTP 400 (Bad Request) status code.
113     *
114     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
115     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
116     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
117     *      5.2 - The OAuth 2.0 Authorization Framework</a>
118     * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 -
119     *      The OAuth 2.0 Authorization Framework: Bearer Token Usage</a>
120     */
121    public static final String E_INVALID_REQUEST = "invalid_request";
122
123    /**
124     * The requested scope is invalid, unknown, malformed, or exceeds the scope
125     * granted by the resource owner.
126     *
127     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
128     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
129     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
130     *      5.2 - The OAuth 2.0 Authorization Framework</a>
131     */
132    public static final String E_INVALID_SCOPE = "invalid_scope";
133
134    /**
135     * The access token provided is expired, revoked, malformed, or invalid for
136     * other reasons. The resource SHOULD respond with the HTTP 401
137     * (Unauthorized) status code. The client MAY request a new access token and
138     * retry the protected resource request.
139     *
140     * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 -
141     *      The OAuth 2.0 Authorization Framework: Bearer Token Usage</a>
142     */
143    public static final String E_INVALID_TOKEN = "invalid_token";
144
145    /**
146     * The authorization server encountered an unexpected condition that
147     * prevented it from fulfilling the request. (This error code is needed
148     * because a 500 Internal Server Error HTTP status code cannot be returned
149     * to the client via an HTTP redirect.)
150     *
151     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
152     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
153     */
154    public static final String E_SERVER_ERROR = "server_error";
155
156    /**
157     * The authorization server is currently unable to handle the request due to
158     * a temporary overloading or maintenance of the server. (This error code is
159     * needed because a 503 Service Unavailable HTTP status code cannot be
160     * returned to the client via an HTTP redirect.)
161     *
162     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
163     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
164     */
165    public static final String E_TEMPORARILY_UNAVAILABLE = "temporarily_unavailable";
166
167    /**
168     * The authenticated client is not authorized to use this authorization
169     * grant type.
170     *
171     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
172     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
173     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
174     *      5.2 - The OAuth 2.0 Authorization Framework</a>
175     */
176    public static final String E_UNAUTHORIZED_CLIENT = "unauthorized_client";
177
178    /**
179     * The authorization grant type is not supported by the authorization
180     * server.
181     *
182     * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 #
183     *      5.2 - The OAuth 2.0 Authorization Framework</a>
184     */
185    public static final String E_UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type";
186
187    /**
188     * The authorization server does not support obtaining an authorization code
189     * using this method.
190     *
191     * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749
192     *      # 4.1.2 - The OAuth 2.0 Authorization Framework</a>
193     */
194    public static final String E_UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type";
195
196    /**
197     * The name of the field which communicates the error code.
198     */
199    public static final String F_ERROR = "error";
200
201    /**
202     * The name of the field which communicates the error description.
203     */
204    public static final String F_ERROR_DESCRIPTION = "error_description";
205
206    /**
207     * The name of the field which communicates the error uri.
208     */
209    public static final String F_ERROR_URI = "error_uri";
210
211    /**
212     * The name of the field which communicates the realm.
213     */
214    public static final String F_REALM = "realm";
215
216    /**
217     * The name of the field which communicates the scope.
218     */
219    public static final String F_SCOPE = "scope";
220
221    /**
222     * The WWW-Authenticate header prefix, 'Bearer'.
223     */
224    public static final String H_BEARER = "Bearer";
225
226    /**
227     * Singleton instance used for empty WWW-Authenticate headers.
228     */
229    private static final OAuth2Error EMPTY = new OAuth2Error(null, null, null, null, null);
230
231    /**
232     * The WWW-Authenticate header prefix including trailing space separator.
233     */
234    private static final String H_BEARER_WITH_SPACE = H_BEARER + " ";
235
236    /**
237     * Returns an OAuth 2.0 resource server error whose values are determined on
238     * a best-effort basis from the provided incomplete error and HTTP status
239     * code.
240     *
241     * @param status
242     *            The HTTP status code.
243     * @param incomplete
244     *            The incomplete and possibly {@code null} error.
245     * @return A non-{@code null} error whose error code has been determined
246     *         from the HTTP status code.
247     */
248    public static OAuth2Error bestEffortResourceServerError(final Status status,
249                                                            final OAuth2Error incomplete) {
250        if (incomplete != null && incomplete.error != null) {
251            // Seems ok.
252            return incomplete;
253        }
254        final String error = mapStatusToError(status);
255        if (incomplete == null) {
256            return new OAuth2Error(null, null, error, null, null);
257        } else {
258            return new OAuth2Error(incomplete.getRealm(),
259                                   incomplete.getScope(),
260                                   error,
261                                   incomplete.getErrorDescription(),
262                                   incomplete.getErrorUri());
263        }
264    }
265
266    private static String mapStatusToError(final Status status) {
267        if (Status.BAD_REQUEST.equals(status)) {
268            return E_INVALID_REQUEST;
269        } else if (Status.UNAUTHORIZED.equals(status)) {
270            return E_INVALID_TOKEN;
271        } else if (Status.FORBIDDEN.equals(status)) {
272            return E_INVALID_SCOPE;
273        } else if (Status.METHOD_NOT_ALLOWED.equals(status)) {
274            return E_INVALID_REQUEST;
275        } else if (Status.INTERNAL_SERVER_ERROR.equals(status)) {
276            return E_SERVER_ERROR;
277        } else if (Status.SERVICE_UNAVAILABLE.equals(status)) {
278            return E_TEMPORARILY_UNAVAILABLE;
279        } else {
280            return E_SERVER_ERROR; // No idea
281        }
282    }
283
284    /**
285     * Returns an OAuth 2.0 error suitable for inclusion in authorization
286     * call-back responses and access token and refresh token responses.
287     *
288     * @param error
289     *            The error code specifying the cause of the failure.
290     * @param errorDescription
291     *            The human-readable ASCII text providing additional
292     *            information, or {@code null}.
293     * @return The OAuth 2.0 error.
294     * @throws NullPointerException
295     *             If {@code error} was {@code null}.
296     */
297    public static OAuth2Error newAuthorizationServerError(final String error,
298            final String errorDescription) {
299        Reject.ifNull(error);
300        return new OAuth2Error(null, null, error, errorDescription, null);
301    }
302
303    /**
304     * Returns an OAuth 2.0 error suitable for inclusion in authorization
305     * call-back responses and access token and refresh token responses.
306     *
307     * @param error
308     *            The error code specifying the cause of the failure.
309     * @param errorDescription
310     *            The human-readable ASCII text providing additional
311     *            information, or {@code null}.
312     * @param errorUri
313     *            A URI identifying a human-readable web page with information
314     *            about the error, or {@code null}.
315     * @return The OAuth 2.0 error.
316     * @throws NullPointerException
317     *             If {@code error} was {@code null}.
318     */
319    public static OAuth2Error newAuthorizationServerError(final String error,
320            final String errorDescription, final String errorUri) {
321        Reject.ifNull(error);
322        return new OAuth2Error(null, null, error, errorDescription, errorUri);
323    }
324
325    /**
326     * Returns an OAuth 2.0 error suitable for inclusion in resource server
327     * WWW-Authenticate response headers.
328     *
329     * @param realm
330     *            The scope of protection required to access the protected
331     *            resource, or {@code null}.
332     * @param scope
333     *            The required scope(s) of the access token for accessing the
334     *            requested resource, or {@code null}.
335     * @param error
336     *            The error code specifying the cause of the failure, or
337     *            {@code null}.
338     * @param errorDescription
339     *            The human-readable ASCII text providing additional
340     *            information, or {@code null}.
341     * @param errorUri
342     *            A URI identifying a human-readable web page with information
343     *            about the error, or {@code null}.
344     * @return The OAuth 2.0 error.
345     */
346    public static OAuth2Error newResourceServerError(final String realm, final List<String> scope,
347            final String error, final String errorDescription, final String errorUri) {
348        return new OAuth2Error(realm, scope, error, errorDescription, errorUri);
349    }
350
351    /**
352     * Parses the provided {@link #toString()} representation as an OAuth 2.0
353     * error.
354     *
355     * @param s
356     *            The string to parse.
357     * @return The parsed OAuth 2.0 error.
358     */
359    public static OAuth2Error valueOf(final String s) {
360        final List<String> attributes = split(s, ',');
361        final Map<String, String> map = parseParameters(attributes);
362        final String realm = map.get("realm");
363        final String scopeString = map.get("scope");
364        final List<String> scopes =
365                scopeString != null ? Arrays.asList(scopeString.trim().split("\\s+")) : null;
366        final String error = map.get("error");
367        final String errorDescription = map.get("error_description");
368        final String errorUri = map.get("error_uri");
369        return new OAuth2Error(realm, scopes, error, errorDescription, errorUri);
370    }
371
372    /**
373     * Parses the Form representation of an authorization call-back error as an
374     * OAuth 2.0 error. Only the error, error description, and error URI fields
375     * will be included.
376     *
377     * @param form
378     *            The Form representation of an authorization call-back error.
379     * @return The parsed OAuth 2.0 error.
380     */
381    public static OAuth2Error valueOfForm(final Form form) {
382        return new OAuth2Error(null, null, form.getFirst(F_ERROR), form
383                .getFirst(F_ERROR_DESCRIPTION), form.getFirst(F_ERROR_URI));
384    }
385
386    /**
387     * Parses the JSON representation of an access token error response as an
388     * OAuth 2.0 error. Only the error, error description, and error URI fields
389     * will be included.
390     *
391     * @param json
392     *            The JSON representation of an access token error response.
393     * @return The parsed OAuth 2.0 error.
394     * @throws IllegalArgumentException
395     *             If the JSON content was malformed.
396     */
397    public static OAuth2Error valueOfJsonContent(final Map<String, Object> json) {
398        final JsonValue jv = new JsonValue(json);
399        try {
400            return new OAuth2Error(null, null, jv.get(F_ERROR).asString(), jv.get(
401                    F_ERROR_DESCRIPTION).asString(), jv.get(F_ERROR_URI).asString());
402        } catch (final JsonValueException e) {
403            throw new IllegalArgumentException(e);
404        }
405    }
406
407    /**
408     * Parses the provided WWW-Authenticate header content as an OAuth 2.0
409     * error.
410     *
411     * @param s
412     *            The string containing the WWW-Authenticate header content.
413     * @return The parsed OAuth 2.0 error.
414     * @throws IllegalArgumentException
415     *             If the header value was malformed.
416     */
417    public static OAuth2Error valueOfWWWAuthenticateHeader(final String s) {
418        if (H_BEARER.equals(s)) {
419            return EMPTY;
420        } else if (s.startsWith(H_BEARER_WITH_SPACE)) {
421            return valueOf(s.substring(H_BEARER_WITH_SPACE.length()));
422        } else {
423            throw new IllegalArgumentException("Malformed WWW-Authenticate header '" + s + "'");
424        }
425    }
426
427    private final String error;
428    private final String errorDescription;
429    private final String errorUri;
430    private final String realm;
431    private final List<String> scope;
432    private transient String stringValue;
433
434    private OAuth2Error(final String realm, final List<String> scope, final String error,
435            final String errorDescription, final String errorUri) {
436        this.realm = realm;
437        this.scope =
438                scope != null ? Collections.unmodifiableList(scope) : Collections
439                        .<String> emptyList();
440        this.error = error;
441        this.errorDescription = errorDescription;
442        this.errorUri = errorUri;
443    }
444
445    @Override
446    public boolean equals(final Object obj) {
447        if (this == obj) {
448            return true;
449        } else if (obj instanceof OAuth2Error) {
450            return toString().equals(obj.toString());
451        } else {
452            return false;
453        }
454    }
455
456    /**
457     * Returns the error code specifying the cause of the failure.
458     *
459     * @return The error code specifying the cause of the failure, or
460     *         {@code null} if no error code was provided (which may be the case
461     *         for WWW-Authenticate headers).
462     */
463    public String getError() {
464        return error;
465    }
466
467    /**
468     * Returns the human-readable ASCII text providing additional information,
469     * used to assist the client developer in understanding the error that
470     * occurred.
471     *
472     * @return The human-readable ASCII text providing additional information,
473     *         or {@code null} if no description was provided.
474     */
475    public String getErrorDescription() {
476        return errorDescription;
477    }
478
479    /**
480     * Returns a URI identifying a human-readable web page with information
481     * about the error, used to provide the client developer with additional
482     * information about the error.
483     *
484     * @return A URI identifying a human-readable web page with information
485     *         about the error, or {@code null} if no error URI was provided.
486     */
487    public String getErrorUri() {
488        return errorUri;
489    }
490
491    /**
492     * Returns the scope of protection required to access the protected
493     * resource. The realm is only included with {@code WWW-Authenticate}
494     * headers in response to a failure to access a protected resource.
495     *
496     * @return The scope of protection required to access the protected
497     *         resource, or {@code null} if no realm was provided (which will
498     *         always be the case for authorization call-back failures and
499     *         access/refresh token requests).
500     */
501    public String getRealm() {
502        return realm;
503    }
504
505    /**
506     * Returns the required scope of the access token for accessing the
507     * requested resource. The scope is only included with
508     * {@code WWW-Authenticate} headers in response to a failure to access a
509     * protected resource.
510     *
511     * @return The required scope of the access token for accessing the
512     *         requested resource, which may be empty (never {@code null}) if no
513     *         scope was provided (which will always be the case for
514     *         authorization call-back failures and access/refresh token
515     *         requests).
516     */
517    public List<String> getScope() {
518        return scope;
519    }
520
521    @Override
522    public int hashCode() {
523        return toString().hashCode();
524    }
525
526    /**
527     * Returns {@code true} if this error includes an error code and it matches
528     * the provided error code.
529     *
530     * @param error
531     *            The error code.
532     * @return {@code true} if this error includes an error code and it matches
533     *         the provided error code.
534     */
535    public boolean is(final String error) {
536        return error.equalsIgnoreCase(this.error);
537    }
538
539    /**
540     * Returns the form representation of this error suitable for inclusion in
541     * an authorization call-back query. Only the error, error description, and
542     * error URI fields will be included.
543     *
544     * @return The form representation of this error suitable for inclusion in
545     *         an authorization call-back query.
546     */
547    public Form toForm() {
548        final Form form = new Form();
549        if (error != null) {
550            form.add(F_ERROR, error);
551        }
552        if (errorDescription != null) {
553            form.add(F_ERROR_DESCRIPTION, errorDescription);
554        }
555        if (errorUri != null) {
556            form.add(F_ERROR_URI, errorUri);
557        }
558        return form;
559    }
560
561    /**
562     * Returns the JSON representation of this error formatted as an access
563     * token error response. Only the error, error description, and error URI
564     * fields will be included.
565     *
566     * @return The JSON representation of this error formatted as an access
567     *         token error response.
568     */
569    public Map<String, Object> toJsonContent() {
570        final Map<String, Object> json = new LinkedHashMap<>(3);
571        if (error != null) {
572            json.put(F_ERROR, error);
573        }
574        if (errorDescription != null) {
575            json.put(F_ERROR_DESCRIPTION, errorDescription);
576        }
577        if (errorUri != null) {
578            json.put(F_ERROR_URI, errorUri);
579        }
580        return json;
581    }
582
583    @Override
584    public String toString() {
585        // Use lazy initialization: minor race conditions don't matter.
586        if (stringValue == null) {
587            final StringBuilder builder = new StringBuilder();
588            appendAttribute(builder, F_REALM, realm);
589            appendAttribute(builder, F_SCOPE, scope.isEmpty() ? null : joinAsString(" ", scope));
590            appendAttribute(builder, F_ERROR, error);
591            appendAttribute(builder, F_ERROR_DESCRIPTION, errorDescription);
592            appendAttribute(builder, F_ERROR_URI, errorUri);
593            stringValue = builder.toString();
594        }
595        return stringValue;
596    }
597
598    /**
599     * Returns the string representation of this error formatted as a
600     * {@code WWW-Authenticate} header.
601     *
602     * @return The string representation of this error formatted as a
603     *         {@code WWW-Authenticate} header.
604     */
605    public String toWWWAuthenticateHeader() {
606        final String stringValue = toString();
607        return stringValue.isEmpty() ? H_BEARER : H_BEARER_WITH_SPACE + stringValue;
608    }
609
610    private void addSeparator(final StringBuilder builder) {
611        if (builder.length() > 0) {
612            builder.append(", ");
613        }
614    }
615
616    private void appendAttribute(final StringBuilder builder, final String key, final String value) {
617        if (value != null) {
618            addSeparator(builder);
619            builder.append(key).append('=').append(quote(value));
620        }
621    }
622}