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