001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2021, 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.net.URISyntaxException; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026import com.nimbusds.oauth2.sdk.ErrorObject; 027import com.nimbusds.oauth2.sdk.ParseException; 028import com.nimbusds.oauth2.sdk.Scope; 029 030 031/** 032 * The base abstract class for token scheme errors. Concrete extending classes 033 * should be immutable. 034 */ 035public abstract class TokenSchemeError extends ErrorObject { 036 037 038 private static final long serialVersionUID = -1132784406578139418L; 039 040 041 /** 042 * Regex pattern for matching the realm parameter of a WWW-Authenticate 043 * header. Limits the realm string length to 256 chars to prevent 044 * potential stack overflow exception for very long strings due to 045 * recursive nature of regex. 046 */ 047 static final Pattern REALM_PATTERN = Pattern.compile("realm=\"(([^\\\\\"]|\\\\.){0,256})\""); 048 049 050 /** 051 * Regex pattern for matching the error parameter of a WWW-Authenticate 052 * header. Double quoting is optional. 053 */ 054 static final Pattern ERROR_PATTERN = Pattern.compile("error=(\"([\\w\\_-]+)\"|([\\w\\_-]+))"); 055 056 057 /** 058 * Regex pattern for matching the error description parameter of a 059 * WWW-Authenticate header. 060 */ 061 static final Pattern ERROR_DESCRIPTION_PATTERN = Pattern.compile("error_description=\"([^\"]+)\""); 062 063 064 /** 065 * Regex pattern for matching the error URI parameter of a 066 * WWW-Authenticate header. 067 */ 068 static final Pattern ERROR_URI_PATTERN = Pattern.compile("error_uri=\"([^\"]+)\""); 069 070 071 /** 072 * Regex pattern for matching the scope parameter of a WWW-Authenticate 073 * header. 074 */ 075 static final Pattern SCOPE_PATTERN = Pattern.compile("scope=\"([^\"]+)"); 076 077 078 /** 079 * The token scheme. 080 */ 081 private final AccessTokenType scheme; 082 083 084 /** 085 * The realm, {@code null} if not specified. 086 */ 087 private final String realm; 088 089 090 /** 091 * Required scope, {@code null} if not specified. 092 */ 093 private final Scope scope; 094 095 096 /** 097 * Returns {@code true} if the specified scope consists of valid 098 * characters. Values for the "scope" attributes must not include 099 * characters outside the [0x20, 0x21] | [0x23 - 0x5B] | [0x5D - 0x7E] 100 * range. See RFC 6750, section 3. 101 * 102 * @see ErrorObject#isLegal(String) 103 * 104 * @param scope The scope. 105 * 106 * @return {@code true} if the scope contains valid characters, else 107 * {@code false}. 108 */ 109 public static boolean isScopeWithValidChars(final Scope scope) { 110 111 return ErrorObject.isLegal(scope.toString()); 112 } 113 114 115 /** 116 * Creates a new token error with the specified code, description, HTTP 117 * status code, page URI, realm and scope. 118 * 119 * @param scheme The token scheme. Must not be {@code null}. 120 * @param code The error code, {@code null} if not specified. 121 * @param description The error description, {@code null} if not 122 * specified. 123 * @param httpStatusCode The HTTP status code, zero if not specified. 124 * @param uri The error page URI, {@code null} if not 125 * specified. 126 * @param realm The realm, {@code null} if not specified. 127 * @param scope The required scope, {@code null} if not 128 * specified. 129 */ 130 protected TokenSchemeError(final AccessTokenType scheme, 131 final String code, 132 final String description, 133 final int httpStatusCode, 134 final URI uri, 135 final String realm, 136 final Scope scope) { 137 138 super(code, description, httpStatusCode, uri); 139 140 if (scheme == null) { 141 throw new IllegalArgumentException("The token scheme must not be null"); 142 } 143 this.scheme = scheme; 144 145 this.realm = realm; 146 this.scope = scope; 147 148 if (scope != null && ! isScopeWithValidChars(scope)) { 149 throw new IllegalArgumentException("The scope contains illegal characters, see RFC 6750, section 3"); 150 } 151 } 152 153 154 /** 155 * Returns the token scheme. 156 * 157 * @return The token scheme. 158 */ 159 public AccessTokenType getScheme() { 160 161 return scheme; 162 } 163 164 165 /** 166 * Returns the realm. 167 * 168 * @return The realm, {@code null} if not specified. 169 */ 170 public String getRealm() { 171 172 return realm; 173 } 174 175 176 /** 177 * Returns the required scope. 178 * 179 * @return The required scope, {@code null} if not specified. 180 */ 181 public Scope getScope() { 182 183 return scope; 184 } 185 186 187 @Override 188 public abstract TokenSchemeError setDescription(final String description); 189 190 191 @Override 192 public abstract TokenSchemeError appendDescription(final String text); 193 194 195 @Override 196 public abstract TokenSchemeError setHTTPStatusCode(final int httpStatusCode); 197 198 199 @Override 200 public abstract TokenSchemeError setURI(final URI uri); 201 202 203 /** 204 * Sets the realm. 205 * 206 * @param realm realm, {@code null} if not specified. 207 * 208 * @return A copy of this error with the specified realm. 209 */ 210 public abstract TokenSchemeError setRealm(final String realm); 211 212 213 /** 214 * Sets the required scope. 215 * 216 * @param scope The required scope, {@code null} if not specified. 217 * 218 * @return A copy of this error with the specified required scope. 219 */ 220 public abstract TokenSchemeError setScope(final Scope scope); 221 222 223 /** 224 * Returns the {@code WWW-Authenticate} HTTP response header code for 225 * this token scheme error. 226 * 227 * <p>Example: 228 * 229 * <pre> 230 * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token" 231 * </pre> 232 * 233 * @return The {@code Www-Authenticate} header value. 234 */ 235 public String toWWWAuthenticateHeader() { 236 237 StringBuilder sb = new StringBuilder(getScheme().getValue()); 238 239 int numParams = 0; 240 241 // Serialise realm, may contain double quotes 242 if (getRealm() != null) { 243 sb.append(" realm=\""); 244 sb.append(getRealm().replaceAll("\"","\\\\\"")); 245 sb.append('"'); 246 247 numParams++; 248 } 249 250 // Serialise error, error_description, error_uri 251 if (getCode() != null) { 252 253 if (numParams > 0) 254 sb.append(','); 255 256 sb.append(" error=\""); 257 sb.append(getCode()); 258 sb.append('"'); 259 numParams++; 260 261 if (getDescription() != null) { 262 // Output description only if code is present 263 sb.append(','); 264 sb.append(" error_description=\""); 265 sb.append(getDescription()); 266 sb.append('"'); 267 numParams++; 268 } 269 270 if (getURI() != null) { 271 // Output description only if code is present 272 sb.append(','); 273 sb.append(" error_uri=\""); 274 sb.append(getURI().toString()); // double quotes always escaped in URI representation 275 sb.append('"'); 276 numParams++; 277 } 278 } 279 280 // Serialise scope 281 if (getScope() != null) { 282 283 if (numParams > 0) 284 sb.append(','); 285 286 sb.append(" scope=\""); 287 sb.append(getScope().toString()); 288 sb.append('"'); 289 } 290 291 return sb.toString(); 292 } 293 294 295 /** 296 * Parses an OAuth 2.0 generic token scheme error from the specified 297 * HTTP response {@code WWW-Authenticate} header. 298 * 299 * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 300 * Must not be {@code null}. 301 * @param scheme The token scheme. Must not be {@code null}. 302 * 303 * @return The generic token scheme error. 304 * 305 * @throws ParseException If the {@code WWW-Authenticate} header value 306 * couldn't be parsed to a generic token scheme 307 * error. 308 */ 309 static TokenSchemeError parse(final String wwwAuth, 310 final AccessTokenType scheme) 311 throws ParseException { 312 313 // We must have a WWW-Authenticate header set to <Scheme> .* 314 if (! wwwAuth.regionMatches(true, 0, scheme.getValue(), 0, scheme.getValue().length())) 315 throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 DPoP"); 316 317 Matcher m; 318 319 // Parse optional realm 320 m = REALM_PATTERN.matcher(wwwAuth); 321 322 String realm = null; 323 324 if (m.find()) 325 realm = m.group(1); 326 327 if (realm != null) 328 realm = realm.replace("\\\"", "\""); // strip escaped double quotes 329 330 331 // Parse optional error 332 String errorCode = null; 333 String errorDescription = null; 334 URI errorURI = null; 335 336 m = ERROR_PATTERN.matcher(wwwAuth); 337 338 if (m.find()) { 339 340 // Error code: try group with double quotes, else group with no quotes 341 errorCode = m.group(2) != null ? m.group(2) : m.group(3); 342 343 if (! ErrorObject.isLegal(errorCode)) 344 errorCode = null; // found invalid chars 345 346 // Parse optional error description 347 m = ERROR_DESCRIPTION_PATTERN.matcher(wwwAuth); 348 349 if (m.find()) 350 errorDescription = m.group(1); 351 352 353 // Parse optional error URI 354 m = ERROR_URI_PATTERN.matcher(wwwAuth); 355 356 if (m.find()) { 357 try { 358 errorURI = new URI(m.group(1)); 359 } catch (URISyntaxException e) { 360 // ignore, URI is not required to construct error object 361 } 362 } 363 } 364 365 366 Scope scope = null; 367 368 m = SCOPE_PATTERN.matcher(wwwAuth); 369 370 if (m.find()) 371 scope = Scope.parse(m.group(1)); 372 373 374 return new TokenSchemeError(AccessTokenType.UNKNOWN, errorCode, errorDescription, 0, errorURI, realm, scope) { 375 376 private static final long serialVersionUID = -1629382220440634919L; 377 378 379 @Override 380 public TokenSchemeError setDescription(String description) { 381 return null; 382 } 383 384 385 @Override 386 public TokenSchemeError appendDescription(String text) { 387 return null; 388 } 389 390 391 @Override 392 public TokenSchemeError setHTTPStatusCode(int httpStatusCode) { 393 return null; 394 } 395 396 397 @Override 398 public TokenSchemeError setURI(URI uri) { 399 return null; 400 } 401 402 403 @Override 404 public TokenSchemeError setRealm(String realm) { 405 return null; 406 } 407 408 409 @Override 410 public TokenSchemeError setScope(Scope scope) { 411 return null; 412 } 413 }; 414 } 415}