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