001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014-2015 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.filter.oauth2.client; 018 019import static org.forgerock.http.header.HeaderUtil.parseParameters; 020import static org.forgerock.http.header.HeaderUtil.quote; 021import static org.forgerock.http.header.HeaderUtil.split; 022import static org.forgerock.util.Utils.joinAsString; 023 024import java.util.Arrays; 025import java.util.Collections; 026import java.util.LinkedHashMap; 027import java.util.List; 028import java.util.Map; 029 030import org.forgerock.http.protocol.Form; 031import org.forgerock.http.protocol.Status; 032import org.forgerock.json.JsonValue; 033import org.forgerock.json.JsonValueException; 034import org.forgerock.util.Reject; 035 036/** 037 * Describes an error which occurred during an OAuth 2.0 authorization request 038 * or when performing an authorized request. More specifically, errors are 039 * communicated: 040 * <ul> 041 * <li>as query parameters in a failed authorization call-back. These errors are 042 * defined in RFC 6749 # 4.1.2 and comprise of an error code, optional error 043 * description, and optional error URI 044 * <li>as JSON encoded content in a failed access token request or failed 045 * refresh token request. These errors are defined in RFC 6749 # 5.2 and 046 * comprise of an error code, optional error description, and optional error URI 047 * <li>using the {@code WWW-Authenticate} response header in response to a 048 * failed attempt to access an OAuth 2.0 protected resource on a resource 049 * server. These errors are defined in RFC 6750 # 3.1 and comprise of an 050 * optional error code, optional error description, optional error URI, optional 051 * list of required scopes, and optional realm. 052 * </ul> 053 * 054 * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749 # 055 * 4.1.2 - The OAuth 2.0 Authorization Framework</a> 056 * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 # 5.2 057 * - The OAuth 2.0 Authorization Framework</a> 058 * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 - The 059 * OAuth 2.0 Authorization Framework: Bearer Token Usage</a> 060 */ 061public final class OAuth2Error { 062 /** 063 * The resource owner or authorization server denied the request. 064 * 065 * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749 066 * # 4.1.2 - The OAuth 2.0 Authorization Framework</a> 067 */ 068 public static final String E_ACCESS_DENIED = "access_denied"; 069 070 /** 071 * The request requires higher privileges than provided by the access token. 072 * The resource server SHOULD respond with the HTTP 403 (Forbidden) status 073 * code and MAY include the "scope" attribute with the scope necessary to 074 * access the protected resource. 075 * 076 * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 - 077 * The OAuth 2.0 Authorization Framework: Bearer Token Usage</a> 078 */ 079 public static final String E_INSUFFICIENT_SCOPE = "insufficient_scope"; 080 081 /** 082 * Client authentication failed (e.g., unknown client, no client 083 * authentication included, or unsupported authentication method). The 084 * authorization server MAY return an HTTP 401 (Unauthorized) status code to 085 * indicate which HTTP authentication schemes are supported. If the client 086 * attempted to authenticate via the "Authorization" request header field, 087 * the authorization server MUST respond with an HTTP 401 (Unauthorized) 088 * status code and include the "WWW-Authenticate" response header field 089 * matching the authentication scheme used by the client. 090 * 091 * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 # 092 * 5.2 - The OAuth 2.0 Authorization Framework</a> 093 */ 094 public static final String E_INVALID_CLIENT = "invalid_client"; 095 096 /** 097 * The provided authorization grant (e.g., authorization code, resource 098 * owner credentials) or refresh token is invalid, expired, revoked, does 099 * not match the redirection URI used in the authorization request, or was 100 * issued to another client. 101 * 102 * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 # 103 * 5.2 - The OAuth 2.0 Authorization Framework</a> 104 */ 105 public static final String E_INVALID_GRANT = "invalid_grant"; 106 107 /** 108 * The request is missing a required parameter, includes an unsupported 109 * parameter value (other than grant type), repeats a parameter, includes 110 * multiple credentials, utilizes more than one mechanism for authenticating 111 * the client, or is otherwise malformed. The resource server SHOULD respond 112 * with the HTTP 400 (Bad Request) status code. 113 * 114 * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749 115 * # 4.1.2 - The OAuth 2.0 Authorization Framework</a> 116 * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 # 117 * 5.2 - The OAuth 2.0 Authorization Framework</a> 118 * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 - 119 * The OAuth 2.0 Authorization Framework: Bearer Token Usage</a> 120 */ 121 public static final String E_INVALID_REQUEST = "invalid_request"; 122 123 /** 124 * The requested scope is invalid, unknown, malformed, or exceeds the scope 125 * granted by the resource owner. 126 * 127 * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749 128 * # 4.1.2 - The OAuth 2.0 Authorization Framework</a> 129 * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 # 130 * 5.2 - The OAuth 2.0 Authorization Framework</a> 131 */ 132 public static final String E_INVALID_SCOPE = "invalid_scope"; 133 134 /** 135 * The access token provided is expired, revoked, malformed, or invalid for 136 * other reasons. The resource SHOULD respond with the HTTP 401 137 * (Unauthorized) status code. The client MAY request a new access token and 138 * retry the protected resource request. 139 * 140 * @see <a href="http://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 - 141 * The OAuth 2.0 Authorization Framework: Bearer Token Usage</a> 142 */ 143 public static final String E_INVALID_TOKEN = "invalid_token"; 144 145 /** 146 * The authorization server encountered an unexpected condition that 147 * prevented it from fulfilling the request. (This error code is needed 148 * because a 500 Internal Server Error HTTP status code cannot be returned 149 * to the client via an HTTP redirect.) 150 * 151 * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749 152 * # 4.1.2 - The OAuth 2.0 Authorization Framework</a> 153 */ 154 public static final String E_SERVER_ERROR = "server_error"; 155 156 /** 157 * The authorization server is currently unable to handle the request due to 158 * a temporary overloading or maintenance of the server. (This error code is 159 * needed because a 503 Service Unavailable HTTP status code cannot be 160 * returned to the client via an HTTP redirect.) 161 * 162 * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749 163 * # 4.1.2 - The OAuth 2.0 Authorization Framework</a> 164 */ 165 public static final String E_TEMPORARILY_UNAVAILABLE = "temporarily_unavailable"; 166 167 /** 168 * The authenticated client is not authorized to use this authorization 169 * grant type. 170 * 171 * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749 172 * # 4.1.2 - The OAuth 2.0 Authorization Framework</a> 173 * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 # 174 * 5.2 - The OAuth 2.0 Authorization Framework</a> 175 */ 176 public static final String E_UNAUTHORIZED_CLIENT = "unauthorized_client"; 177 178 /** 179 * The authorization grant type is not supported by the authorization 180 * server. 181 * 182 * @see <a href="http://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749 # 183 * 5.2 - The OAuth 2.0 Authorization Framework</a> 184 */ 185 public static final String E_UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type"; 186 187 /** 188 * The authorization server does not support obtaining an authorization code 189 * using this method. 190 * 191 * @see <a href="http://tools.ietf.org/html/rfc6749#section-4.1.2">RFC 6749 192 * # 4.1.2 - The OAuth 2.0 Authorization Framework</a> 193 */ 194 public static final String E_UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type"; 195 196 /** 197 * The name of the field which communicates the error code. 198 */ 199 public static final String F_ERROR = "error"; 200 201 /** 202 * The name of the field which communicates the error description. 203 */ 204 public static final String F_ERROR_DESCRIPTION = "error_description"; 205 206 /** 207 * The name of the field which communicates the error uri. 208 */ 209 public static final String F_ERROR_URI = "error_uri"; 210 211 /** 212 * The name of the field which communicates the realm. 213 */ 214 public static final String F_REALM = "realm"; 215 216 /** 217 * The name of the field which communicates the scope. 218 */ 219 public static final String F_SCOPE = "scope"; 220 221 /** 222 * The WWW-Authenticate header prefix, 'Bearer'. 223 */ 224 public static final String H_BEARER = "Bearer"; 225 226 /** 227 * Singleton instance used for empty WWW-Authenticate headers. 228 */ 229 private static final OAuth2Error EMPTY = new OAuth2Error(null, null, null, null, null); 230 231 /** 232 * The WWW-Authenticate header prefix including trailing space separator. 233 */ 234 private static final String H_BEARER_WITH_SPACE = H_BEARER + " "; 235 236 /** 237 * Returns an OAuth 2.0 resource server error whose values are determined on 238 * a best-effort basis from the provided incomplete error and HTTP status 239 * code. 240 * 241 * @param status 242 * The HTTP status code. 243 * @param incomplete 244 * The incomplete and possibly {@code null} error. 245 * @return A non-{@code null} error whose error code has been determined 246 * from the HTTP status code. 247 */ 248 public static OAuth2Error bestEffortResourceServerError(final Status status, 249 final OAuth2Error incomplete) { 250 if (incomplete != null && incomplete.error != null) { 251 // Seems ok. 252 return incomplete; 253 } 254 final String error = mapStatusToError(status); 255 if (incomplete == null) { 256 return new OAuth2Error(null, null, error, null, null); 257 } else { 258 return new OAuth2Error(incomplete.getRealm(), 259 incomplete.getScope(), 260 error, 261 incomplete.getErrorDescription(), 262 incomplete.getErrorUri()); 263 } 264 } 265 266 private static String mapStatusToError(final Status status) { 267 if (Status.BAD_REQUEST.equals(status)) { 268 return E_INVALID_REQUEST; 269 } else if (Status.UNAUTHORIZED.equals(status)) { 270 return E_INVALID_TOKEN; 271 } else if (Status.FORBIDDEN.equals(status)) { 272 return E_INVALID_SCOPE; 273 } else if (Status.METHOD_NOT_ALLOWED.equals(status)) { 274 return E_INVALID_REQUEST; 275 } else if (Status.INTERNAL_SERVER_ERROR.equals(status)) { 276 return E_SERVER_ERROR; 277 } else if (Status.SERVICE_UNAVAILABLE.equals(status)) { 278 return E_TEMPORARILY_UNAVAILABLE; 279 } else { 280 return E_SERVER_ERROR; // No idea 281 } 282 } 283 284 /** 285 * Returns an OAuth 2.0 error suitable for inclusion in authorization 286 * call-back responses and access token and refresh token responses. 287 * 288 * @param error 289 * The error code specifying the cause of the failure. 290 * @param errorDescription 291 * The human-readable ASCII text providing additional 292 * information, or {@code null}. 293 * @return The OAuth 2.0 error. 294 * @throws NullPointerException 295 * If {@code error} was {@code null}. 296 */ 297 public static OAuth2Error newAuthorizationServerError(final String error, 298 final String errorDescription) { 299 Reject.ifNull(error); 300 return new OAuth2Error(null, null, error, errorDescription, null); 301 } 302 303 /** 304 * Returns an OAuth 2.0 error suitable for inclusion in authorization 305 * call-back responses and access token and refresh token responses. 306 * 307 * @param error 308 * The error code specifying the cause of the failure. 309 * @param errorDescription 310 * The human-readable ASCII text providing additional 311 * information, or {@code null}. 312 * @param errorUri 313 * A URI identifying a human-readable web page with information 314 * about the error, or {@code null}. 315 * @return The OAuth 2.0 error. 316 * @throws NullPointerException 317 * If {@code error} was {@code null}. 318 */ 319 public static OAuth2Error newAuthorizationServerError(final String error, 320 final String errorDescription, final String errorUri) { 321 Reject.ifNull(error); 322 return new OAuth2Error(null, null, error, errorDescription, errorUri); 323 } 324 325 /** 326 * Returns an OAuth 2.0 error suitable for inclusion in resource server 327 * WWW-Authenticate response headers. 328 * 329 * @param realm 330 * The scope of protection required to access the protected 331 * resource, or {@code null}. 332 * @param scope 333 * The required scope(s) of the access token for accessing the 334 * requested resource, or {@code null}. 335 * @param error 336 * The error code specifying the cause of the failure, or 337 * {@code null}. 338 * @param errorDescription 339 * The human-readable ASCII text providing additional 340 * information, or {@code null}. 341 * @param errorUri 342 * A URI identifying a human-readable web page with information 343 * about the error, or {@code null}. 344 * @return The OAuth 2.0 error. 345 */ 346 public static OAuth2Error newResourceServerError(final String realm, final List<String> scope, 347 final String error, final String errorDescription, final String errorUri) { 348 return new OAuth2Error(realm, scope, error, errorDescription, errorUri); 349 } 350 351 /** 352 * Parses the provided {@link #toString()} representation as an OAuth 2.0 353 * error. 354 * 355 * @param s 356 * The string to parse. 357 * @return The parsed OAuth 2.0 error. 358 */ 359 public static OAuth2Error valueOf(final String s) { 360 final List<String> attributes = split(s, ','); 361 final Map<String, String> map = parseParameters(attributes); 362 final String realm = map.get("realm"); 363 final String scopeString = map.get("scope"); 364 final List<String> scopes = 365 scopeString != null ? Arrays.asList(scopeString.trim().split("\\s+")) : null; 366 final String error = map.get("error"); 367 final String errorDescription = map.get("error_description"); 368 final String errorUri = map.get("error_uri"); 369 return new OAuth2Error(realm, scopes, error, errorDescription, errorUri); 370 } 371 372 /** 373 * Parses the Form representation of an authorization call-back error as an 374 * OAuth 2.0 error. Only the error, error description, and error URI fields 375 * will be included. 376 * 377 * @param form 378 * The Form representation of an authorization call-back error. 379 * @return The parsed OAuth 2.0 error. 380 */ 381 public static OAuth2Error valueOfForm(final Form form) { 382 return new OAuth2Error(null, null, form.getFirst(F_ERROR), form 383 .getFirst(F_ERROR_DESCRIPTION), form.getFirst(F_ERROR_URI)); 384 } 385 386 /** 387 * Parses the JSON representation of an access token error response as an 388 * OAuth 2.0 error. Only the error, error description, and error URI fields 389 * will be included. 390 * 391 * @param json 392 * The JSON representation of an access token error response. 393 * @return The parsed OAuth 2.0 error. 394 * @throws IllegalArgumentException 395 * If the JSON content was malformed. 396 */ 397 public static OAuth2Error valueOfJsonContent(final Map<String, Object> json) { 398 final JsonValue jv = new JsonValue(json); 399 try { 400 return new OAuth2Error(null, null, jv.get(F_ERROR).asString(), jv.get( 401 F_ERROR_DESCRIPTION).asString(), jv.get(F_ERROR_URI).asString()); 402 } catch (final JsonValueException e) { 403 throw new IllegalArgumentException(e); 404 } 405 } 406 407 /** 408 * Parses the provided WWW-Authenticate header content as an OAuth 2.0 409 * error. 410 * 411 * @param s 412 * The string containing the WWW-Authenticate header content. 413 * @return The parsed OAuth 2.0 error. 414 * @throws IllegalArgumentException 415 * If the header value was malformed. 416 */ 417 public static OAuth2Error valueOfWWWAuthenticateHeader(final String s) { 418 if (H_BEARER.equals(s)) { 419 return EMPTY; 420 } else if (s.startsWith(H_BEARER_WITH_SPACE)) { 421 return valueOf(s.substring(H_BEARER_WITH_SPACE.length())); 422 } else { 423 throw new IllegalArgumentException("Malformed WWW-Authenticate header '" + s + "'"); 424 } 425 } 426 427 private final String error; 428 private final String errorDescription; 429 private final String errorUri; 430 private final String realm; 431 private final List<String> scope; 432 private transient String stringValue; 433 434 private OAuth2Error(final String realm, final List<String> scope, final String error, 435 final String errorDescription, final String errorUri) { 436 this.realm = realm; 437 this.scope = 438 scope != null ? Collections.unmodifiableList(scope) : Collections 439 .<String> emptyList(); 440 this.error = error; 441 this.errorDescription = errorDescription; 442 this.errorUri = errorUri; 443 } 444 445 @Override 446 public boolean equals(final Object obj) { 447 if (this == obj) { 448 return true; 449 } else if (obj instanceof OAuth2Error) { 450 return toString().equals(obj.toString()); 451 } else { 452 return false; 453 } 454 } 455 456 /** 457 * Returns the error code specifying the cause of the failure. 458 * 459 * @return The error code specifying the cause of the failure, or 460 * {@code null} if no error code was provided (which may be the case 461 * for WWW-Authenticate headers). 462 */ 463 public String getError() { 464 return error; 465 } 466 467 /** 468 * Returns the human-readable ASCII text providing additional information, 469 * used to assist the client developer in understanding the error that 470 * occurred. 471 * 472 * @return The human-readable ASCII text providing additional information, 473 * or {@code null} if no description was provided. 474 */ 475 public String getErrorDescription() { 476 return errorDescription; 477 } 478 479 /** 480 * Returns a URI identifying a human-readable web page with information 481 * about the error, used to provide the client developer with additional 482 * information about the error. 483 * 484 * @return A URI identifying a human-readable web page with information 485 * about the error, or {@code null} if no error URI was provided. 486 */ 487 public String getErrorUri() { 488 return errorUri; 489 } 490 491 /** 492 * Returns the scope of protection required to access the protected 493 * resource. The realm is only included with {@code WWW-Authenticate} 494 * headers in response to a failure to access a protected resource. 495 * 496 * @return The scope of protection required to access the protected 497 * resource, or {@code null} if no realm was provided (which will 498 * always be the case for authorization call-back failures and 499 * access/refresh token requests). 500 */ 501 public String getRealm() { 502 return realm; 503 } 504 505 /** 506 * Returns the required scope of the access token for accessing the 507 * requested resource. The scope is only included with 508 * {@code WWW-Authenticate} headers in response to a failure to access a 509 * protected resource. 510 * 511 * @return The required scope of the access token for accessing the 512 * requested resource, which may be empty (never {@code null}) if no 513 * scope was provided (which will always be the case for 514 * authorization call-back failures and access/refresh token 515 * requests). 516 */ 517 public List<String> getScope() { 518 return scope; 519 } 520 521 @Override 522 public int hashCode() { 523 return toString().hashCode(); 524 } 525 526 /** 527 * Returns {@code true} if this error includes an error code and it matches 528 * the provided error code. 529 * 530 * @param error 531 * The error code. 532 * @return {@code true} if this error includes an error code and it matches 533 * the provided error code. 534 */ 535 public boolean is(final String error) { 536 return error.equalsIgnoreCase(this.error); 537 } 538 539 /** 540 * Returns the form representation of this error suitable for inclusion in 541 * an authorization call-back query. Only the error, error description, and 542 * error URI fields will be included. 543 * 544 * @return The form representation of this error suitable for inclusion in 545 * an authorization call-back query. 546 */ 547 public Form toForm() { 548 final Form form = new Form(); 549 if (error != null) { 550 form.add(F_ERROR, error); 551 } 552 if (errorDescription != null) { 553 form.add(F_ERROR_DESCRIPTION, errorDescription); 554 } 555 if (errorUri != null) { 556 form.add(F_ERROR_URI, errorUri); 557 } 558 return form; 559 } 560 561 /** 562 * Returns the JSON representation of this error formatted as an access 563 * token error response. Only the error, error description, and error URI 564 * fields will be included. 565 * 566 * @return The JSON representation of this error formatted as an access 567 * token error response. 568 */ 569 public Map<String, Object> toJsonContent() { 570 final Map<String, Object> json = new LinkedHashMap<>(3); 571 if (error != null) { 572 json.put(F_ERROR, error); 573 } 574 if (errorDescription != null) { 575 json.put(F_ERROR_DESCRIPTION, errorDescription); 576 } 577 if (errorUri != null) { 578 json.put(F_ERROR_URI, errorUri); 579 } 580 return json; 581 } 582 583 @Override 584 public String toString() { 585 // Use lazy initialization: minor race conditions don't matter. 586 if (stringValue == null) { 587 final StringBuilder builder = new StringBuilder(); 588 appendAttribute(builder, F_REALM, realm); 589 appendAttribute(builder, F_SCOPE, scope.isEmpty() ? null : joinAsString(" ", scope)); 590 appendAttribute(builder, F_ERROR, error); 591 appendAttribute(builder, F_ERROR_DESCRIPTION, errorDescription); 592 appendAttribute(builder, F_ERROR_URI, errorUri); 593 stringValue = builder.toString(); 594 } 595 return stringValue; 596 } 597 598 /** 599 * Returns the string representation of this error formatted as a 600 * {@code WWW-Authenticate} header. 601 * 602 * @return The string representation of this error formatted as a 603 * {@code WWW-Authenticate} header. 604 */ 605 public String toWWWAuthenticateHeader() { 606 final String stringValue = toString(); 607 return stringValue.isEmpty() ? H_BEARER : H_BEARER_WITH_SPACE + stringValue; 608 } 609 610 private void addSeparator(final StringBuilder builder) { 611 if (builder.length() > 0) { 612 builder.append(", "); 613 } 614 } 615 616 private void appendAttribute(final StringBuilder builder, final String key, final String value) { 617 if (value != null) { 618 addSeparator(builder); 619 builder.append(key).append('=').append(quote(value)); 620 } 621 } 622}