001package com.nimbusds.oauth2.sdk.token; 002 003 004import java.net.URI; 005import java.net.URISyntaxException; 006import java.util.regex.Matcher; 007import java.util.regex.Pattern; 008 009import net.jcip.annotations.Immutable; 010 011import org.apache.commons.lang3.StringEscapeUtils; 012 013import com.nimbusds.oauth2.sdk.ErrorObject; 014import com.nimbusds.oauth2.sdk.ParseException; 015import com.nimbusds.oauth2.sdk.Scope; 016import com.nimbusds.oauth2.sdk.http.HTTPResponse; 017 018 019/** 020 * OAuth 2.0 bearer token error. Used to indicate that access to a resource 021 * protected by a Bearer access token is denied, due to the request or token 022 * being invalid, or due to the access token having insufficient scope. 023 * 024 * <p>Standard bearer access token errors: 025 * 026 * <ul> 027 * <li>{@link #MISSING_TOKEN} 028 * <li>{@link #INVALID_REQUEST} 029 * <li>{@link #INVALID_TOKEN} 030 * <li>{@link #INSUFFICIENT_SCOPE} 031 * </ul> 032 * 033 * <p>Example HTTP response: 034 * 035 * <pre> 036 * HTTP/1.1 401 Unauthorized 037 * WWW-Authenticate: Bearer realm="example.com", 038 * error="invalid_token", 039 * error_description="The access token expired" 040 * </pre> 041 * 042 * <p>Related specifications: 043 * 044 * <ul> 045 * <li>OAuth 2.0 Bearer Token Usage (RFC 6750), section 3.1. 046 * </ul> 047 */ 048@Immutable 049public class BearerTokenError extends ErrorObject { 050 051 052 /** 053 * The request does not contain an access token. No error code or 054 * description is specified for this error, just the HTTP status code 055 * is set to 401 (Unauthorized). 056 * 057 * <p>Example: 058 * 059 * <pre> 060 * HTTP/1.1 401 Unauthorized 061 * WWW-Authenticate: Bearer 062 * </pre> 063 */ 064 public static final BearerTokenError MISSING_TOKEN = 065 new BearerTokenError(null, null, HTTPResponse.SC_UNAUTHORIZED); 066 067 /** 068 * The request is missing a required parameter, includes an unsupported 069 * parameter or parameter value, repeats the same parameter, uses more 070 * than one method for including an access token, or is otherwise 071 * malformed. The HTTP status code is set to 400 (Bad Request). 072 */ 073 public static final BearerTokenError INVALID_REQUEST = 074 new BearerTokenError("invalid_request", "Invalid request", 075 HTTPResponse.SC_BAD_REQUEST); 076 077 078 /** 079 * The access token provided is expired, revoked, malformed, or invalid 080 * for other reasons. The HTTP status code is set to 401 081 * (Unauthorized). 082 */ 083 public static final BearerTokenError INVALID_TOKEN = 084 new BearerTokenError("invalid_token", "Invalid access token", 085 HTTPResponse.SC_UNAUTHORIZED); 086 087 088 /** 089 * The request requires higher privileges than provided by the access 090 * token. The HTTP status code is set to 403 (Forbidden). 091 */ 092 public static final BearerTokenError INSUFFICIENT_SCOPE = 093 new BearerTokenError("insufficient_scope", "Insufficient scope", 094 HTTPResponse.SC_FORBIDDEN); 095 096 097 /** 098 * Regex pattern for matching the realm parameter of a WWW-Authenticate 099 * header. 100 */ 101 private static final Pattern realmPattern = Pattern.compile("realm=\"([^\"]+)"); 102 103 104 /** 105 * Regex pattern for matching the error parameter of a WWW-Authenticate 106 * header. 107 */ 108 private static final Pattern errorPattern = Pattern.compile("error=\"([^\"]+)"); 109 110 111 /** 112 * Regex pattern for matching the error description parameter of a 113 * WWW-Authenticate header. 114 */ 115 private static final Pattern errorDescriptionPattern = Pattern.compile("error_description=\"([^\"]+)\""); 116 117 118 /** 119 * Regex pattern for matching the error URI parameter of a 120 * WWW-Authenticate header. 121 */ 122 private static final Pattern errorURIPattern = Pattern.compile("error_uri=\"([^\"]+)\""); 123 124 125 /** 126 * Regex pattern for matching the scope parameter of a WWW-Authenticate 127 * header. 128 */ 129 private static final Pattern scopePattern = Pattern.compile("scope=\"([^\"]+)"); 130 131 132 /** 133 * The realm, {@code null} if not specified. 134 */ 135 private final String realm; 136 137 138 /** 139 * Required scope, {@code null} if not specified. 140 */ 141 private final Scope scope; 142 143 144 /** 145 * Creates a new OAuth 2.0 bearer token error with the specified code 146 * and description. 147 * 148 * @param code The error code, {@code null} if not specified. 149 * @param description The error description, {@code null} if not 150 * specified. 151 */ 152 public BearerTokenError(final String code, final String description) { 153 154 this(code, description, 0, null, null, null); 155 } 156 157 158 /** 159 * Creates a new OAuth 2.0 bearer token error with the specified code, 160 * description and HTTP status code. 161 * 162 * @param code The error code, {@code null} if not specified. 163 * @param description The error description, {@code null} if not 164 * specified. 165 * @param httpStatusCode The HTTP status code, zero if not specified. 166 */ 167 public BearerTokenError(final String code, final String description, final int httpStatusCode) { 168 169 this(code, description, httpStatusCode, null, null, null); 170 } 171 172 173 /** 174 * Creates a new OAuth 2.0 bearer token error with the specified code, 175 * description, HTTP status code, page URI, realm and scope. 176 * 177 * @param code The error code, {@code null} if not specified. 178 * @param description The error description, {@code null} if not 179 * specified. 180 * @param httpStatusCode The HTTP status code, zero if not specified. 181 * @param uri The error page URI, {@code null} if not 182 * specified. 183 * @param realm The realm, {@code null} if not specified. 184 * @param scope The required scope, {@code null} if not 185 * specified. 186 */ 187 public BearerTokenError(final String code, 188 final String description, 189 final int httpStatusCode, 190 final URI uri, 191 final String realm, 192 final Scope scope) { 193 194 super(code, description, httpStatusCode, uri); 195 this.realm = realm; 196 this.scope = scope; 197 } 198 199 200 @Override 201 public BearerTokenError setDescription(final String description) { 202 203 return new BearerTokenError(super.getCode(), description, super.getHTTPStatusCode(), super.getURI(), realm, scope); 204 } 205 206 207 @Override 208 public BearerTokenError appendDescription(final String text) { 209 210 String newDescription; 211 212 if (getDescription() != null) 213 newDescription = getDescription() + text; 214 else 215 newDescription = text; 216 217 return new BearerTokenError(super.getCode(), newDescription, super.getHTTPStatusCode(), super.getURI(), realm, scope); 218 } 219 220 221 @Override 222 public BearerTokenError setHTTPStatusCode(final int httpStatusCode) { 223 224 return new BearerTokenError(super.getCode(), super.getDescription(), httpStatusCode, super.getURI(), realm, scope); 225 } 226 227 228 @Override 229 public BearerTokenError setURI(final URI uri) { 230 231 return new BearerTokenError(super.getCode(), super.getDescription(), super.getHTTPStatusCode(), uri, realm, scope); 232 } 233 234 235 /** 236 * Gets the realm. 237 * 238 * @return The realm, {@code null} if not specified. 239 */ 240 public String getRealm() { 241 242 return realm; 243 } 244 245 246 /** 247 * Sets the realm. 248 * 249 * @param realm realm, {@code null} if not specified. 250 * 251 * @return A copy of this error with the specified realm. 252 */ 253 public BearerTokenError setRealm(final String realm) { 254 255 return new BearerTokenError(getCode(), 256 getDescription(), 257 getHTTPStatusCode(), 258 getURI(), 259 realm, 260 getScope()); 261 } 262 263 264 /** 265 * Gets the required scope. 266 * 267 * @return The required scope, {@code null} if not specified. 268 */ 269 public Scope getScope() { 270 271 return scope; 272 } 273 274 275 /** 276 * Sets the required scope. 277 * 278 * @param scope The required scope, {@code null} if not specified. 279 * 280 * @return A copy of this error with the specified required scope. 281 */ 282 public BearerTokenError setScope(final Scope scope) { 283 284 return new BearerTokenError(getCode(), 285 getDescription(), 286 getHTTPStatusCode(), 287 getURI(), 288 getRealm(), 289 scope); 290 } 291 292 293 /** 294 * Returns the {@code WWW-Authenticate} HTTP response header code for 295 * this bearer access token error response. 296 * 297 * <p>Example: 298 * 299 * <pre> 300 * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token" 301 * </pre> 302 * 303 * @return The {@code Www-Authenticate} header value. 304 */ 305 public String toWWWAuthenticateHeader() { 306 307 StringBuilder sb = new StringBuilder("Bearer"); 308 309 int numParams = 0; 310 311 // Serialise realm 312 if (realm != null) { 313 sb.append(" realm=\""); 314 sb.append(StringEscapeUtils.escapeJava(realm)); 315 sb.append('"'); 316 317 numParams++; 318 } 319 320 // Serialise error, error_description, error_uri 321 if (getCode() != null) { 322 323 if (numParams > 0) 324 sb.append(','); 325 326 sb.append(" error=\""); 327 sb.append(StringEscapeUtils.escapeJava(getCode())); 328 sb.append('"'); 329 numParams++; 330 331 if (getDescription() != null) { 332 333 if (numParams > 0) 334 sb.append(','); 335 336 sb.append(" error_description=\""); 337 sb.append(StringEscapeUtils.escapeJava(getDescription())); 338 sb.append('"'); 339 numParams++; 340 } 341 342 if (getURI() != null) { 343 344 if (numParams > 0) 345 sb.append(','); 346 347 sb.append(" error_uri=\""); 348 sb.append(StringEscapeUtils.escapeJava(getURI().toString())); 349 sb.append('"'); 350 numParams++; 351 } 352 } 353 354 // Serialise scope 355 if (scope != null) { 356 357 if (numParams > 0) 358 sb.append(','); 359 360 sb.append(" scope=\""); 361 sb.append(StringEscapeUtils.escapeJava(scope.toString())); 362 sb.append('"'); 363 } 364 365 366 return sb.toString(); 367 } 368 369 370 /** 371 * Parses an OAuth 2.0 bearer token error from the specified HTTP 372 * response {@code WWW-Authenticate} header. 373 * 374 * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 375 * Must not be {@code null}. 376 * 377 * @throws ParseException If the {@code WWW-Authenticate} header value 378 * couldn't be parsed to a Bearer token error. 379 */ 380 public static BearerTokenError parse(final String wwwAuth) 381 throws ParseException { 382 383 // We must have a WWW-Authenticate header set to Bearer .* 384 if (! wwwAuth.regionMatches(true, 0, "Bearer", 0, "Bearer".length())) 385 throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 Bearer"); 386 387 Matcher m; 388 389 // Parse optional realm 390 m = realmPattern.matcher(wwwAuth); 391 392 String realm = null; 393 394 if (m.find()) 395 realm = m.group(1); 396 397 398 // Parse optional error 399 String errorCode = null; 400 String errorDescription = null; 401 URI errorURI = null; 402 403 m = errorPattern.matcher(wwwAuth); 404 405 if (m.find()) { 406 407 errorCode = m.group(1); 408 409 // Parse optional error description 410 m = errorDescriptionPattern.matcher(wwwAuth); 411 412 if (m.find()) 413 errorDescription = m.group(1); 414 415 416 // Parse optional error URI 417 m = errorURIPattern.matcher(wwwAuth); 418 419 if (m.find()) { 420 421 try { 422 errorURI = new URI(m.group(1)); 423 424 } catch (URISyntaxException e) { 425 426 throw new ParseException("Invalid error URI: " + m.group(1), e); 427 } 428 } 429 } 430 431 432 Scope scope = null; 433 434 m = scopePattern.matcher(wwwAuth); 435 436 if (m.find()) 437 scope = Scope.parse(m.group(1)); 438 439 440 return new BearerTokenError(errorCode, 441 errorDescription, 442 0, // HTTP status code 443 errorURI, 444 realm, 445 scope); 446 } 447}