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.token; 019 020 021import java.net.URI; 022import java.net.URISyntaxException; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026import net.jcip.annotations.Immutable; 027 028import com.nimbusds.oauth2.sdk.ErrorObject; 029import com.nimbusds.oauth2.sdk.ParseException; 030import com.nimbusds.oauth2.sdk.Scope; 031import com.nimbusds.oauth2.sdk.http.HTTPResponse; 032 033 034/** 035 * OAuth 2.0 bearer token error. Used to indicate that access to a resource 036 * protected by a Bearer access token is denied, due to the request or token 037 * being invalid, or due to the access token having insufficient scope. 038 * 039 * <p>Standard bearer access token errors: 040 * 041 * <ul> 042 * <li>{@link #MISSING_TOKEN} 043 * <li>{@link #INVALID_REQUEST} 044 * <li>{@link #INVALID_TOKEN} 045 * <li>{@link #INSUFFICIENT_SCOPE} 046 * </ul> 047 * 048 * <p>Example HTTP response: 049 * 050 * <pre> 051 * HTTP/1.1 401 Unauthorized 052 * WWW-Authenticate: Bearer realm="example.com", 053 * error="invalid_token", 054 * error_description="The access token expired" 055 * </pre> 056 * 057 * <p>Related specifications: 058 * 059 * <ul> 060 * <li>OAuth 2.0 Bearer Token Usage (RFC 6750), section 3.1. 061 * <li>Hypertext Transfer Protocol (HTTP/1.1): Authentication (RFC 7235), 062 * section 4.1. 063 * </ul> 064 */ 065@Immutable 066public class BearerTokenError extends ErrorObject { 067 068 069 /** 070 * The request does not contain an access token. No error code or 071 * description is specified for this error, just the HTTP status code 072 * is set to 401 (Unauthorized). 073 * 074 * <p>Example: 075 * 076 * <pre> 077 * HTTP/1.1 401 Unauthorized 078 * WWW-Authenticate: Bearer 079 * </pre> 080 */ 081 public static final BearerTokenError MISSING_TOKEN = 082 new BearerTokenError(null, null, HTTPResponse.SC_UNAUTHORIZED); 083 084 /** 085 * The request is missing a required parameter, includes an unsupported 086 * parameter or parameter value, repeats the same parameter, uses more 087 * than one method for including an access token, or is otherwise 088 * malformed. The HTTP status code is set to 400 (Bad Request). 089 */ 090 public static final BearerTokenError INVALID_REQUEST = 091 new BearerTokenError("invalid_request", "Invalid request", 092 HTTPResponse.SC_BAD_REQUEST); 093 094 095 /** 096 * The access token provided is expired, revoked, malformed, or invalid 097 * for other reasons. The HTTP status code is set to 401 098 * (Unauthorized). 099 */ 100 public static final BearerTokenError INVALID_TOKEN = 101 new BearerTokenError("invalid_token", "Invalid access token", 102 HTTPResponse.SC_UNAUTHORIZED); 103 104 105 /** 106 * The request requires higher privileges than provided by the access 107 * token. The HTTP status code is set to 403 (Forbidden). 108 */ 109 public static final BearerTokenError INSUFFICIENT_SCOPE = 110 new BearerTokenError("insufficient_scope", "Insufficient scope", 111 HTTPResponse.SC_FORBIDDEN); 112 113 114 /** 115 * Returns {@code true} if the specified error code consists of valid 116 * characters. Values for the "error" and "error_description" 117 * attributes must not include characters outside the [0x20, 0x21] | 118 * [0x23 - 0x5B] | [0x5D - 0x7E] range. See RFC 6750, section 3. 119 * 120 * @see ErrorObject#isLegal(String) 121 * 122 * @param errorCode The error code string. 123 * 124 * @return {@code true} if the error code string contains valid 125 * characters, else {@code false}. 126 */ 127 @Deprecated 128 public static boolean isCodeWithValidChars(final String errorCode) { 129 130 return ErrorObject.isLegal(errorCode); 131 } 132 133 134 /** 135 * Returns {@code true} if the specified error description consists of 136 * valid characters. Values for the "error" and "error_description" 137 * attributes must not include characters outside the [0x20, 0x21] | 138 * [0x23 - 0x5B] | [0x5D - 0x7E] range. See RFC 6750, section 3. 139 * 140 * @see ErrorObject#isLegal(String) 141 * 142 * @param errorDescription The error description string. 143 * 144 * @return {@code true} if the error description string contains valid 145 * characters, else {@code false}. 146 */ 147 @Deprecated 148 public static boolean isDescriptionWithValidChars(final String errorDescription) { 149 150 return ErrorObject.isLegal(errorDescription); 151 } 152 153 154 /** 155 * Returns {@code true} if the specified scope consists of valid 156 * characters. Values for the "scope" attributes must not include 157 * characters outside the [0x20, 0x21] | [0x23 - 0x5B] | [0x5D - 0x7E] 158 * range. See RFC 6750, section 3. 159 * 160 * @see ErrorObject#isLegal(String) 161 * 162 * @param scope The scope. 163 * 164 * @return {@code true} if the scope contains valid characters, else 165 * {@code false}. 166 */ 167 public static boolean isScopeWithValidChars(final Scope scope) { 168 169 170 return ErrorObject.isLegal(scope.toString()); 171 } 172 173 174 /** 175 * Regex pattern for matching the realm parameter of a WWW-Authenticate 176 * header. Limits the realm string length to 256 chars to prevent 177 * potential stack overflow exception for very long strings due to 178 * recursive nature of regex. 179 */ 180 private static final Pattern realmPattern = Pattern.compile("realm=\"(([^\\\\\"]|\\\\.){0,256})\""); 181 182 183 /** 184 * Regex pattern for matching the error parameter of a WWW-Authenticate 185 * header. Double quoting is optional. 186 */ 187 private static final Pattern errorPattern = Pattern.compile("error=(\"([\\w\\_-]+)\"|([\\w\\_-]+))"); 188 189 190 /** 191 * Regex pattern for matching the error description parameter of a 192 * WWW-Authenticate header. 193 */ 194 private static final Pattern errorDescriptionPattern = Pattern.compile("error_description=\"([^\"]+)\""); 195 196 197 /** 198 * Regex pattern for matching the error URI parameter of a 199 * WWW-Authenticate header. 200 */ 201 private static final Pattern errorURIPattern = Pattern.compile("error_uri=\"([^\"]+)\""); 202 203 204 /** 205 * Regex pattern for matching the scope parameter of a WWW-Authenticate 206 * header. 207 */ 208 private static final Pattern scopePattern = Pattern.compile("scope=\"([^\"]+)"); 209 210 211 /** 212 * The realm, {@code null} if not specified. 213 */ 214 private final String realm; 215 216 217 /** 218 * Required scope, {@code null} if not specified. 219 */ 220 private final Scope scope; 221 222 223 /** 224 * Creates a new OAuth 2.0 bearer token error with the specified code 225 * and description. 226 * 227 * @param code The error code, {@code null} if not specified. 228 * @param description The error description, {@code null} if not 229 * specified. 230 */ 231 public BearerTokenError(final String code, final String description) { 232 233 this(code, description, 0, null, null, null); 234 } 235 236 237 /** 238 * Creates a new OAuth 2.0 bearer token error with the specified code, 239 * description and HTTP status code. 240 * 241 * @param code The error code, {@code null} if not specified. 242 * @param description The error description, {@code null} if not 243 * specified. 244 * @param httpStatusCode The HTTP status code, zero if not specified. 245 */ 246 public BearerTokenError(final String code, final String description, final int httpStatusCode) { 247 248 this(code, description, httpStatusCode, null, null, null); 249 } 250 251 252 /** 253 * Creates a new OAuth 2.0 bearer token error with the specified code, 254 * description, HTTP status code, page URI, realm and scope. 255 * 256 * @param code The error code, {@code null} if not specified. 257 * @param description The error description, {@code null} if not 258 * specified. 259 * @param httpStatusCode The HTTP status code, zero if not specified. 260 * @param uri The error page URI, {@code null} if not 261 * specified. 262 * @param realm The realm, {@code null} if not specified. 263 * @param scope The required scope, {@code null} if not 264 * specified. 265 */ 266 public BearerTokenError(final String code, 267 final String description, 268 final int httpStatusCode, 269 final URI uri, 270 final String realm, 271 final Scope scope) { 272 273 super(code, description, httpStatusCode, uri); 274 this.realm = realm; 275 this.scope = scope; 276 277 if (scope != null && ! isScopeWithValidChars(scope)) { 278 throw new IllegalArgumentException("The scope contains illegal characters, see RFC 6750, section 3"); 279 } 280 } 281 282 283 @Override 284 public BearerTokenError setDescription(final String description) { 285 286 return new BearerTokenError(super.getCode(), description, super.getHTTPStatusCode(), super.getURI(), realm, scope); 287 } 288 289 290 @Override 291 public BearerTokenError appendDescription(final String text) { 292 293 String newDescription; 294 295 if (getDescription() != null) 296 newDescription = getDescription() + text; 297 else 298 newDescription = text; 299 300 return new BearerTokenError(super.getCode(), newDescription, super.getHTTPStatusCode(), super.getURI(), realm, scope); 301 } 302 303 304 @Override 305 public BearerTokenError setHTTPStatusCode(final int httpStatusCode) { 306 307 return new BearerTokenError(super.getCode(), super.getDescription(), httpStatusCode, super.getURI(), realm, scope); 308 } 309 310 311 @Override 312 public BearerTokenError setURI(final URI uri) { 313 314 return new BearerTokenError(super.getCode(), super.getDescription(), super.getHTTPStatusCode(), uri, realm, scope); 315 } 316 317 318 /** 319 * Gets the realm. 320 * 321 * @return The realm, {@code null} if not specified. 322 */ 323 public String getRealm() { 324 325 return realm; 326 } 327 328 329 /** 330 * Sets the realm. 331 * 332 * @param realm realm, {@code null} if not specified. 333 * 334 * @return A copy of this error with the specified realm. 335 */ 336 public BearerTokenError setRealm(final String realm) { 337 338 return new BearerTokenError(getCode(), 339 getDescription(), 340 getHTTPStatusCode(), 341 getURI(), 342 realm, 343 getScope()); 344 } 345 346 347 /** 348 * Gets the required scope. 349 * 350 * @return The required scope, {@code null} if not specified. 351 */ 352 public Scope getScope() { 353 354 return scope; 355 } 356 357 358 /** 359 * Sets the required scope. 360 * 361 * @param scope The required scope, {@code null} if not specified. 362 * 363 * @return A copy of this error with the specified required scope. 364 */ 365 public BearerTokenError setScope(final Scope scope) { 366 367 return new BearerTokenError(getCode(), 368 getDescription(), 369 getHTTPStatusCode(), 370 getURI(), 371 getRealm(), 372 scope); 373 } 374 375 376 /** 377 * Returns the {@code WWW-Authenticate} HTTP response header code for 378 * this bearer access token error response. 379 * 380 * <p>Example: 381 * 382 * <pre> 383 * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token" 384 * </pre> 385 * 386 * @return The {@code Www-Authenticate} header value. 387 */ 388 public String toWWWAuthenticateHeader() { 389 390 StringBuilder sb = new StringBuilder("Bearer"); 391 392 int numParams = 0; 393 394 // Serialise realm, may contain double quotes 395 if (realm != null) { 396 sb.append(" realm=\""); 397 sb.append(getRealm().replaceAll("\"","\\\\\"")); 398 sb.append('"'); 399 400 numParams++; 401 } 402 403 // Serialise error, error_description, error_uri 404 if (getCode() != null) { 405 406 if (numParams > 0) 407 sb.append(','); 408 409 sb.append(" error=\""); 410 sb.append(getCode()); 411 sb.append('"'); 412 numParams++; 413 414 if (getDescription() != null) { 415 416 if (numParams > 0) 417 sb.append(','); 418 419 sb.append(" error_description=\""); 420 sb.append(getDescription()); 421 sb.append('"'); 422 numParams++; 423 } 424 425 if (getURI() != null) { 426 427 if (numParams > 0) 428 sb.append(','); 429 430 sb.append(" error_uri=\""); 431 sb.append(getURI().toString()); // double quotes always escaped in URI representation 432 sb.append('"'); 433 numParams++; 434 } 435 } 436 437 // Serialise scope 438 if (scope != null) { 439 440 if (numParams > 0) 441 sb.append(','); 442 443 sb.append(" scope=\""); 444 sb.append(scope.toString()); 445 sb.append('"'); 446 } 447 448 449 return sb.toString(); 450 } 451 452 453 /** 454 * Parses an OAuth 2.0 bearer token error from the specified HTTP 455 * response {@code WWW-Authenticate} header. 456 * 457 * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 458 * Must not be {@code null}. 459 * 460 * @return The bearer token error. 461 * 462 * @throws ParseException If the {@code WWW-Authenticate} header value 463 * couldn't be parsed to a Bearer token error. 464 */ 465 public static BearerTokenError parse(final String wwwAuth) 466 throws ParseException { 467 468 // We must have a WWW-Authenticate header set to Bearer .* 469 if (! wwwAuth.regionMatches(true, 0, "Bearer", 0, "Bearer".length())) 470 throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 Bearer"); 471 472 Matcher m; 473 474 // Parse optional realm 475 m = realmPattern.matcher(wwwAuth); 476 477 String realm = null; 478 479 if (m.find()) 480 realm = m.group(1); 481 482 if (realm != null) 483 realm = realm.replace("\\\"", "\""); // strip escaped double quotes 484 485 486 // Parse optional error 487 String errorCode = null; 488 String errorDescription = null; 489 URI errorURI = null; 490 491 m = errorPattern.matcher(wwwAuth); 492 493 if (m.find()) { 494 495 // Error code: try group with double quotes, else group with no quotes 496 errorCode = m.group(2) != null ? m.group(2) : m.group(3); 497 498 if (! ErrorObject.isLegal(errorCode)) 499 errorCode = null; // found invalid chars 500 501 // Parse optional error description 502 m = errorDescriptionPattern.matcher(wwwAuth); 503 504 if (m.find()) 505 errorDescription = m.group(1); 506 507 508 // Parse optional error URI 509 m = errorURIPattern.matcher(wwwAuth); 510 511 if (m.find()) { 512 try { 513 errorURI = new URI(m.group(1)); 514 } catch (URISyntaxException e) { 515 // ignore, URI is not required to construct error object 516 } 517 } 518 } 519 520 521 Scope scope = null; 522 523 m = scopePattern.matcher(wwwAuth); 524 525 if (m.find()) 526 scope = Scope.parse(m.group(1)); 527 528 529 return new BearerTokenError(errorCode, 530 errorDescription, 531 0, // HTTP status code not known 532 errorURI, 533 realm, 534 scope); 535 } 536}