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 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 acceptable JWS algorithms, {@code null} if not specified. 140 */ 141 private final Set<JWSAlgorithm> jwsAlgs; 142 143 144 /** 145 * Creates a new OAuth 2.0 DPoP 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 DPoPTokenError(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 DPoP 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 DPoPTokenError(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 DPoP 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 DPoPTokenError(final String code, 188 final String description, 189 final int httpStatusCode, 190 final URI uri, 191 final String realm, 192 final Scope scope) { 193 194 this(code, description, httpStatusCode, uri, realm, scope, null); 195 } 196 197 198 /** 199 * Creates a new OAuth 2.0 DPoP token error with the specified code, 200 * description, HTTP status code, page URI, realm and scope. 201 * 202 * @param code The error code, {@code null} if not specified. 203 * @param description The error description, {@code null} if not 204 * specified. 205 * @param httpStatusCode The HTTP status code, zero if not specified. 206 * @param uri The error page URI, {@code null} if not 207 * specified. 208 * @param realm The realm, {@code null} if not specified. 209 * @param scope The required scope, {@code null} if not 210 * specified. 211 * @param jwsAlgs The acceptable JWS algorithms, {@code null} if 212 * not specified. 213 */ 214 public DPoPTokenError(final String code, 215 final String description, 216 final int httpStatusCode, 217 final URI uri, 218 final String realm, 219 final Scope scope, 220 final Set<JWSAlgorithm> jwsAlgs) { 221 222 super(AccessTokenType.DPOP, code, description, httpStatusCode, uri, realm, scope); 223 224 this.jwsAlgs = jwsAlgs; 225 } 226 227 228 @Override 229 public DPoPTokenError setDescription(final String description) { 230 231 return new DPoPTokenError( 232 getCode(), 233 description, 234 getHTTPStatusCode(), 235 getURI(), 236 getRealm(), 237 getScope(), 238 getJWSAlgorithms() 239 ); 240 } 241 242 243 @Override 244 public DPoPTokenError appendDescription(final String text) { 245 246 String newDescription; 247 if (getDescription() != null) 248 newDescription = getDescription() + text; 249 else 250 newDescription = text; 251 252 return new DPoPTokenError( 253 getCode(), 254 newDescription, 255 getHTTPStatusCode(), 256 getURI(), 257 getRealm(), 258 getScope(), 259 getJWSAlgorithms() 260 ); 261 } 262 263 264 @Override 265 public DPoPTokenError setHTTPStatusCode(final int httpStatusCode) { 266 267 return new DPoPTokenError( 268 getCode(), 269 getDescription(), 270 httpStatusCode, 271 getURI(), 272 getRealm(), 273 getScope(), 274 getJWSAlgorithms() 275 ); 276 } 277 278 279 @Override 280 public DPoPTokenError setURI(final URI uri) { 281 282 return new DPoPTokenError( 283 getCode(), 284 getDescription(), 285 getHTTPStatusCode(), 286 uri, 287 getRealm(), 288 getScope(), 289 getJWSAlgorithms() 290 ); 291 } 292 293 294 @Override 295 public DPoPTokenError setRealm(final String realm) { 296 297 return new DPoPTokenError( 298 getCode(), 299 getDescription(), 300 getHTTPStatusCode(), 301 getURI(), 302 realm, 303 getScope(), 304 getJWSAlgorithms() 305 ); 306 } 307 308 309 @Override 310 public DPoPTokenError setScope(final Scope scope) { 311 312 return new DPoPTokenError( 313 getCode(), 314 getDescription(), 315 getHTTPStatusCode(), 316 getURI(), 317 getRealm(), 318 scope, 319 getJWSAlgorithms() 320 ); 321 } 322 323 324 /** 325 * Returns the acceptable JWS algorithms. 326 * 327 * @return The acceptable JWS algorithms, {@code null} if not 328 * specified. 329 */ 330 public Set<JWSAlgorithm> getJWSAlgorithms() { 331 332 return jwsAlgs; 333 } 334 335 336 /** 337 * Sets the acceptable JWS algorithms. 338 * 339 * @param jwsAlgs The acceptable JWS algorithms, {@code null} if not 340 * specified. 341 * 342 * @return A copy of this error with the specified acceptable JWS 343 * algorithms. 344 */ 345 public DPoPTokenError setJWSAlgorithms(final Set<JWSAlgorithm> jwsAlgs) { 346 347 return new DPoPTokenError( 348 getCode(), 349 getDescription(), 350 getHTTPStatusCode(), 351 getURI(), 352 getRealm(), 353 getScope(), 354 jwsAlgs 355 ); 356 } 357 358 359 /** 360 * Returns the {@code WWW-Authenticate} HTTP response header code for 361 * this DPoP access token error response. 362 * 363 * <p>Example: 364 * 365 * <pre> 366 * DPoP realm="example.com", error="invalid_token", error_description="Invalid access token" 367 * </pre> 368 * 369 * @return The {@code Www-Authenticate} header value. 370 */ 371 @Override 372 public String toWWWAuthenticateHeader() { 373 374 String header = super.toWWWAuthenticateHeader(); 375 376 if (CollectionUtils.isEmpty(getJWSAlgorithms())) { 377 return header; 378 } 379 380 StringBuilder sb = new StringBuilder(header); 381 382 if (header.contains("=")) { 383 sb.append(','); 384 } 385 386 sb.append(" algs=\""); 387 388 String delim = ""; 389 for (JWSAlgorithm alg: getJWSAlgorithms()) { 390 sb.append(delim); 391 delim = " "; 392 sb.append(alg.getName()); 393 } 394 sb.append("\""); 395 396 return sb.toString(); 397 } 398 399 400 /** 401 * Parses an OAuth 2.0 DPoP token error from the specified HTTP 402 * response {@code WWW-Authenticate} header. 403 * 404 * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 405 * Must not be {@code null}. 406 * 407 * @return The DPoP token error. 408 * 409 * @throws ParseException If the {@code WWW-Authenticate} header value 410 * couldn't be parsed to a DPoP token error. 411 */ 412 public static DPoPTokenError parse(final String wwwAuth) 413 throws ParseException { 414 415 TokenSchemeError genericError = TokenSchemeError.parse(wwwAuth, AccessTokenType.DPOP); 416 417 Set<JWSAlgorithm> jwsAlgs = null; 418 419 Matcher m = ALGS_PATTERN.matcher(wwwAuth); 420 421 if (m.find()) { 422 String algsString = m.group(1); 423 jwsAlgs = new HashSet<>(); 424 for (String algName: algsString.split("\\s+")) { 425 jwsAlgs.add(JWSAlgorithm.parse(algName)); 426 } 427 } 428 429 return new DPoPTokenError( 430 genericError.getCode(), 431 genericError.getDescription(), 432 genericError.getHTTPStatusCode(), 433 genericError.getURI(), 434 genericError.getRealm(), 435 genericError.getScope(), 436 jwsAlgs 437 ); 438 } 439}