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