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 com.nimbusds.oauth2.sdk.ErrorObject; 022import com.nimbusds.oauth2.sdk.ParseException; 023import com.nimbusds.oauth2.sdk.Scope; 024 025import java.net.URI; 026import java.net.URISyntaxException; 027import java.util.Objects; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030 031 032/** 033 * The base abstract class for token scheme errors. Concrete extending classes 034 * should be immutable. 035 */ 036public abstract class TokenSchemeError extends ErrorObject { 037 038 039 private static final long serialVersionUID = -1132784406578139418L; 040 041 042 /** 043 * Regex pattern for matching the realm parameter of a WWW-Authenticate 044 * header. Limits the realm string length to 256 chars to prevent 045 * potential stack overflow exception for very long strings due to 046 * recursive nature of regex. 047 */ 048 static final Pattern REALM_PATTERN = Pattern.compile("realm=\"(([^\\\\\"]|\\\\.){0,256})\""); 049 050 051 /** 052 * Regex pattern for matching the error parameter of a WWW-Authenticate 053 * header. Double quoting is optional. 054 */ 055 static final Pattern ERROR_PATTERN = Pattern.compile("error=(\"([\\w\\_-]+)\"|([\\w\\_-]+))"); 056 057 058 /** 059 * Regex pattern for matching the error description parameter of a 060 * WWW-Authenticate header. 061 */ 062 static final Pattern ERROR_DESCRIPTION_PATTERN = Pattern.compile("error_description=\"([^\"]+)\""); 063 064 065 /** 066 * Regex pattern for matching the error URI parameter of a 067 * WWW-Authenticate header. 068 */ 069 static final Pattern ERROR_URI_PATTERN = Pattern.compile("error_uri=\"([^\"]+)\""); 070 071 072 /** 073 * Regex pattern for matching the scope parameter of a WWW-Authenticate 074 * header. 075 */ 076 static final Pattern SCOPE_PATTERN = Pattern.compile("scope=\"([^\"]+)"); 077 078 079 /** 080 * The token scheme. 081 */ 082 private final AccessTokenType scheme; 083 084 085 /** 086 * The realm, {@code null} if not specified. 087 */ 088 private final String realm; 089 090 091 /** 092 * Required scope, {@code null} if not specified. 093 */ 094 private final Scope scope; 095 096 097 /** 098 * Returns {@code true} if the specified scope consists of valid 099 * characters. Values for the "scope" attributes must not include 100 * characters outside the [0x20, 0x21] | [0x23 - 0x5B] | [0x5D - 0x7E] 101 * range. See RFC 6750, section 3. 102 * 103 * @see ErrorObject#isLegal(String) 104 * 105 * @param scope The scope. 106 * 107 * @return {@code true} if the scope contains valid characters, else 108 * {@code false}. 109 */ 110 public static boolean isScopeWithValidChars(final Scope scope) { 111 112 return ErrorObject.isLegal(scope.toString()); 113 } 114 115 116 /** 117 * Creates a new token error with the specified code, description, HTTP 118 * status code, page URI, realm and scope. 119 * 120 * @param scheme The token scheme. Must not be {@code null}. 121 * @param code The error code, {@code null} if not specified. 122 * @param description The error description, {@code null} if not 123 * specified. 124 * @param httpStatusCode The HTTP status code, zero if not specified. 125 * @param uri The error page URI, {@code null} if not 126 * specified. 127 * @param realm The realm, {@code null} if not specified. 128 * @param scope The required scope, {@code null} if not 129 * specified. 130 */ 131 protected TokenSchemeError(final AccessTokenType scheme, 132 final String code, 133 final String description, 134 final int httpStatusCode, 135 final URI uri, 136 final String realm, 137 final Scope scope) { 138 139 super(code, description, httpStatusCode, uri); 140 141 this.scheme = Objects.requireNonNull(scheme); 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 373 private static final long serialVersionUID = -1629382220440634919L; 374 375 376 @Override 377 public TokenSchemeError setDescription(String description) { 378 return null; 379 } 380 381 382 @Override 383 public TokenSchemeError appendDescription(String text) { 384 return null; 385 } 386 387 388 @Override 389 public TokenSchemeError setHTTPStatusCode(int httpStatusCode) { 390 return null; 391 } 392 393 394 @Override 395 public TokenSchemeError setURI(URI uri) { 396 return null; 397 } 398 399 400 @Override 401 public TokenSchemeError setRealm(String realm) { 402 return null; 403 } 404 405 406 @Override 407 public TokenSchemeError setScope(Scope scope) { 408 return null; 409 } 410 }; 411 } 412}