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}