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