001package com.nimbusds.oauth2.sdk.token; 002 003 004import java.net.MalformedURLException; 005import java.net.URL; 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 URL 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 /** 201 * Gets the realm. 202 * 203 * @return The realm, {@code null} if not specified. 204 */ 205 public String getRealm() { 206 207 return realm; 208 } 209 210 211 /** 212 * Sets the realm. 213 * 214 * @param realm realm, {@code null} if not specified. 215 * 216 * @return A copy of this error with the specified realm. 217 */ 218 public BearerTokenError setRealm(final String realm) { 219 220 return new BearerTokenError(getCode(), 221 getDescription(), 222 getHTTPStatusCode(), 223 getURI(), 224 realm, 225 getScope()); 226 } 227 228 229 /** 230 * Gets the required scope. 231 * 232 * @return The required scope, {@code null} if not specified. 233 */ 234 public Scope getScope() { 235 236 return scope; 237 } 238 239 240 /** 241 * Sets the required scope. 242 * 243 * @param scope The required scope, {@code null} if not specified. 244 * 245 * @return A copy of this error with the specified required scope. 246 */ 247 public BearerTokenError setScope(final Scope scope) { 248 249 return new BearerTokenError(getCode(), 250 getDescription(), 251 getHTTPStatusCode(), 252 getURI(), 253 getRealm(), 254 scope); 255 } 256 257 258 /** 259 * Returns the {@code WWW-Authenticate} HTTP response header code for 260 * this bearer access token error response. 261 * 262 * <p>Example: 263 * 264 * <pre> 265 * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token" 266 * </pre> 267 * 268 * @return The {@code Www-Authenticate} header value. 269 */ 270 public String toWWWAuthenticateHeader() { 271 272 StringBuilder sb = new StringBuilder("Bearer"); 273 274 int numParams = 0; 275 276 // Serialise realm 277 if (realm != null) { 278 sb.append(" realm=\""); 279 sb.append(StringEscapeUtils.escapeJava(realm)); 280 sb.append('"'); 281 282 numParams++; 283 } 284 285 // Serialise error, error_description, error_uri 286 if (getCode() != null) { 287 288 if (numParams > 0) 289 sb.append(','); 290 291 sb.append(" error=\""); 292 sb.append(StringEscapeUtils.escapeJava(getCode())); 293 sb.append('"'); 294 numParams++; 295 296 if (getDescription() != null) { 297 298 if (numParams > 0) 299 sb.append(','); 300 301 sb.append(" error_description=\""); 302 sb.append(StringEscapeUtils.escapeJava(getDescription())); 303 sb.append('"'); 304 numParams++; 305 } 306 307 if (getURI() != null) { 308 309 if (numParams > 0) 310 sb.append(','); 311 312 sb.append(" error_uri=\""); 313 sb.append(StringEscapeUtils.escapeJava(getURI().toString())); 314 sb.append('"'); 315 numParams++; 316 } 317 } 318 319 // Serialise scope 320 if (scope != null) { 321 322 if (numParams > 0) 323 sb.append(','); 324 325 sb.append(" scope=\""); 326 sb.append(StringEscapeUtils.escapeJava(scope.toString())); 327 sb.append('"'); 328 } 329 330 331 return sb.toString(); 332 } 333 334 335 /** 336 * Parses an OAuth 2.0 bearer token error from the specified HTTP 337 * response {@code WWW-Authenticate} header. 338 * 339 * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 340 * Must not be {@code null}. 341 * 342 * @throws ParseException If the {@code WWW-Authenticate} header value 343 * couldn't be parsed to a Bearer token error. 344 */ 345 public static BearerTokenError parse(final String wwwAuth) 346 throws ParseException { 347 348 // We must have a WWW-Authenticate header set to Bearer .* 349 if (! wwwAuth.regionMatches(true, 0, "Bearer", 0, "Bearer".length())) 350 throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 Bearer"); 351 352 Matcher m; 353 354 // Parse optional realm 355 m = realmPattern.matcher(wwwAuth); 356 357 String realm = null; 358 359 if (m.find()) 360 realm = m.group(1); 361 362 363 // Parse optional error 364 String errorCode = null; 365 String errorDescription = null; 366 URL errorURI = null; 367 368 m = errorPattern.matcher(wwwAuth); 369 370 if (m.find()) { 371 372 errorCode = m.group(1); 373 374 // Parse optional error description 375 m = errorDescriptionPattern.matcher(wwwAuth); 376 377 if (m.find()) 378 errorDescription = m.group(1); 379 380 381 // Parse optional error URI 382 m = errorURIPattern.matcher(wwwAuth); 383 384 if (m.find()) { 385 386 try { 387 errorURI = new URL(m.group(1)); 388 389 } catch (MalformedURLException e) { 390 391 throw new ParseException("Invalid error URI: " + m.group(1), e); 392 } 393 } 394 } 395 396 397 Scope scope = null; 398 399 m = scopePattern.matcher(wwwAuth); 400 401 if (m.find()) 402 scope = Scope.parse(m.group(1)); 403 404 405 return new BearerTokenError(errorCode, 406 errorDescription, 407 0, // HTTP status code 408 errorURI, 409 realm, 410 scope); 411 } 412}