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