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