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' 500 * representation. 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}