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. 177 */ 178 private static final Pattern realmPattern = Pattern.compile("realm=\"(([^\\\\\"]|\\\\.)*)\""); 179 180 181 /** 182 * Regex pattern for matching the error parameter of a WWW-Authenticate 183 * header. Double quoting is optional. 184 */ 185 private static final Pattern errorPattern = Pattern.compile("error=(\"([\\w\\_-]+)\"|([\\w\\_-]+))"); 186 187 188 /** 189 * Regex pattern for matching the error description parameter of a 190 * WWW-Authenticate header. 191 */ 192 private static final Pattern errorDescriptionPattern = Pattern.compile("error_description=\"([^\"]+)\""); 193 194 195 /** 196 * Regex pattern for matching the error URI parameter of a 197 * WWW-Authenticate header. 198 */ 199 private static final Pattern errorURIPattern = Pattern.compile("error_uri=\"([^\"]+)\""); 200 201 202 /** 203 * Regex pattern for matching the scope parameter of a WWW-Authenticate 204 * header. 205 */ 206 private static final Pattern scopePattern = Pattern.compile("scope=\"([^\"]+)"); 207 208 209 /** 210 * The realm, {@code null} if not specified. 211 */ 212 private final String realm; 213 214 215 /** 216 * Required scope, {@code null} if not specified. 217 */ 218 private final Scope scope; 219 220 221 /** 222 * Creates a new OAuth 2.0 bearer token error with the specified code 223 * and description. 224 * 225 * @param code The error code, {@code null} if not specified. 226 * @param description The error description, {@code null} if not 227 * specified. 228 */ 229 public BearerTokenError(final String code, final String description) { 230 231 this(code, description, 0, null, null, null); 232 } 233 234 235 /** 236 * Creates a new OAuth 2.0 bearer token error with the specified code, 237 * description and HTTP status code. 238 * 239 * @param code The error code, {@code null} if not specified. 240 * @param description The error description, {@code null} if not 241 * specified. 242 * @param httpStatusCode The HTTP status code, zero if not specified. 243 */ 244 public BearerTokenError(final String code, final String description, final int httpStatusCode) { 245 246 this(code, description, httpStatusCode, null, null, null); 247 } 248 249 250 /** 251 * Creates a new OAuth 2.0 bearer token error with the specified code, 252 * description, HTTP status code, page URI, realm and scope. 253 * 254 * @param code The error code, {@code null} if not specified. 255 * @param description The error description, {@code null} if not 256 * specified. 257 * @param httpStatusCode The HTTP status code, zero if not specified. 258 * @param uri The error page URI, {@code null} if not 259 * specified. 260 * @param realm The realm, {@code null} if not specified. 261 * @param scope The required scope, {@code null} if not 262 * specified. 263 */ 264 public BearerTokenError(final String code, 265 final String description, 266 final int httpStatusCode, 267 final URI uri, 268 final String realm, 269 final Scope scope) { 270 271 super(code, description, httpStatusCode, uri); 272 this.realm = realm; 273 this.scope = scope; 274 275 if (scope != null && ! isScopeWithValidChars(scope)) { 276 throw new IllegalArgumentException("The scope contains illegal characters, see RFC 6750, section 3"); 277 } 278 } 279 280 281 @Override 282 public BearerTokenError setDescription(final String description) { 283 284 return new BearerTokenError(super.getCode(), description, super.getHTTPStatusCode(), super.getURI(), realm, scope); 285 } 286 287 288 @Override 289 public BearerTokenError appendDescription(final String text) { 290 291 String newDescription; 292 293 if (getDescription() != null) 294 newDescription = getDescription() + text; 295 else 296 newDescription = text; 297 298 return new BearerTokenError(super.getCode(), newDescription, super.getHTTPStatusCode(), super.getURI(), realm, scope); 299 } 300 301 302 @Override 303 public BearerTokenError setHTTPStatusCode(final int httpStatusCode) { 304 305 return new BearerTokenError(super.getCode(), super.getDescription(), httpStatusCode, super.getURI(), realm, scope); 306 } 307 308 309 @Override 310 public BearerTokenError setURI(final URI uri) { 311 312 return new BearerTokenError(super.getCode(), super.getDescription(), super.getHTTPStatusCode(), uri, realm, scope); 313 } 314 315 316 /** 317 * Gets the realm. 318 * 319 * @return The realm, {@code null} if not specified. 320 */ 321 public String getRealm() { 322 323 return realm; 324 } 325 326 327 /** 328 * Sets the realm. 329 * 330 * @param realm realm, {@code null} if not specified. 331 * 332 * @return A copy of this error with the specified realm. 333 */ 334 public BearerTokenError setRealm(final String realm) { 335 336 return new BearerTokenError(getCode(), 337 getDescription(), 338 getHTTPStatusCode(), 339 getURI(), 340 realm, 341 getScope()); 342 } 343 344 345 /** 346 * Gets the required scope. 347 * 348 * @return The required scope, {@code null} if not specified. 349 */ 350 public Scope getScope() { 351 352 return scope; 353 } 354 355 356 /** 357 * Sets the required scope. 358 * 359 * @param scope The required scope, {@code null} if not specified. 360 * 361 * @return A copy of this error with the specified required scope. 362 */ 363 public BearerTokenError setScope(final Scope scope) { 364 365 return new BearerTokenError(getCode(), 366 getDescription(), 367 getHTTPStatusCode(), 368 getURI(), 369 getRealm(), 370 scope); 371 } 372 373 374 /** 375 * Returns the {@code WWW-Authenticate} HTTP response header code for 376 * this bearer access token error response. 377 * 378 * <p>Example: 379 * 380 * <pre> 381 * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token" 382 * </pre> 383 * 384 * @return The {@code Www-Authenticate} header value. 385 */ 386 public String toWWWAuthenticateHeader() { 387 388 StringBuilder sb = new StringBuilder("Bearer"); 389 390 int numParams = 0; 391 392 // Serialise realm, may contain double quotes 393 if (realm != null) { 394 sb.append(" realm=\""); 395 sb.append(getRealm().replaceAll("\"","\\\\\"")); 396 sb.append('"'); 397 398 numParams++; 399 } 400 401 // Serialise error, error_description, error_uri 402 if (getCode() != null) { 403 404 if (numParams > 0) 405 sb.append(','); 406 407 sb.append(" error=\""); 408 sb.append(getCode()); 409 sb.append('"'); 410 numParams++; 411 412 if (getDescription() != null) { 413 414 if (numParams > 0) 415 sb.append(','); 416 417 sb.append(" error_description=\""); 418 sb.append(getDescription()); 419 sb.append('"'); 420 numParams++; 421 } 422 423 if (getURI() != null) { 424 425 if (numParams > 0) 426 sb.append(','); 427 428 sb.append(" error_uri=\""); 429 sb.append(getURI().toString()); // double quotes always escaped in URI representation 430 sb.append('"'); 431 numParams++; 432 } 433 } 434 435 // Serialise scope 436 if (scope != null) { 437 438 if (numParams > 0) 439 sb.append(','); 440 441 sb.append(" scope=\""); 442 sb.append(scope.toString()); 443 sb.append('"'); 444 } 445 446 447 return sb.toString(); 448 } 449 450 451 /** 452 * Parses an OAuth 2.0 bearer token error from the specified HTTP 453 * response {@code WWW-Authenticate} header. 454 * 455 * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 456 * Must not be {@code null}. 457 * 458 * @return The bearer token error. 459 * 460 * @throws ParseException If the {@code WWW-Authenticate} header value 461 * couldn't be parsed to a Bearer token error. 462 */ 463 public static BearerTokenError parse(final String wwwAuth) 464 throws ParseException { 465 466 // We must have a WWW-Authenticate header set to Bearer .* 467 if (! wwwAuth.regionMatches(true, 0, "Bearer", 0, "Bearer".length())) 468 throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 Bearer"); 469 470 Matcher m; 471 472 // Parse optional realm 473 m = realmPattern.matcher(wwwAuth); 474 475 String realm = null; 476 477 if (m.find()) 478 realm = m.group(1); 479 480 if (realm != null) 481 realm = realm.replace("\\\"", "\""); // strip escaped double quotes 482 483 484 // Parse optional error 485 String errorCode = null; 486 String errorDescription = null; 487 URI errorURI = null; 488 489 m = errorPattern.matcher(wwwAuth); 490 491 if (m.find()) { 492 493 // Error code: try group with double quotes, else group with no quotes 494 errorCode = m.group(2) != null ? m.group(2) : m.group(3); 495 496 if (! ErrorObject.isLegal(errorCode)) 497 errorCode = null; // found invalid chars 498 499 // Parse optional error description 500 m = errorDescriptionPattern.matcher(wwwAuth); 501 502 if (m.find()) 503 errorDescription = m.group(1); 504 505 506 // Parse optional error URI 507 m = errorURIPattern.matcher(wwwAuth); 508 509 if (m.find()) { 510 try { 511 errorURI = new URI(m.group(1)); 512 } catch (URISyntaxException e) { 513 // ignore, URI is not required to construct error object 514 } 515 } 516 } 517 518 519 Scope scope = null; 520 521 m = scopePattern.matcher(wwwAuth); 522 523 if (m.find()) 524 scope = Scope.parse(m.group(1)); 525 526 527 return new BearerTokenError(errorCode, 528 errorDescription, 529 0, // HTTP status code not known 530 errorURI, 531 realm, 532 scope); 533 } 534}