001/*
002 * Copyright 2015 The AppAuth for Android Authors. All Rights Reserved.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the
010 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
011 * express or implied. See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package net.openid.appauth;
016
017import static net.openid.appauth.Preconditions.checkNotEmpty;
018import static net.openid.appauth.Preconditions.checkNotNull;
019
020import android.content.Intent;
021import android.net.Uri;
022import androidx.annotation.NonNull;
023import androidx.annotation.Nullable;
024import androidx.annotation.VisibleForTesting;
025import androidx.collection.ArrayMap;
026
027import org.json.JSONException;
028import org.json.JSONObject;
029
030import java.util.Collections;
031import java.util.Map;
032
033/**
034 * Returned as a response to OAuth2 requests if they fail. Specifically:
035 *
036 * - The {@link net.openid.appauth.AuthorizationService.TokenResponseCallback response} to
037 * {@link AuthorizationService#performTokenRequest(net.openid.appauth.TokenRequest,
038 * AuthorizationService.TokenResponseCallback) token requests},
039 *
040 * - The {@link net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback
041 * response}
042 * to
043 * {@link AuthorizationServiceConfiguration#fetchFromUrl(android.net.Uri,
044 * AuthorizationServiceConfiguration.RetrieveConfigurationCallback) configuration retrieval}.
045 */
046@SuppressWarnings({"ThrowableInstanceNeverThrown", "ThrowableResultOfMethodCallIgnored"})
047public final class AuthorizationException extends Exception {
048
049    /**
050     * The extra string that used to store an {@link AuthorizationException} in an intent by
051     * {@link #toIntent()}.
052     */
053    public static final String EXTRA_EXCEPTION = "net.openid.appauth.AuthorizationException";
054
055    /**
056     * The OAuth2 parameter used to indicate the type of error during an authorization or
057     * token request.
058     *
059     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.2.1
060     * <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>"
061     * @see "The OAuth 2.0 Authorization Framework" (RFC 6749), Section 5.2
062     * <https://tools.ietf.org/html/rfc6749#section-5.2>"
063     */
064    public static final String PARAM_ERROR = "error";
065
066    /**
067     * The OAuth2 parameter used to provide a human readable description of the error which
068     * occurred.
069     *
070     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.2.1
071     * <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>"
072     * @see "The OAuth 2.0 Authorization Framework" (RFC 6749), Section 5.2
073     * <https://tools.ietf.org/html/rfc6749#section-5.2>"
074     */
075    public static final String PARAM_ERROR_DESCRIPTION = "error_description";
076
077    /**
078     * The OAuth2 parameter used to provide a URI to a human-readable page which describes the
079     * error.
080     *
081     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.2.1
082     * <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>"
083     * @see "The OAuth 2.0 Authorization Framework" (RFC 6749), Section 5.2
084     * <https://tools.ietf.org/html/rfc6749#section-5.2>"
085     */
086    public static final String PARAM_ERROR_URI = "error_uri";
087
088
089    /**
090     * The error type used for all errors that are not specific to OAuth related responses.
091     */
092    public static final int TYPE_GENERAL_ERROR = 0;
093
094    /**
095     * The error type for OAuth specific errors on the authorization endpoint. This error type is
096     * used when the server responds to an authorization request with an explicit OAuth error, as
097     * defined by [the OAuth2 specification, section 4.1.2.1](
098     * https://tools.ietf.org/html/rfc6749#section-4.1.2.1). If the authorization response is
099     * invalid and not explicitly an error response, another error type will be used.
100     *
101     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.2.1
102     * <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>"
103     */
104    public static final int TYPE_OAUTH_AUTHORIZATION_ERROR = 1;
105
106    /**
107     * The error type for OAuth specific errors on the token endpoint. This error type is used when
108     * the server responds with HTTP 400 and an OAuth error, as defined by
109     * [the OAuth2 specification, section 5.2](https://tools.ietf.org/html/rfc6749#section-5.2).
110     * If an HTTP 400 response does not parse as an OAuth error (i.e. no 'error' field is present
111     * or the JSON is invalid), another error domain will be used.
112     *
113     * @see "The OAuth 2.0 Authorization Framework" (RFC 6749), Section 5.2
114     * <https://tools.ietf.org/html/rfc6749#section-5.2>"
115     */
116    public static final int TYPE_OAUTH_TOKEN_ERROR = 2;
117
118    /**
119     * The error type for authorization errors encountered out of band on the resource server.
120     */
121    public static final int TYPE_RESOURCE_SERVER_AUTHORIZATION_ERROR = 3;
122
123    /**
124     * The error type for OAuth specific errors on the registration endpoint.
125     */
126    public static final int TYPE_OAUTH_REGISTRATION_ERROR = 4;
127
128    @VisibleForTesting
129    static final String KEY_TYPE = "type";
130
131    @VisibleForTesting
132    static final String KEY_CODE = "code";
133
134    @VisibleForTesting
135    static final String KEY_ERROR = "error";
136
137    @VisibleForTesting
138    static final String KEY_ERROR_DESCRIPTION = "errorDescription";
139
140    @VisibleForTesting
141    static final String KEY_ERROR_URI = "errorUri";
142
143    /**
144     * Prime number multiplier used to produce a reasonable hash value distribution.
145     */
146    private static final int HASH_MULTIPLIER = 31;
147
148    /**
149     * Error codes specific to AppAuth for Android, rather than those defined in the OAuth2 and
150     * OpenID specifications.
151     */
152    public static final class GeneralErrors {
153        // codes in this group should be between 0-999
154
155        /**
156         * Indicates a problem parsing an OpenID Connect Service Discovery document.
157         */
158        public static final AuthorizationException INVALID_DISCOVERY_DOCUMENT =
159                generalEx(0, "Invalid discovery document");
160
161        /**
162         * Indicates the user manually canceled the OAuth authorization code flow.
163         */
164        public static final AuthorizationException USER_CANCELED_AUTH_FLOW =
165                generalEx(1, "User cancelled flow");
166
167        /**
168         * Indicates an OAuth authorization flow was programmatically cancelled.
169         */
170        public static final AuthorizationException PROGRAM_CANCELED_AUTH_FLOW =
171                generalEx(2, "Flow cancelled programmatically");
172
173        /**
174         * Indicates a network error occurred.
175         */
176        public static final AuthorizationException NETWORK_ERROR =
177                generalEx(3, "Network error");
178
179        /**
180         * Indicates a server error occurred.
181         */
182        public static final AuthorizationException SERVER_ERROR =
183                generalEx(4, "Server error");
184
185        /**
186         * Indicates a problem occurred deserializing JSON.
187         */
188        public static final AuthorizationException JSON_DESERIALIZATION_ERROR =
189                generalEx(5, "JSON deserialization error");
190
191        /**
192         * Indicates a problem occurred constructing a {@link TokenResponse token response} object
193         * from the JSON provided by the server.
194         */
195        public static final AuthorizationException TOKEN_RESPONSE_CONSTRUCTION_ERROR =
196                generalEx(6, "Token response construction error");
197
198        /**
199         * Indicates a problem parsing an OpenID Connect Registration Response.
200         */
201        public static final AuthorizationException INVALID_REGISTRATION_RESPONSE =
202                generalEx(7, "Invalid registration response");
203
204        /**
205         * Indicates that a received ID token could not be parsed
206         */
207        public static final AuthorizationException ID_TOKEN_PARSING_ERROR =
208                generalEx(8, "Unable to parse ID Token");
209
210        /**
211         * Indicates that a received ID token is invalid
212         */
213        public static final AuthorizationException ID_TOKEN_VALIDATION_ERROR =
214                generalEx(9, "Invalid ID Token");
215    }
216
217    /**
218     * Error codes related to failed authorization requests.
219     *
220     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.2.1
221     * <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>"
222     */
223    public static final class AuthorizationRequestErrors {
224        // codes in this group should be between 1000-1999
225
226        /**
227         * An `invalid_request` OAuth2 error response.
228         */
229        public static final AuthorizationException INVALID_REQUEST =
230                authEx(1000, "invalid_request");
231
232        /**
233         * An `unauthorized_client` OAuth2 error response.
234         */
235        public static final AuthorizationException UNAUTHORIZED_CLIENT =
236                authEx(1001, "unauthorized_client");
237
238        /**
239         * An `access_denied` OAuth2 error response.
240         */
241        public static final AuthorizationException ACCESS_DENIED =
242                authEx(1002, "access_denied");
243
244        /**
245         * An `unsupported_response_type` OAuth2 error response.
246         */
247        public static final AuthorizationException UNSUPPORTED_RESPONSE_TYPE =
248                authEx(1003, "unsupported_response_type");
249
250        /**
251         * An `invalid_scope` OAuth2 error response.
252         */
253        public static final AuthorizationException INVALID_SCOPE =
254                authEx(1004, "invalid_scope");
255
256        /**
257         * An `server_error` OAuth2 error response, equivalent to an HTTP 500 error code, but
258         * sent via redirect.
259         */
260        public static final AuthorizationException SERVER_ERROR =
261                authEx(1005, "server_error");
262
263        /**
264         * A `temporarily_unavailable` OAuth2 error response, equivalent to an HTTP 503 error
265         * code, but sent via redirect.
266         */
267        public static final AuthorizationException TEMPORARILY_UNAVAILABLE =
268                authEx(1006, "temporarily_unavailable");
269
270        /**
271         * An authorization error occurring on the client rather than the server. For example,
272         * due to client misconfiguration. This error should be treated as unrecoverable.
273         */
274        public static final AuthorizationException CLIENT_ERROR =
275                authEx(1007, null);
276
277        /**
278         * Indicates an OAuth error as per RFC 6749, but the error code is not known to the
279         * AppAuth for Android library. It could be a custom error or code, or one from an
280         * OAuth extension. The {@link #error} field provides the exact error string returned by
281         * the server.
282         */
283        public static final AuthorizationException OTHER =
284                authEx(1008, null);
285
286        /**
287         * Indicates that the response state param did not match the request state param,
288         * resulting in the response being discarded.
289         */
290        public static final AuthorizationException STATE_MISMATCH =
291                generalEx(9, "Response state param did not match request state");
292
293        private static final Map<String, AuthorizationException> STRING_TO_EXCEPTION =
294                exceptionMapByString(
295                        INVALID_REQUEST,
296                        UNAUTHORIZED_CLIENT,
297                        ACCESS_DENIED,
298                        UNSUPPORTED_RESPONSE_TYPE,
299                        INVALID_SCOPE,
300                        SERVER_ERROR,
301                        TEMPORARILY_UNAVAILABLE,
302                        CLIENT_ERROR,
303                        OTHER);
304
305        /**
306         * Returns the matching exception type for the provided OAuth2 error string, or
307         * {@link #OTHER} if unknown.
308         */
309        @NonNull
310        public static AuthorizationException byString(String error) {
311            AuthorizationException ex = STRING_TO_EXCEPTION.get(error);
312            if (ex != null) {
313                return ex;
314            }
315            return OTHER;
316        }
317    }
318
319    /**
320     * Error codes related to failed token requests.
321     *
322     * @see "The OAuth 2.0 Authorization Framework" (RFC 6749), Section 5.2
323     * <https://tools.ietf.org/html/rfc6749#section-5.2>"
324     */
325    public static final class TokenRequestErrors {
326        // codes in this group should be between 2000-2999
327
328        /**
329         * An `invalid_request` OAuth2 error response.
330         */
331        public static final AuthorizationException INVALID_REQUEST =
332                tokenEx(2000, "invalid_request");
333
334        /**
335         * An `invalid_client` OAuth2 error response.
336         */
337        public static final AuthorizationException INVALID_CLIENT =
338                tokenEx(2001, "invalid_client");
339
340        /**
341         * An `invalid_grant` OAuth2 error response.
342         */
343        public static final AuthorizationException INVALID_GRANT =
344                tokenEx(2002, "invalid_grant");
345
346        /**
347         * An `unauthorized_client` OAuth2 error response.
348         */
349        public static final AuthorizationException UNAUTHORIZED_CLIENT =
350                tokenEx(2003, "unauthorized_client");
351
352        /**
353         * An `unsupported_grant_type` OAuth2 error response.
354         */
355        public static final AuthorizationException UNSUPPORTED_GRANT_TYPE =
356                tokenEx(2004, "unsupported_grant_type");
357
358        /**
359         * An `invalid_scope` OAuth2 error response.
360         */
361        public static final AuthorizationException INVALID_SCOPE =
362                tokenEx(2005, "invalid_scope");
363
364        /**
365         * An authorization error occurring on the client rather than the server. For example,
366         * due to client misconfiguration. This error should be treated as unrecoverable.
367         */
368        public static final AuthorizationException CLIENT_ERROR =
369                tokenEx(2006, null);
370
371        /**
372         * Indicates an OAuth error as per RFC 6749, but the error code is not known to the
373         * AppAuth for Android library. It could be a custom error or code, or one from an
374         * OAuth extension. The {@link #error} field provides the exact error string returned by
375         * the server.
376         */
377        public static final AuthorizationException OTHER =
378                tokenEx(2007, null);
379
380        private static final Map<String, AuthorizationException> STRING_TO_EXCEPTION =
381                exceptionMapByString(
382                        INVALID_REQUEST,
383                        INVALID_CLIENT,
384                        INVALID_GRANT,
385                        UNAUTHORIZED_CLIENT,
386                        UNSUPPORTED_GRANT_TYPE,
387                        INVALID_SCOPE,
388                        CLIENT_ERROR,
389                        OTHER);
390
391        /**
392         * Returns the matching exception type for the provided OAuth2 error string, or
393         * {@link #OTHER} if unknown.
394         */
395        public static AuthorizationException byString(String error) {
396            AuthorizationException ex = STRING_TO_EXCEPTION.get(error);
397            if (ex != null) {
398                return ex;
399            }
400            return OTHER;
401        }
402    }
403
404    /**
405     * Error codes related to failed registration requests.
406     */
407    public static final class RegistrationRequestErrors {
408        // codes in this group should be between 4000-4999
409
410        /**
411         * An `invalid_request` OAuth2 error response.
412         */
413        public static final AuthorizationException INVALID_REQUEST =
414                registrationEx(4000, "invalid_request");
415
416        /**
417         * An `invalid_client` OAuth2 error response.
418         */
419        public static final AuthorizationException INVALID_REDIRECT_URI =
420                registrationEx(4001, "invalid_redirect_uri");
421
422        /**
423         * An `invalid_grant` OAuth2 error response.
424         */
425        public static final AuthorizationException INVALID_CLIENT_METADATA =
426                registrationEx(4002, "invalid_client_metadata");
427
428        /**
429         * An authorization error occurring on the client rather than the server. For example,
430         * due to client misconfiguration. This error should be treated as unrecoverable.
431         */
432        public static final AuthorizationException CLIENT_ERROR =
433                registrationEx(4003, null);
434
435        /**
436         * Indicates an OAuth error as per RFC 6749, but the error code is not known to the
437         * AppAuth for Android library. It could be a custom error or code, or one from an
438         * OAuth extension. The {@link #error} field provides the exact error string returned by
439         * the server.
440         */
441        public static final AuthorizationException OTHER =
442                registrationEx(4004, null);
443
444        private static final Map<String, AuthorizationException> STRING_TO_EXCEPTION =
445                exceptionMapByString(
446                        INVALID_REQUEST,
447                        INVALID_REDIRECT_URI,
448                        INVALID_CLIENT_METADATA,
449                        CLIENT_ERROR,
450                        OTHER);
451
452        /**
453         * Returns the matching exception type for the provided OAuth2 error string, or
454         * {@link #OTHER} if unknown.
455         */
456        public static AuthorizationException byString(String error) {
457            AuthorizationException ex = STRING_TO_EXCEPTION.get(error);
458            if (ex != null) {
459                return ex;
460            }
461            return OTHER;
462        }
463    }
464
465    private static AuthorizationException generalEx(int code, @Nullable String errorDescription) {
466        return new AuthorizationException(
467                TYPE_GENERAL_ERROR, code, null, errorDescription, null, null);
468    }
469
470    private static AuthorizationException authEx(int code, @Nullable String error) {
471        return new AuthorizationException(
472                TYPE_OAUTH_AUTHORIZATION_ERROR, code, error, null, null, null);
473    }
474
475    private static AuthorizationException tokenEx(int code, @Nullable String error) {
476        return new AuthorizationException(
477                TYPE_OAUTH_TOKEN_ERROR, code, error, null, null, null);
478    }
479
480    private static AuthorizationException registrationEx(int code, @Nullable String error) {
481        return new AuthorizationException(
482                TYPE_OAUTH_REGISTRATION_ERROR, code, error, null, null, null);
483    }
484
485    /**
486     * Creates an exception based on one of the existing values defined in
487     * {@link GeneralErrors}, {@link AuthorizationRequestErrors} or {@link TokenRequestErrors},
488     * providing a root cause.
489     */
490    public static AuthorizationException fromTemplate(
491            @NonNull AuthorizationException ex,
492            @Nullable Throwable rootCause) {
493        return new AuthorizationException(
494                ex.type,
495                ex.code,
496                ex.error,
497                ex.errorDescription,
498                ex.errorUri,
499                rootCause);
500    }
501
502    /**
503     * Creates an exception based on one of the existing values defined in
504     * {@link AuthorizationRequestErrors} or {@link TokenRequestErrors}, adding information
505     * retrieved from OAuth error response.
506     */
507    public static AuthorizationException fromOAuthTemplate(
508            @NonNull AuthorizationException ex,
509            @Nullable String errorOverride,
510            @Nullable String errorDescriptionOverride,
511            @Nullable Uri errorUriOverride) {
512        return new AuthorizationException(
513                ex.type,
514                ex.code,
515                (errorOverride != null) ? errorOverride : ex.error,
516                (errorDescriptionOverride != null) ? errorDescriptionOverride : ex.errorDescription,
517                (errorUriOverride != null) ? errorUriOverride : ex.errorUri,
518                null);
519    }
520
521    /**
522     * Creates an exception from an OAuth redirect URI that describes an authorization failure.
523     */
524    public static AuthorizationException fromOAuthRedirect(
525            @NonNull Uri redirectUri) {
526        String error = redirectUri.getQueryParameter(PARAM_ERROR);
527        String errorDescription = redirectUri.getQueryParameter(PARAM_ERROR_DESCRIPTION);
528        String errorUri = redirectUri.getQueryParameter(PARAM_ERROR_URI);
529        AuthorizationException base = AuthorizationRequestErrors.byString(error);
530        return new AuthorizationException(
531                base.type,
532                base.code,
533                error,
534                errorDescription != null ? errorDescription : base.errorDescription,
535                errorUri != null ? Uri.parse(errorUri) : base.errorUri,
536                null);
537    }
538
539    /**
540     * Reconstructs an {@link AuthorizationException} from the JSON produced by
541     * {@link #toJsonString()}.
542     * @throws JSONException if the JSON is malformed or missing required properties
543     */
544    public static AuthorizationException fromJson(@NonNull String jsonStr) throws JSONException {
545        checkNotEmpty(jsonStr, "jsonStr cannot be null or empty");
546        return fromJson(new JSONObject(jsonStr));
547    }
548
549    /**
550     * Reconstructs an {@link AuthorizationException} from the JSON produced by
551     * {@link #toJson()}.
552     * @throws JSONException if the JSON is malformed or missing required properties
553     */
554    public static AuthorizationException fromJson(@NonNull JSONObject json) throws JSONException {
555        checkNotNull(json, "json cannot be null");
556        return new AuthorizationException(
557                json.getInt(KEY_TYPE),
558                json.getInt(KEY_CODE),
559                JsonUtil.getStringIfDefined(json, KEY_ERROR),
560                JsonUtil.getStringIfDefined(json, KEY_ERROR_DESCRIPTION),
561                JsonUtil.getUriIfDefined(json, KEY_ERROR_URI),
562                null);
563    }
564
565    /**
566     * Extracts an {@link AuthorizationException} from an intent produced by {@link #toIntent()}.
567     * This is used to retrieve an error response in the handler registered for a call to
568     * {@link AuthorizationService#performAuthorizationRequest}.
569     */
570    @Nullable
571    public static AuthorizationException fromIntent(Intent data) {
572        checkNotNull(data);
573
574        if (!data.hasExtra(EXTRA_EXCEPTION)) {
575            return null;
576        }
577
578        try {
579            return fromJson(data.getStringExtra(EXTRA_EXCEPTION));
580        } catch (JSONException ex) {
581            throw new IllegalArgumentException("Intent contains malformed exception data", ex);
582        }
583    }
584
585    private static Map<String, AuthorizationException> exceptionMapByString(
586            AuthorizationException... exceptions) {
587        ArrayMap<String, AuthorizationException> map =
588                new ArrayMap<>(exceptions != null ? exceptions.length : 0);
589
590        if (exceptions != null) {
591            for (AuthorizationException ex : exceptions) {
592                if (ex.error != null) {
593                    map.put(ex.error, ex);
594                }
595            }
596        }
597
598        return Collections.unmodifiableMap(map);
599    }
600
601    /**
602     * The type of the error.
603     * @see #TYPE_GENERAL_ERROR
604     * @see #TYPE_OAUTH_AUTHORIZATION_ERROR
605     * @see #TYPE_OAUTH_TOKEN_ERROR
606     * @see #TYPE_RESOURCE_SERVER_AUTHORIZATION_ERROR
607     */
608    public final int type;
609
610    /**
611     * The error code describing the class of problem encountered from the set defined in this
612     * class.
613     */
614    public final int code;
615
616    /**
617     * The error string as it is found in the OAuth2 protocol.
618     */
619    @Nullable
620    public final String error;
621
622    /**
623     * The human readable error message associated with this exception, if available.
624     */
625    @Nullable
626    public final String errorDescription;
627
628    /**
629     * A URI identifying a human-readable web page with information about this error.
630     */
631    @Nullable
632    public final Uri errorUri;
633
634    /**
635     * Instantiates an authorization request with optional root cause information.
636     */
637    public AuthorizationException(
638            int type,
639            int code,
640            @Nullable String error,
641            @Nullable String errorDescription,
642            @Nullable Uri errorUri,
643            @Nullable Throwable rootCause) {
644        super(errorDescription, rootCause);
645        this.type = type;
646        this.code = code;
647        this.error = error;
648        this.errorDescription = errorDescription;
649        this.errorUri = errorUri;
650    }
651
652    /**
653     * Produces a JSON representation of the authorization exception, for transmission or storage.
654     * This does not include any provided root cause.
655     */
656    @NonNull
657    public JSONObject toJson() {
658        JSONObject json = new JSONObject();
659        JsonUtil.put(json, KEY_TYPE, type);
660        JsonUtil.put(json, KEY_CODE, code);
661        JsonUtil.putIfNotNull(json, KEY_ERROR, error);
662        JsonUtil.putIfNotNull(json, KEY_ERROR_DESCRIPTION, errorDescription);
663        JsonUtil.putIfNotNull(json, KEY_ERROR_URI, errorUri);
664        return json;
665    }
666
667    /**
668     * Provides a JSON string representation of an authorization exception, for transmission or
669     * storage. This does not include any provided root cause.
670     */
671    @NonNull
672    public String toJsonString() {
673        return toJson().toString();
674    }
675
676    /**
677     * Creates an intent from this exception. Used to carry error responses to the handling activity
678     * specified in calls to {@link AuthorizationService#performAuthorizationRequest}.
679     */
680    @NonNull
681    public Intent toIntent() {
682        Intent data = new Intent();
683        data.putExtra(EXTRA_EXCEPTION, toJsonString());
684        return data;
685    }
686
687    /**
688     * Exceptions are considered to be equal if their {@link #type type} and {@link #code code}
689     * are the same; all other properties are irrelevant for comparison.
690     */
691    @Override
692    public boolean equals(Object obj) {
693        if (obj == this) {
694            return true;
695        }
696
697        if (obj == null || !(obj instanceof AuthorizationException)) {
698            return false;
699        }
700
701        AuthorizationException other = (AuthorizationException) obj;
702        return this.type == other.type && this.code == other.code;
703    }
704
705    @Override
706    public int hashCode() {
707        // equivalent to Arrays.hashCode(new int[] { type, code });
708        return (HASH_MULTIPLIER * (HASH_MULTIPLIER + type)) + code;
709    }
710
711    @Override
712    public String toString() {
713        return "AuthorizationException: " + toJsonString();
714    }
715}