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