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