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