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 if (! isLegal(description)) { 476 description = null; 477 } 478 479 URI uri = null; 480 try { 481 uri = JSONObjectUtils.getURI(jsonObject, "error_uri", null); 482 } catch (ParseException e) { 483 // ignore and continue 484 } 485 486 Map<String, String> customParams = null; 487 for (Map.Entry<String, Object> en: jsonObject.entrySet()) { 488 if (!"error".equals(en.getKey()) && !"error_description".equals(en.getKey()) && !"error_uri".equals(en.getKey())) { 489 if (en.getValue() == null || en.getValue() instanceof String) { 490 if (customParams == null) { 491 customParams = new HashMap<>(); 492 } 493 customParams.put(en.getKey(), (String)en.getValue()); 494 } 495 } 496 } 497 498 return new ErrorObject(code, description, 0, uri, customParams); 499 } 500 501 502 /** 503 * Parses an error object from the specified parameters representation. 504 * Suitable for URL-encoded error responses. 505 * 506 * @param params The parameters. Must not be {@code null}. 507 * 508 * @return The error object. 509 */ 510 public static ErrorObject parse(final Map<String, List<String>> params) { 511 512 String code = MultivaluedMapUtils.getFirstValue(params, "error"); 513 String description = MultivaluedMapUtils.getFirstValue(params, "error_description"); 514 String uriString = MultivaluedMapUtils.getFirstValue(params, "error_uri"); 515 516 if (! isLegal(code)) { 517 code = null; 518 } 519 520 if (! isLegal(description)) { 521 description = null; 522 } 523 524 URI uri = null; 525 if (uriString != null) { 526 try { 527 uri = new URI(uriString); 528 } catch (URISyntaxException e) { 529 // ignore 530 } 531 } 532 533 Map<String, String> customParams = null; 534 for (Map.Entry<String, List<String>> en: params.entrySet()) { 535 if (!"error".equals(en.getKey()) && !"error_description".equals(en.getKey()) && !"error_uri".equals(en.getKey())) { 536 537 if (customParams == null) { 538 customParams = new HashMap<>(); 539 } 540 541 if (en.getValue() == null) { 542 customParams.put(en.getKey(), null); 543 } else if (! en.getValue().isEmpty()) { 544 customParams.put(en.getKey(), en.getValue().get(0)); 545 } 546 } 547 } 548 549 return new ErrorObject(code, description, 0, uri, customParams); 550 } 551 552 553 /** 554 * Parses an error object from the specified HTTP response. 555 * 556 * @param httpResponse The HTTP response to parse. Must not be 557 * {@code null}. 558 * 559 * @return The error object. 560 */ 561 public static ErrorObject parse(final HTTPResponse httpResponse) { 562 563 JSONObject jsonObject; 564 try { 565 jsonObject = httpResponse.getContentAsJSONObject(); 566 } catch (ParseException e) { 567 return new ErrorObject(null, null, httpResponse.getStatusCode()); 568 } 569 570 ErrorObject intermediary = parse(jsonObject); 571 572 return new ErrorObject( 573 intermediary.getCode(), 574 intermediary.description, 575 httpResponse.getStatusCode(), 576 intermediary.getURI(), 577 intermediary.getCustomParams()); 578 } 579 580 581 /** 582 * Returns {@code true} if the characters in the specified string are 583 * within the {@link #isLegal(char)} legal ranges} for OAuth 2.0 error 584 * codes and messages. 585 * 586 * <p>See RFC 6749, section 5.2. 587 * 588 * @param s The string to check. May be be {@code null}. 589 * 590 * @return {@code true} if the string is legal, else {@code false}. 591 */ 592 public static boolean isLegal(final String s) { 593 594 if (s == null) { 595 return true; 596 } 597 598 for (char c: s.toCharArray()) { 599 if (! isLegal(c)) { 600 return false; 601 } 602 } 603 604 return true; 605 } 606 607 608 /** 609 * Returns {@code true} if the specified char is within the legal 610 * ranges [0x20, 0x21] | [0x23 - 0x5B] | [0x5D - 0x7E] for OAuth 2.0 611 * error codes and messages. 612 * 613 * <p>See RFC 6749, section 5.2. 614 * 615 * @param c The character to check. Must not be {@code null}. 616 * 617 * @return {@code true} if the character is legal, else {@code false}. 618 */ 619 public static boolean isLegal(final char c) { 620 621 // https://tools.ietf.org/html/rfc6749#section-5.2 622 // 623 // Values for the "error" parameter MUST NOT include characters outside the 624 // set %x20-21 / %x23-5B / %x5D-7E. 625 // 626 // Values for the "error_description" parameter MUST NOT include characters 627 // outside the set %x20-21 / %x23-5B / %x5D-7E. 628 629 if (c > 0x7f) { 630 // Not ASCII 631 return false; 632 } 633 634 return c >= 0x20 && c <= 0x21 || c >= 0x23 && c <=0x5b || c >= 0x5d && c <= 0x7e; 635 } 636}