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 org.apache.commons.lang3.StringEscapeUtils; 029 030import com.nimbusds.oauth2.sdk.ErrorObject; 031import com.nimbusds.oauth2.sdk.ParseException; 032import com.nimbusds.oauth2.sdk.Scope; 033import com.nimbusds.oauth2.sdk.http.HTTPResponse; 034 035 036/** 037 * OAuth 2.0 bearer token error. Used to indicate that access to a resource 038 * protected by a Bearer access token is denied, due to the request or token 039 * being invalid, or due to the access token having insufficient scope. 040 * 041 * <p>Standard bearer access token errors: 042 * 043 * <ul> 044 * <li>{@link #MISSING_TOKEN} 045 * <li>{@link #INVALID_REQUEST} 046 * <li>{@link #INVALID_TOKEN} 047 * <li>{@link #INSUFFICIENT_SCOPE} 048 * </ul> 049 * 050 * <p>Example HTTP response: 051 * 052 * <pre> 053 * HTTP/1.1 401 Unauthorized 054 * WWW-Authenticate: Bearer realm="example.com", 055 * error="invalid_token", 056 * error_description="The access token expired" 057 * </pre> 058 * 059 * <p>Related specifications: 060 * 061 * <ul> 062 * <li>OAuth 2.0 Bearer Token Usage (RFC 6750), section 3.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 * Regex pattern for matching the realm parameter of a WWW-Authenticate 116 * header. 117 */ 118 private static final Pattern realmPattern = Pattern.compile("realm=\"([^\"]+)"); 119 120 121 /** 122 * Regex pattern for matching the error parameter of a WWW-Authenticate 123 * header. Double quoting is optional. 124 */ 125 private static final Pattern errorPattern = Pattern.compile("error=[\"]?([\\w\\_-]+)[\"]?"); 126 127 128 /** 129 * Regex pattern for matching the error description parameter of a 130 * WWW-Authenticate header. 131 */ 132 private static final Pattern errorDescriptionPattern = Pattern.compile("error_description=\"([^\"]+)\""); 133 134 135 /** 136 * Regex pattern for matching the error URI parameter of a 137 * WWW-Authenticate header. 138 */ 139 private static final Pattern errorURIPattern = Pattern.compile("error_uri=\"([^\"]+)\""); 140 141 142 /** 143 * Regex pattern for matching the scope parameter of a WWW-Authenticate 144 * header. 145 */ 146 private static final Pattern scopePattern = Pattern.compile("scope=\"([^\"]+)"); 147 148 149 /** 150 * The realm, {@code null} if not specified. 151 */ 152 private final String realm; 153 154 155 /** 156 * Required scope, {@code null} if not specified. 157 */ 158 private final Scope scope; 159 160 161 /** 162 * Creates a new OAuth 2.0 bearer token error with the specified code 163 * and description. 164 * 165 * @param code The error code, {@code null} if not specified. 166 * @param description The error description, {@code null} if not 167 * specified. 168 */ 169 public BearerTokenError(final String code, final String description) { 170 171 this(code, description, 0, null, null, null); 172 } 173 174 175 /** 176 * Creates a new OAuth 2.0 bearer token error with the specified code, 177 * description and HTTP status code. 178 * 179 * @param code The error code, {@code null} if not specified. 180 * @param description The error description, {@code null} if not 181 * specified. 182 * @param httpStatusCode The HTTP status code, zero if not specified. 183 */ 184 public BearerTokenError(final String code, final String description, final int httpStatusCode) { 185 186 this(code, description, httpStatusCode, null, null, null); 187 } 188 189 190 /** 191 * Creates a new OAuth 2.0 bearer token error with the specified code, 192 * description, HTTP status code, page URI, realm and scope. 193 * 194 * @param code The error code, {@code null} if not specified. 195 * @param description The error description, {@code null} if not 196 * specified. 197 * @param httpStatusCode The HTTP status code, zero if not specified. 198 * @param uri The error page URI, {@code null} if not 199 * specified. 200 * @param realm The realm, {@code null} if not specified. 201 * @param scope The required scope, {@code null} if not 202 * specified. 203 */ 204 public BearerTokenError(final String code, 205 final String description, 206 final int httpStatusCode, 207 final URI uri, 208 final String realm, 209 final Scope scope) { 210 211 super(code, description, httpStatusCode, uri); 212 this.realm = realm; 213 this.scope = scope; 214 } 215 216 217 @Override 218 public BearerTokenError setDescription(final String description) { 219 220 return new BearerTokenError(super.getCode(), description, super.getHTTPStatusCode(), super.getURI(), realm, scope); 221 } 222 223 224 @Override 225 public BearerTokenError appendDescription(final String text) { 226 227 String newDescription; 228 229 if (getDescription() != null) 230 newDescription = getDescription() + text; 231 else 232 newDescription = text; 233 234 return new BearerTokenError(super.getCode(), newDescription, super.getHTTPStatusCode(), super.getURI(), realm, scope); 235 } 236 237 238 @Override 239 public BearerTokenError setHTTPStatusCode(final int httpStatusCode) { 240 241 return new BearerTokenError(super.getCode(), super.getDescription(), httpStatusCode, super.getURI(), realm, scope); 242 } 243 244 245 @Override 246 public BearerTokenError setURI(final URI uri) { 247 248 return new BearerTokenError(super.getCode(), super.getDescription(), super.getHTTPStatusCode(), uri, realm, scope); 249 } 250 251 252 /** 253 * Gets the realm. 254 * 255 * @return The realm, {@code null} if not specified. 256 */ 257 public String getRealm() { 258 259 return realm; 260 } 261 262 263 /** 264 * Sets the realm. 265 * 266 * @param realm realm, {@code null} if not specified. 267 * 268 * @return A copy of this error with the specified realm. 269 */ 270 public BearerTokenError setRealm(final String realm) { 271 272 return new BearerTokenError(getCode(), 273 getDescription(), 274 getHTTPStatusCode(), 275 getURI(), 276 realm, 277 getScope()); 278 } 279 280 281 /** 282 * Gets the required scope. 283 * 284 * @return The required scope, {@code null} if not specified. 285 */ 286 public Scope getScope() { 287 288 return scope; 289 } 290 291 292 /** 293 * Sets the required scope. 294 * 295 * @param scope The required scope, {@code null} if not specified. 296 * 297 * @return A copy of this error with the specified required scope. 298 */ 299 public BearerTokenError setScope(final Scope scope) { 300 301 return new BearerTokenError(getCode(), 302 getDescription(), 303 getHTTPStatusCode(), 304 getURI(), 305 getRealm(), 306 scope); 307 } 308 309 310 /** 311 * Returns the {@code WWW-Authenticate} HTTP response header code for 312 * this bearer access token error response. 313 * 314 * <p>Example: 315 * 316 * <pre> 317 * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token" 318 * </pre> 319 * 320 * @return The {@code Www-Authenticate} header value. 321 */ 322 public String toWWWAuthenticateHeader() { 323 324 StringBuilder sb = new StringBuilder("Bearer"); 325 326 int numParams = 0; 327 328 // Serialise realm 329 if (realm != null) { 330 sb.append(" realm=\""); 331 sb.append(StringEscapeUtils.escapeJava(realm)); 332 sb.append('"'); 333 334 numParams++; 335 } 336 337 // Serialise error, error_description, error_uri 338 if (getCode() != null) { 339 340 if (numParams > 0) 341 sb.append(','); 342 343 sb.append(" error=\""); 344 sb.append(StringEscapeUtils.escapeJava(getCode())); 345 sb.append('"'); 346 numParams++; 347 348 if (getDescription() != null) { 349 350 if (numParams > 0) 351 sb.append(','); 352 353 sb.append(" error_description=\""); 354 sb.append(StringEscapeUtils.escapeJava(getDescription())); 355 sb.append('"'); 356 numParams++; 357 } 358 359 if (getURI() != null) { 360 361 if (numParams > 0) 362 sb.append(','); 363 364 sb.append(" error_uri=\""); 365 sb.append(StringEscapeUtils.escapeJava(getURI().toString())); 366 sb.append('"'); 367 numParams++; 368 } 369 } 370 371 // Serialise scope 372 if (scope != null) { 373 374 if (numParams > 0) 375 sb.append(','); 376 377 sb.append(" scope=\""); 378 sb.append(StringEscapeUtils.escapeJava(scope.toString())); 379 sb.append('"'); 380 } 381 382 383 return sb.toString(); 384 } 385 386 387 /** 388 * Parses an OAuth 2.0 bearer token error from the specified HTTP 389 * response {@code WWW-Authenticate} header. 390 * 391 * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 392 * Must not be {@code null}. 393 * 394 * @throws ParseException If the {@code WWW-Authenticate} header value 395 * couldn't be parsed to a Bearer token error. 396 */ 397 public static BearerTokenError parse(final String wwwAuth) 398 throws ParseException { 399 400 // We must have a WWW-Authenticate header set to Bearer .* 401 if (! wwwAuth.regionMatches(true, 0, "Bearer", 0, "Bearer".length())) 402 throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 Bearer"); 403 404 Matcher m; 405 406 // Parse optional realm 407 m = realmPattern.matcher(wwwAuth); 408 409 String realm = null; 410 411 if (m.find()) 412 realm = m.group(1); 413 414 415 // Parse optional error 416 String errorCode = null; 417 String errorDescription = null; 418 URI errorURI = null; 419 420 m = errorPattern.matcher(wwwAuth); 421 422 if (m.find()) { 423 424 errorCode = m.group(1); 425 426 // Parse optional error description 427 m = errorDescriptionPattern.matcher(wwwAuth); 428 429 if (m.find()) 430 errorDescription = m.group(1); 431 432 433 // Parse optional error URI 434 m = errorURIPattern.matcher(wwwAuth); 435 436 if (m.find()) { 437 438 try { 439 errorURI = new URI(m.group(1)); 440 441 } catch (URISyntaxException e) { 442 443 throw new ParseException("Invalid error URI: " + m.group(1), e); 444 } 445 } 446 } 447 448 449 Scope scope = null; 450 451 m = scopePattern.matcher(wwwAuth); 452 453 if (m.find()) 454 scope = Scope.parse(m.group(1)); 455 456 457 return new BearerTokenError(errorCode, 458 errorDescription, 459 0, // HTTP status code 460 errorURI, 461 realm, 462 scope); 463 } 464}