001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk;
019
020
021import java.io.Serializable;
022import java.net.URI;
023import java.net.URISyntaxException;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029import net.jcip.annotations.Immutable;
030import net.minidev.json.JSONObject;
031
032import com.nimbusds.common.contenttype.ContentType;
033import com.nimbusds.oauth2.sdk.http.HTTPResponse;
034import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
035import com.nimbusds.oauth2.sdk.util.MapUtils;
036import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
037
038
039/**
040 * Error object, used to encapsulate OAuth 2.0 and other errors. Supports
041 * custom parameters.
042 *
043 * <p>Example error object as HTTP response:
044 *
045 * <pre>
046 * HTTP/1.1 400 Bad Request
047 * Content-Type: application/json;charset=UTF-8
048 * Cache-Control: no-store
049 * Pragma: no-cache
050 *
051 * {
052 *   "error" : "invalid_request"
053 * }
054 * </pre>
055 */
056@Immutable
057public class ErrorObject implements Serializable {
058        
059        
060        private static final long serialVersionUID = -361808781364656206L;
061        
062        
063        /**
064         * The error code, may not always be defined.
065         */
066        private final String code;
067
068
069        /**
070         * Optional error description.
071         */
072        private final String description;
073
074
075        /**
076         * Optional HTTP status code, 0 if not specified.
077         */
078        private final int httpStatusCode;
079
080
081        /**
082         * Optional URI of a web page that includes additional information 
083         * about the error.
084         */
085        private final URI uri;
086        
087        
088        /**
089         * Optional custom parameters, empty or {@code null} if none.
090         */
091        private final Map<String,String> customParams;
092
093
094        /**
095         * Creates a new error with the specified code. The code must be within
096         * the {@link #isLegal(String) legal} character range.
097         *
098         * @param code The error code, {@code null} if not specified.
099         */
100        public ErrorObject(final String code) {
101        
102                this(code, null, 0, null);
103        }
104        
105        
106        /**
107         * Creates a new error with the specified code and description. The
108         * code and the description must be within the {@link #isLegal(String)
109         * legal} character range.
110         *
111         * @param code        The error code, {@code null} if not specified.
112         * @param description The error description, {@code null} if not
113         *                    specified.
114         */
115        public ErrorObject(final String code, final String description) {
116        
117                this(code, description, 0, null);
118        }
119
120
121        /**
122         * Creates a new error with the specified code, description and HTTP 
123         * status code. The code and the description must be within the
124         * {@link #isLegal(String) legal} character range.
125         *
126         * @param code           The error code, {@code null} if not specified.
127         * @param description    The error description, {@code null} if not
128         *                       specified.
129         * @param httpStatusCode The HTTP status code, zero if not specified.
130         */
131        public ErrorObject(final String code, final String description, final int httpStatusCode) {
132        
133                this(code, description, httpStatusCode, null);
134        }
135
136
137        /**
138         * Creates a new error with the specified code, description, HTTP 
139         * status code and page URI. The code and the description must be
140         * within the {@link #isLegal(String) legal} character range.
141         *
142         * @param code           The error code, {@code null} if not specified.
143         * @param description    The error description, {@code null} if not
144         *                       specified.
145         * @param httpStatusCode The HTTP status code, zero if not specified.
146         * @param uri            The error page URI, {@code null} if not
147         *                       specified.
148         */
149        public ErrorObject(final String code,
150                           final String description,
151                           final int httpStatusCode,
152                           final URI uri) {
153        
154                this(code, description, httpStatusCode, uri, null);
155        }
156
157
158        /**
159         * Creates a new error with the specified code, description, HTTP
160         * status code and page URI. The code and the description must be
161         * within the {@link #isLegal(String) legal} character range.
162         *
163         * @param code           The error code, {@code null} if not specified.
164         * @param description    The error description, {@code null} if not
165         *                       specified.
166         * @param httpStatusCode The HTTP status code, zero if not specified.
167         * @param uri            The error page URI, {@code null} if not
168         *                       specified.
169         * @param customParams   Custom parameters, {@code null} if none.
170         */
171        public ErrorObject(final String code,
172                           final String description,
173                           final int httpStatusCode,
174                           final URI uri,
175                           final Map<String,String> customParams) {
176        
177                if (! isLegal(code)) {
178                        throw new IllegalArgumentException("Illegal char(s) in code, see RFC 6749, section 5.2");
179                }
180                this.code = code;
181                
182                if (! isLegal(description)) {
183                        throw new IllegalArgumentException("Illegal char(s) in description, see RFC 6749, section 5.2");
184                }
185                this.description = description;
186                
187                this.httpStatusCode = httpStatusCode;
188                this.uri = uri;
189                
190                this.customParams = customParams;
191        }
192
193
194        /**
195         * Returns the error code.
196         *
197         * @return The error code, {@code null} if not specified.
198         */
199        public String getCode() {
200
201                return code;
202        }
203        
204        
205        /**
206         * Returns the error description.
207         *
208         * @return The error description, {@code null} if not specified.
209         */
210        public String getDescription() {
211        
212                return description;
213        }
214
215
216        /**
217         * Sets the error description.
218         *
219         * @param description The error description, {@code null} if not 
220         *                    specified.
221         *
222         * @return A copy of this error with the specified description.
223         */
224        public ErrorObject setDescription(final String description) {
225
226                return new ErrorObject(getCode(), description, getHTTPStatusCode(), getURI(), getCustomParams());
227        }
228
229
230        /**
231         * Appends the specified text to the error description.
232         *
233         * @param text The text to append to the error description, 
234         *             {@code null} if not specified.
235         *
236         * @return A copy of this error with the specified appended 
237         *         description.
238         */
239        public ErrorObject appendDescription(final String text) {
240
241                String newDescription;
242
243                if (getDescription() != null)
244                        newDescription = getDescription() + text;
245                else
246                        newDescription = text;
247
248                return new ErrorObject(getCode(), newDescription, getHTTPStatusCode(), getURI(), getCustomParams());
249        }
250
251
252        /**
253         * Returns the HTTP status code.
254         *
255         * @return The HTTP status code, zero if not specified.
256         */
257        public int getHTTPStatusCode() {
258
259                return httpStatusCode;
260        }
261
262
263        /**
264         * Sets the HTTP status code.
265         *
266         * @param httpStatusCode  The HTTP status code, zero if not specified.
267         *
268         * @return A copy of this error with the specified HTTP status code.
269         */
270        public ErrorObject setHTTPStatusCode(final int httpStatusCode) {
271
272                return new ErrorObject(getCode(), getDescription(), httpStatusCode, getURI(), getCustomParams());
273        }
274
275
276        /**
277         * Returns the error page URI.
278         *
279         * @return The error page URI, {@code null} if not specified.
280         */
281        public URI getURI() {
282
283                return uri;
284        }
285
286
287        /**
288         * Sets the error page URI.
289         *
290         * @param uri The error page URI, {@code null} if not specified.
291         *
292         * @return A copy of this error with the specified page URI.
293         */
294        public ErrorObject setURI(final URI uri) {
295
296                return new ErrorObject(getCode(), getDescription(), getHTTPStatusCode(), uri, getCustomParams());
297        }
298        
299        
300        /**
301         * Returns the custom parameters.
302         *
303         * @return The custom parameters, empty map if none.
304         */
305        public Map<String,String> getCustomParams() {
306                if (MapUtils.isNotEmpty(customParams)) {
307                        return Collections.unmodifiableMap(customParams);
308                } else {
309                        return Collections.emptyMap();
310                }
311        }
312        
313        
314        /**
315         * Sets the custom parameters.
316         *
317         * @param customParams The custom parameters, {@code null} if none.
318         *
319         * @return A copy of this error with the specified custom parameters.
320         */
321        public ErrorObject setCustomParams(final Map<String,String> customParams) {
322                
323                return new ErrorObject(getCode(), getDescription(), getHTTPStatusCode(), getURI(), customParams);
324        }
325
326
327        /**
328         * Returns a JSON object representation of this error object.
329         *
330         * <p>Example:
331         *
332         * <pre>
333         * {
334         *   "error"             : "invalid_grant",
335         *   "error_description" : "Invalid resource owner credentials"
336         * }
337         * </pre>
338         *
339         * @return The JSON object.
340         */
341        public JSONObject toJSONObject() {
342
343                JSONObject o = new JSONObject();
344
345                if (getCode() != null) {
346                        o.put("error", getCode());
347                }
348
349                if (getDescription() != null) {
350                        o.put("error_description", getDescription());
351                }
352
353                if (getURI() != null) {
354                        o.put("error_uri", getURI().toString());
355                }
356                
357                if (! getCustomParams().isEmpty()) {
358                        o.putAll(getCustomParams());
359                }
360
361                return o;
362        }
363        
364        
365        /**
366         * Returns a parameters representation of this error object. Suitable
367         * for URL-encoded error responses.
368         *
369         * @return The parameters.
370         */
371        public Map<String, List<String>> toParameters() {
372                
373                Map<String,List<String>> params = new HashMap<>();
374                
375                if (getCode() != null) {
376                        params.put("error", Collections.singletonList(getCode()));
377                }
378                
379                if (getDescription() != null) {
380                        params.put("error_description", Collections.singletonList(getDescription()));
381                }
382                
383                if (getURI() != null) {
384                        params.put("error_uri", Collections.singletonList(getURI().toString()));
385                }
386                
387                if (! getCustomParams().isEmpty()) {
388                        for (Map.Entry<String, String> en: getCustomParams().entrySet()) {
389                                params.put(en.getKey(), Collections.singletonList(en.getValue()));
390                        }
391                }
392                
393                return params;
394        }
395        
396        
397        /**
398         * Returns an HTTP response for this error object. If no HTTP status
399         * code is specified it will be set to 400 (Bad Request). If an error
400         * code is specified the {@code Content-Type} header will be set to
401         * {@link ContentType#APPLICATION_JSON application/json; charset=UTF-8}
402         * and the error JSON object will be put in the entity body.
403         *
404         * @return The HTTP response.
405         */
406        public HTTPResponse toHTTPResponse() {
407                
408                int statusCode = (getHTTPStatusCode() > 0) ? getHTTPStatusCode() : HTTPResponse.SC_BAD_REQUEST;
409                HTTPResponse httpResponse = new HTTPResponse(statusCode);
410                httpResponse.setCacheControl("no-store");
411                httpResponse.setPragma("no-cache");
412                
413                if (getCode() != null) {
414                        httpResponse.setEntityContentType(ContentType.APPLICATION_JSON);
415                        httpResponse.setContent(toJSONObject().toJSONString());
416                }
417                
418                return httpResponse;
419        }
420
421
422        /**
423         * @see #getCode
424         */
425        @Override
426        public String toString() {
427
428                return code != null ? code : "null";
429        }
430
431
432        @Override
433        public int hashCode() {
434
435                return code != null ? code.hashCode() : "null".hashCode();
436        }
437
438
439        @Override
440        public boolean equals(final Object object) {
441        
442                return object instanceof ErrorObject &&
443                       this.toString().equals(object.toString());
444        }
445
446
447        /**
448         * Parses an error object from the specified JSON object.
449         *
450         * @param jsonObject The JSON object to parse. Must not be
451         *                   {@code null}.
452         *
453         * @return The error object.
454         */
455        public static ErrorObject parse(final JSONObject jsonObject) {
456
457                String code = null;
458                try {
459                        code = JSONObjectUtils.getString(jsonObject, "error", null);
460                } catch (ParseException e) {
461                        // ignore and continue
462                }
463                
464                if (! isLegal(code)) {
465                        code = null;
466                }
467                
468                String description = null;
469                try {
470                        description = JSONObjectUtils.getString(jsonObject, "error_description", null);
471                } catch (ParseException e) {
472                        // ignore and continue
473                }
474                
475                URI uri = null;
476                try {
477                        uri = JSONObjectUtils.getURI(jsonObject, "error_uri", null);
478                } catch (ParseException e) {
479                        // ignore and continue
480                }
481                
482                Map<String, String> customParams = null;
483                for (Map.Entry<String, Object> en: jsonObject.entrySet()) {
484                        if (!"error".equals(en.getKey()) && !"error_description".equals(en.getKey()) && !"error_uri".equals(en.getKey())) {
485                                if (en.getValue() == null || en.getValue() instanceof String) {
486                                        if (customParams == null) {
487                                                customParams = new HashMap<>();
488                                        }
489                                        customParams.put(en.getKey(), (String)en.getValue());
490                                }
491                        }
492                }
493
494                return new ErrorObject(code, removeIllegalChars(description), 0, uri, customParams);
495        }
496        
497        
498        /**
499         * Parses an error object from the specified parameters representation.
500         * Suitable for URL-encoded error responses.
501         *
502         * @param params The parameters. Must not be {@code null}.
503         *
504         * @return The error object.
505         */
506        public static ErrorObject parse(final Map<String, List<String>> params) {
507                
508                String code = MultivaluedMapUtils.getFirstValue(params, "error");
509                String description = MultivaluedMapUtils.getFirstValue(params, "error_description");
510                String uriString = MultivaluedMapUtils.getFirstValue(params, "error_uri");
511                
512                if (! isLegal(code)) {
513                        code = null;
514                }
515                
516                URI uri = null;
517                if (uriString != null) {
518                        try {
519                                uri = new URI(uriString);
520                        } catch (URISyntaxException e) {
521                                // ignore
522                        }
523                }
524                
525                Map<String, String> customParams = null;
526                for (Map.Entry<String, List<String>> en: params.entrySet()) {
527                        if (!"error".equals(en.getKey()) && !"error_description".equals(en.getKey()) && !"error_uri".equals(en.getKey())) {
528                        
529                                if (customParams == null) {
530                                        customParams = new HashMap<>();
531                                }
532                                
533                                if (en.getValue() == null) {
534                                        customParams.put(en.getKey(), null);
535                                } else if (! en.getValue().isEmpty()) {
536                                        customParams.put(en.getKey(), en.getValue().get(0));
537                                }
538                        }
539                }
540                
541                return new ErrorObject(code, removeIllegalChars(description), 0, uri, customParams);
542        }
543
544
545        /**
546         * Parses an error object from the specified HTTP response.
547         *
548         * @param httpResponse The HTTP response to parse. Must not be
549         *                     {@code null}.
550         *
551         * @return The error object.
552         */
553        public static ErrorObject parse(final HTTPResponse httpResponse) {
554
555                JSONObject jsonObject;
556                try {
557                        jsonObject = httpResponse.getContentAsJSONObject();
558                } catch (ParseException e) {
559                        return new ErrorObject(null, null, httpResponse.getStatusCode());
560                }
561
562                ErrorObject intermediary = parse(jsonObject);
563
564                return new ErrorObject(
565                        intermediary.getCode(),
566                        intermediary.description,
567                        httpResponse.getStatusCode(),
568                        intermediary.getURI(),
569                        intermediary.getCustomParams());
570        }
571        
572        
573        /**
574         * Removes any characters from the specified string that are not
575         * within the {@link #isLegal(char) legal range} for OAuth 2.0 error
576         * codes and messages.
577         *
578         * <p>See RFC 6749, section 5.2.
579         *
580         * @param s The string to check. May be {@code null}.
581         *
582         * @return The string with removed illegal characters, {@code null} if
583         *         the original string was {@code null}.
584         */
585        public static String removeIllegalChars(final String s) {
586                
587                if (s == null) {
588                        return null;
589                }
590                
591                StringBuilder sb = new StringBuilder();
592                
593                for (char c: s.toCharArray()) {
594                        if (isLegal(c)) {
595                                sb.append(c);
596                        }
597                }
598                
599                return sb.toString();
600        }
601        
602        
603        /**
604         * Returns {@code true} if the characters in the specified string are
605         * within the {@link #isLegal(char) legal ranges} for OAuth 2.0 error
606         * codes and messages.
607         *
608         * <p>See RFC 6749, section 5.2.
609         *
610         * @param s The string to check. May be {@code null}.
611         *
612         * @return {@code true} if the string is legal, else {@code false}.
613         */
614        public static boolean isLegal(final String s) {
615        
616                if (s == null) {
617                        return true;
618                }
619                
620                for (char c: s.toCharArray()) {
621                        if (! isLegal(c)) {
622                                return false;
623                        }
624                }
625                
626                return true;
627        }
628        
629        
630        /**
631         * Returns {@code true} if the specified char is within the legal
632         * ranges [0x20, 0x21] | [0x23 - 0x5B] | [0x5D - 0x7E] for OAuth 2.0
633         * error codes and messages.
634         *
635         * <p>See RFC 6749, section 5.2.
636         *
637         * @param c The character to check. Must not be {@code null}.
638         *
639         * @return {@code true} if the character is legal, else {@code false}.
640         */
641        public static boolean isLegal(final char c) {
642                
643                // https://tools.ietf.org/html/rfc6749#section-5.2
644                //
645                // Values for the "error" parameter MUST NOT include characters outside the
646                // set %x20-21 / %x23-5B / %x5D-7E.
647                //
648                // Values for the "error_description" parameter MUST NOT include characters
649                // outside the set %x20-21 / %x23-5B / %x5D-7E.
650                
651                if (c > 0x7f) {
652                        // Not ASCII
653                        return false;
654                }
655                
656                return c >= 0x20 && c <= 0x21 || c >= 0x23 && c <=0x5b || c >= 0x5d && c <= 0x7e;
657        }
658}