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