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