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}