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.MultivaluedMapUtils;
036
037
038/**
039 * Error object, used to encapsulate OAuth 2.0 and other errors.
040 *
041 * <p>Example error object as HTTP response:
042 *
043 * <pre>
044 * HTTP/1.1 400 Bad Request
045 * Content-Type: application/json;charset=UTF-8
046 * Cache-Control: no-store
047 * Pragma: no-cache
048 *
049 * {
050 *   "error" : "invalid_request"
051 * }
052 * </pre>
053 */
054@Immutable
055public class ErrorObject implements Serializable {
056        
057        
058        private static final long serialVersionUID = -361808781364656206L;
059        
060        
061        /**
062         * The error code, may not always be defined.
063         */
064        private final String code;
065
066
067        /**
068         * Optional error description.
069         */
070        private final String description;
071
072
073        /**
074         * Optional HTTP status code, 0 if not specified.
075         */
076        private final int httpStatusCode;
077
078
079        /**
080         * Optional URI of a web page that includes additional information 
081         * about the error.
082         */
083        private final URI uri;
084
085
086        /**
087         * Creates a new error with the specified code. The code must be within
088         * the {@link #isLegal(String) legal} character range.
089         *
090         * @param code The error code, {@code null} if not specified.
091         */
092        public ErrorObject(final String code) {
093        
094                this(code, null, 0, null);
095        }
096        
097        
098        /**
099         * Creates a new error with the specified code and description. The
100         * code and the description must be within the {@link #isLegal(String)
101         * legal} character range.
102         *
103         * @param code        The error code, {@code null} if not specified.
104         * @param description The error description, {@code null} if not
105         *                    specified.
106         */
107        public ErrorObject(final String code, final String description) {
108        
109                this(code, description, 0, null);
110        }
111
112
113        /**
114         * Creates a new error with the specified code, description and HTTP 
115         * status code. The code and the description must be within the
116         * {@link #isLegal(String) legal} character range.
117         *
118         * @param code           The error code, {@code null} if not specified.
119         * @param description    The error description, {@code null} if not
120         *                       specified.
121         * @param httpStatusCode The HTTP status code, zero if not specified.
122         */
123        public ErrorObject(final String code, final String description, final int httpStatusCode) {
124        
125                this(code, description, httpStatusCode, null);
126        }
127
128
129        /**
130         * Creates a new error with the specified code, description, HTTP 
131         * status code and page URI. The code and the description must be
132         * within the {@link #isLegal(String) legal} character range.
133         *
134         * @param code           The error code, {@code null} if not specified.
135         * @param description    The error description, {@code null} if not
136         *                       specified.
137         * @param httpStatusCode The HTTP status code, zero if not specified.
138         * @param uri            The error page URI, {@code null} if not
139         *                       specified.
140         */
141        public ErrorObject(final String code,
142                           final String description,
143                           final int httpStatusCode,
144                           final URI uri) {
145        
146                if (! isLegal(code)) {
147                        throw new IllegalArgumentException("Illegal char(s) in code, see RFC 6749, section 5.2");
148                }
149                this.code = code;
150                
151                if (! isLegal(description)) {
152                        throw new IllegalArgumentException("Illegal char(s) in description, see RFC 6749, section 5.2");
153                }
154                this.description = description;
155                
156                this.httpStatusCode = httpStatusCode;
157                this.uri = uri;
158        }
159
160
161        /**
162         * Returns the error code.
163         *
164         * @return The error code, {@code null} if not specified.
165         */
166        public String getCode() {
167
168                return code;
169        }
170        
171        
172        /**
173         * Returns the error description.
174         *
175         * @return The error description, {@code null} if not specified.
176         */
177        public String getDescription() {
178        
179                return description;
180        }
181
182
183        /**
184         * Sets the error description.
185         *
186         * @param description The error description, {@code null} if not 
187         *                    specified.
188         *
189         * @return A copy of this error with the specified description.
190         */
191        public ErrorObject setDescription(final String description) {
192
193                return new ErrorObject(getCode(), description, getHTTPStatusCode(), getURI());
194        }
195
196
197        /**
198         * Appends the specified text to the error description.
199         *
200         * @param text The text to append to the error description, 
201         *             {@code null} if not specified.
202         *
203         * @return A copy of this error with the specified appended 
204         *         description.
205         */
206        public ErrorObject appendDescription(final String text) {
207
208                String newDescription;
209
210                if (getDescription() != null)
211                        newDescription = getDescription() + text;
212                else
213                        newDescription = text;
214
215                return new ErrorObject(getCode(), newDescription, getHTTPStatusCode(), getURI());
216        }
217
218
219        /**
220         * Returns the HTTP status code.
221         *
222         * @return The HTTP status code, zero if not specified.
223         */
224        public int getHTTPStatusCode() {
225
226                return httpStatusCode;
227        }
228
229
230        /**
231         * Sets the HTTP status code.
232         *
233         * @param httpStatusCode  The HTTP status code, zero if not specified.
234         *
235         * @return A copy of this error with the specified HTTP status code.
236         */
237        public ErrorObject setHTTPStatusCode(final int httpStatusCode) {
238
239                return new ErrorObject(getCode(), getDescription(), httpStatusCode, getURI());
240        }
241
242
243        /**
244         * Returns the error page URI.
245         *
246         * @return The error page URI, {@code null} if not specified.
247         */
248        public URI getURI() {
249
250                return uri;
251        }
252
253
254        /**
255         * Sets the error page URI.
256         *
257         * @param uri The error page URI, {@code null} if not specified.
258         *
259         * @return A copy of this error with the specified page URI.
260         */
261        public ErrorObject setURI(final URI uri) {
262
263                return new ErrorObject(getCode(), getDescription(), getHTTPStatusCode(), uri);
264        }
265
266
267        /**
268         * Returns a JSON object representation of this error object.
269         *
270         * <p>Example:
271         *
272         * <pre>
273         * {
274         *   "error"             : "invalid_grant",
275         *   "error_description" : "Invalid resource owner credentials"
276         * }
277         * </pre>
278         *
279         * @return The JSON object.
280         */
281        public JSONObject toJSONObject() {
282
283                JSONObject o = new JSONObject();
284
285                if (code != null) {
286                        o.put("error", code);
287                }
288
289                if (description != null) {
290                        o.put("error_description", description);
291                }
292
293                if (uri != null) {
294                        o.put("error_uri", uri.toString());
295                }
296
297                return o;
298        }
299        
300        
301        /**
302         * Returns a parameters representation of this error object. Suitable
303         * for URL-encoded error responses.
304         *
305         * @return The parameters.
306         */
307        public Map<String, List<String>> toParameters() {
308                
309                Map<String,List<String>> params = new HashMap<>();
310                
311                if (getCode() != null) {
312                        params.put("error", Collections.singletonList(getCode()));
313                }
314                
315                if (getDescription() != null) {
316                        params.put("error_description", Collections.singletonList(getDescription()));
317                }
318                
319                if (getURI() != null) {
320                        params.put("error_uri", Collections.singletonList(getURI().toString()));
321                }
322                
323                return params;
324        }
325        
326        
327        /**
328         * Returns an HTTP response for this error object. If no HTTP status
329         * code is specified it will be set to 400 (Bad Request). If an error
330         * code is specified the {@code Content-Type} header will be set to
331         * {@link ContentType#APPLICATION_JSON application/json; charset=UTF-8}
332         * and the error JSON object will be put in the entity body.
333         *
334         * @return The HTTP response.
335         */
336        public HTTPResponse toHTTPResponse() {
337                
338                int statusCode = (getHTTPStatusCode() > 0) ? getHTTPStatusCode() : HTTPResponse.SC_BAD_REQUEST;
339                HTTPResponse httpResponse = new HTTPResponse(statusCode);
340                httpResponse.setCacheControl("no-store");
341                httpResponse.setPragma("no-cache");
342                
343                if (getCode() != null) {
344                        httpResponse.setEntityContentType(ContentType.APPLICATION_JSON);
345                        httpResponse.setContent(toJSONObject().toJSONString());
346                }
347                
348                return httpResponse;
349        }
350
351
352        /**
353         * @see #getCode
354         */
355        @Override
356        public String toString() {
357
358                return code != null ? code : "null";
359        }
360
361
362        @Override
363        public int hashCode() {
364
365                return code != null ? code.hashCode() : "null".hashCode();
366        }
367
368
369        @Override
370        public boolean equals(final Object object) {
371        
372                return object instanceof ErrorObject &&
373                       this.toString().equals(object.toString());
374        }
375
376
377        /**
378         * Parses an error object from the specified JSON object.
379         *
380         * @param jsonObject The JSON object to parse. Must not be
381         *                   {@code null}.
382         *
383         * @return The error object.
384         */
385        public static ErrorObject parse(final JSONObject jsonObject) {
386
387                String code = null;
388                try {
389                        code = JSONObjectUtils.getString(jsonObject, "error", null);
390                } catch (ParseException e) {
391                        // ignore and continue
392                }
393                
394                if (! isLegal(code)) {
395                        code = null;
396                }
397                
398                String description = null;
399                try {
400                        description = JSONObjectUtils.getString(jsonObject, "error_description", null);
401                } catch (ParseException e) {
402                        // ignore and continue
403                }
404                
405                if (! isLegal(description)) {
406                        description = null;
407                }
408                
409                URI uri = null;
410                try {
411                        uri = JSONObjectUtils.getURI(jsonObject, "error_uri", null);
412                } catch (ParseException e) {
413                        // ignore and continue
414                }
415
416                return new ErrorObject(code, description, 0, uri);
417        }
418        
419        
420        /**
421         * Parses an error object from the specified parameters representation.
422         * Suitable for URL-encoded error responses.
423         *
424         * @param params The parameters. Must not be {@code null}.
425         *
426         * @return The error object.
427         */
428        public static ErrorObject parse(final Map<String, List<String>> params) {
429                
430                String code = MultivaluedMapUtils.getFirstValue(params, "error");
431                String description = MultivaluedMapUtils.getFirstValue(params, "error_description");
432                String uriString = MultivaluedMapUtils.getFirstValue(params, "error_uri");
433                
434                URI uri = null;
435                if (uriString != null) {
436                        try {
437                                uri = new URI(uriString);
438                        } catch (URISyntaxException e) {
439                                // ignore
440                        }
441                }
442                
443                if (! isLegal(code)) {
444                        code = null;
445                }
446                
447                if (! isLegal(description)) {
448                        description = null;
449                }
450                
451                return new ErrorObject(code, description, 0, uri);
452        }
453
454
455        /**
456         * Parses an error object from the specified HTTP response.
457         *
458         * @param httpResponse The HTTP response to parse. Must not be
459         *                     {@code null}.
460         *
461         * @return The error object.
462         */
463        public static ErrorObject parse(final HTTPResponse httpResponse) {
464
465                JSONObject jsonObject;
466                try {
467                        jsonObject = httpResponse.getContentAsJSONObject();
468                } catch (ParseException e) {
469                        return new ErrorObject(null, null, httpResponse.getStatusCode());
470                }
471
472                ErrorObject intermediary = parse(jsonObject);
473
474                return new ErrorObject(
475                        intermediary.getCode(),
476                        intermediary.description,
477                        httpResponse.getStatusCode(),
478                        intermediary.getURI());
479        }
480        
481        
482        /**
483         * Returns {@code true} if the characters in the specified string are
484         * within the {@link #isLegal(char)} legal ranges} for OAuth 2.0 error
485         * codes and messages.
486         *
487         * <p>See RFC 6749, section 5.2.
488         *
489         * @param s The string to check. May be be {@code null}.
490         *
491         * @return {@code true} if the string is legal, else {@code false}.
492         */
493        public static boolean isLegal(final String s) {
494        
495                if (s == null) {
496                        return true;
497                }
498                
499                for (char c: s.toCharArray()) {
500                        if (! isLegal(c)) {
501                                return false;
502                        }
503                }
504                
505                return true;
506        }
507        
508        
509        /**
510         * Returns {@code true} if the specified char is within the legal
511         * ranges [0x20, 0x21] | [0x23 - 0x5B] | [0x5D - 0x7E] for OAuth 2.0
512         * error codes and messages.
513         *
514         * <p>See RFC 6749, section 5.2.
515         *
516         * @param c The character to check. Must not be {@code null}.
517         *
518         * @return {@code true} if the character is legal, else {@code false}.
519         */
520        public static boolean isLegal(final char c) {
521                
522                // https://tools.ietf.org/html/rfc6749#section-5.2
523                //
524                // Values for the "error" parameter MUST NOT include characters outside the
525                // set %x20-21 / %x23-5B / %x5D-7E.
526                //
527                // Values for the "error_description" parameter MUST NOT include characters
528                // outside the set %x20-21 / %x23-5B / %x5D-7E.
529                
530                if (c > 0x7f) {
531                        // Not ASCII
532                        return false;
533                }
534                
535                return c >= 0x20 && c <= 0x21 || c >= 0x23 && c <=0x5b || c >= 0x5d && c <= 0x7e;
536        }
537}