1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 package org.apache.shiro.web.filter.authc; 20 21 import org.apache.shiro.authc.AuthenticationToken; 22 import org.apache.shiro.web.util.WebUtils; 23 import org.slf4j.Logger; 24 import org.slf4j.LoggerFactory; 25 26 import javax.servlet.ServletRequest; 27 import javax.servlet.ServletResponse; 28 import javax.servlet.http.HttpServletRequest; 29 import javax.servlet.http.HttpServletResponse; 30 import java.util.HashSet; 31 import java.util.Locale; 32 import java.util.Set; 33 34 35 /** 36 * Requires the requesting user to be {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated} for the 37 * request to continue, and if they're not, requires the user to login via the HTTP "Authentication" header (e.g. BASIC, Bearer, etc.) 38 * Upon successful login, they're allowed to continue on to the requested resource/url. 39 * <p/> 40 * The {@link #onAccessDenied(ServletRequest, ServletResponse)} method will 41 * only be called if the subject making the request is not 42 * {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated} 43 * 44 * @see <a href="https://tools.ietf.org/html/rfc2617">RFC 2617</a> 45 * @see <a href="http://en.wikipedia.org/wiki/Basic_access_authentication">Basic Access Authentication</a> 46 * @see <a href="https://tools.ietf.org/html/rfc6750#section-2.1">OAuth2 Authorization Request Header Field</a> 47 * @since 1.5 48 */ 49 abstract class HttpAuthenticationFilter extends AuthenticatingFilter { 50 51 /** 52 * This class's private logger. 53 */ 54 private static final Logger log = LoggerFactory.getLogger(HttpAuthenticationFilter.class); 55 56 /** 57 * HTTP Authorization header, equal to <code>Authorization</code> 58 */ 59 protected static final String AUTHORIZATION_HEADER = "Authorization"; 60 61 /** 62 * HTTP Authentication header, equal to <code>WWW-Authenticate</code> 63 */ 64 protected static final String AUTHENTICATE_HEADER = "WWW-Authenticate"; 65 66 /** 67 * The name that is displayed during the challenge process of authentication, defauls to <code>application</code> 68 * and can be overridden by the {@link #setApplicationName(String) setApplicationName} method. 69 */ 70 private String applicationName = "application"; 71 72 /** 73 * The authcScheme to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code> 74 */ 75 private String authcScheme; 76 77 /** 78 * The authzScheme value to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code> 79 */ 80 private String authzScheme; 81 82 /** 83 * Returns the name to use in the ServletResponse's <b><code>WWW-Authenticate</code></b> header. 84 * <p/> 85 * Per RFC 2617, this name name is displayed to the end user when they are asked to authenticate. Unless overridden 86 * by the {@link #setApplicationName(String) setApplicationName(String)} method, the default value is 'application'. 87 * <p/> 88 * Please see {@link #setApplicationName(String) setApplicationName(String)} for an example of how this functions. 89 * 90 * @return the name to use in the ServletResponse's 'WWW-Authenticate' header. 91 */ 92 public String getApplicationName() { 93 return applicationName; 94 } 95 96 /** 97 * Sets the name to use in the ServletResponse's <b><code>WWW-Authenticate</code></b> header. 98 * <p/> 99 * Per RFC 2617, this name name is displayed to the end user when they are asked to authenticate. Unless overridden 100 * by this method, the default value is "application" 101 * <p/> 102 * For example, setting this property to the value <b><code>Awesome Webapp</code></b> will result in the 103 * following header: 104 * <p/> 105 * <code>WWW-Authenticate: Basic realm="<b>Awesome Webapp</b>"</code> 106 * <p/> 107 * Side note: As you can see from the header text, the HTTP Basic specification calls 108 * this the authentication 'realm', but we call this the 'applicationName' instead to avoid confusion with 109 * Shiro's Realm constructs. 110 * 111 * @param applicationName the name to use in the ServletResponse's 'WWW-Authenticate' header. 112 */ 113 public void setApplicationName(String applicationName) { 114 this.applicationName = applicationName; 115 } 116 117 /** 118 * Returns the HTTP <b><code>Authorization</code></b> header value that this filter will respond to as indicating 119 * a login request. 120 * <p/> 121 * Unless overridden by the {@link #setAuthzScheme(String) setAuthzScheme(String)} method, the 122 * default value is <code>BASIC</code>. 123 * 124 * @return the Http 'Authorization' header value that this filter will respond to as indicating a login request 125 */ 126 public String getAuthzScheme() { 127 return authzScheme; 128 } 129 130 /** 131 * Sets the HTTP <b><code>Authorization</code></b> header value that this filter will respond to as indicating a 132 * login request. 133 * <p/> 134 * Unless overridden by this method, the default value is <code>BASIC</code> 135 * 136 * @param authzScheme the HTTP <code>Authorization</code> header value that this filter will respond to as 137 * indicating a login request. 138 */ 139 public void setAuthzScheme(String authzScheme) { 140 this.authzScheme = authzScheme; 141 } 142 143 /** 144 * Returns the HTTP <b><code>WWW-Authenticate</code></b> header scheme that this filter will use when sending 145 * the HTTP Basic challenge response. The default value is <code>BASIC</code>. 146 * 147 * @return the HTTP <code>WWW-Authenticate</code> header scheme that this filter will use when sending the HTTP 148 * Basic challenge response. 149 * @see #sendChallenge 150 */ 151 public String getAuthcScheme() { 152 return authcScheme; 153 } 154 155 /** 156 * Sets the HTTP <b><code>WWW-Authenticate</code></b> header scheme that this filter will use when sending the 157 * HTTP Basic challenge response. The default value is <code>BASIC</code>. 158 * 159 * @param authcScheme the HTTP <code>WWW-Authenticate</code> header scheme that this filter will use when 160 * sending the Http Basic challenge response. 161 * @see #sendChallenge 162 */ 163 public void setAuthcScheme(String authcScheme) { 164 this.authcScheme = authcScheme; 165 } 166 167 /** 168 * The Basic authentication filter can be configured with a list of HTTP methods to which it should apply. This 169 * method ensures that authentication is <em>only</em> required for those HTTP methods specified. For example, 170 * if you had the configuration: 171 * <pre> 172 * [urls] 173 * /basic/** = authcBasic[POST,PUT,DELETE] 174 * </pre> 175 * then a GET request would not required authentication but a POST would. 176 * @param request The current HTTP servlet request. 177 * @param response The current HTTP servlet response. 178 * @param mappedValue The array of configured HTTP methods as strings. This is empty if no methods are configured. 179 */ 180 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { 181 HttpServletRequest httpRequest = WebUtils.toHttp(request); 182 String httpMethod = httpRequest.getMethod(); 183 184 // Check whether the current request's method requires authentication. 185 // If no methods have been configured, then all of them require auth, 186 // otherwise only the declared ones need authentication. 187 188 Set<String> methods = httpMethodsFromOptions((String[])mappedValue); 189 boolean authcRequired = methods.size() == 0; 190 for (String m : methods) { 191 if (httpMethod.toUpperCase(Locale.ENGLISH).equals(m)) { // list of methods is in upper case 192 authcRequired = true; 193 break; 194 } 195 } 196 197 if (authcRequired) { 198 return super.isAccessAllowed(request, response, mappedValue); 199 } 200 else { 201 return true; 202 } 203 } 204 205 private Set<String> httpMethodsFromOptions(String[] options) { 206 Set<String> methods = new HashSet<String>(); 207 208 if (options != null) { 209 for (String option : options) { 210 // to be backwards compatible with 1.3, we can ONLY check for known args 211 // ideally we would just validate HTTP methods, but someone could already be using this for webdav 212 if (!option.equalsIgnoreCase(PERMISSIVE)) { 213 methods.add(option.toUpperCase(Locale.ENGLISH)); 214 } 215 } 216 } 217 return methods; 218 } 219 220 /** 221 * Processes unauthenticated requests. It handles the two-stage request/challenge authentication protocol. 222 * 223 * @param request incoming ServletRequest 224 * @param response outgoing ServletResponse 225 * @return true if the request should be processed; false if the request should not continue to be processed 226 */ 227 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { 228 boolean loggedIn = false; //false by default or we wouldn't be in this method 229 if (isLoginAttempt(request, response)) { 230 loggedIn = executeLogin(request, response); 231 } 232 if (!loggedIn) { 233 sendChallenge(request, response); 234 } 235 return loggedIn; 236 } 237 238 /** 239 * Determines whether the incoming request is an attempt to log in. 240 * <p/> 241 * The default implementation obtains the value of the request's 242 * {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER}, and if it is not <code>null</code>, delegates 243 * to {@link #isLoginAttempt(String) isLoginAttempt(authzHeaderValue)}. If the header is <code>null</code>, 244 * <code>false</code> is returned. 245 * 246 * @param request incoming ServletRequest 247 * @param response outgoing ServletResponse 248 * @return true if the incoming request is an attempt to log in based, false otherwise 249 */ 250 protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { 251 String authzHeader = getAuthzHeader(request); 252 return authzHeader != null && isLoginAttempt(authzHeader); 253 } 254 255 /** 256 * Delegates to {@link #isLoginAttempt(ServletRequest, ServletResponse) isLoginAttempt}. 257 */ 258 @Override 259 protected final boolean isLoginRequest(ServletRequest request, ServletResponse response) { 260 return this.isLoginAttempt(request, response); 261 } 262 263 /** 264 * Returns the {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER} from the specified ServletRequest. 265 * <p/> 266 * This implementation merely casts the request to an <code>HttpServletRequest</code> and returns the header: 267 * <p/> 268 * <code>HttpServletRequest httpRequest = {@link WebUtils#toHttp(ServletRequest) toHttp(reaquest)};<br/> 269 * return httpRequest.getHeader({@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER});</code> 270 * 271 * @param request the incoming <code>ServletRequest</code> 272 * @return the <code>Authorization</code> header's value. 273 */ 274 protected String getAuthzHeader(ServletRequest request) { 275 HttpServletRequest httpRequest = WebUtils.toHttp(request); 276 return httpRequest.getHeader(AUTHORIZATION_HEADER); 277 } 278 279 /** 280 * Default implementation that returns <code>true</code> if the specified <code>authzHeader</code> 281 * starts with the same (case-insensitive) characters specified by the 282 * {@link #getAuthzScheme() authzScheme}, <code>false</code> otherwise. 283 * <p/> 284 * That is: 285 * <p/> 286 * <code>String authzScheme = getAuthzScheme().toLowerCase();<br/> 287 * return authzHeader.toLowerCase().startsWith(authzScheme);</code> 288 * 289 * @param authzHeader the 'Authorization' header value (guaranteed to be non-null if the 290 * {@link #isLoginAttempt(ServletRequest, ServletResponse)} method is not overriden). 291 * @return <code>true</code> if the authzHeader value matches that configured as defined by 292 * the {@link #getAuthzScheme() authzScheme}. 293 */ 294 protected boolean isLoginAttempt(String authzHeader) { 295 //SHIRO-415: use English Locale: 296 String authzScheme = getAuthzScheme().toLowerCase(Locale.ENGLISH); 297 return authzHeader.toLowerCase(Locale.ENGLISH).startsWith(authzScheme); 298 } 299 300 /** 301 * Builds the challenge for authorization by setting a HTTP <code>401</code> (Unauthorized) status as well as the 302 * response's {@link #AUTHENTICATE_HEADER AUTHENTICATE_HEADER}. 303 * <p/> 304 * The header value constructed is equal to: 305 * <p/> 306 * <code>{@link #getAuthcScheme() getAuthcScheme()} + " realm=\"" + {@link #getApplicationName() getApplicationName()} + "\"";</code> 307 * 308 * @param request incoming ServletRequest, ignored by this implementation 309 * @param response outgoing ServletResponse 310 * @return false - this sends the challenge to be sent back 311 */ 312 protected boolean sendChallenge(ServletRequest request, ServletResponse response) { 313 log.debug("Authentication required: sending 401 Authentication challenge response."); 314 315 HttpServletResponse httpResponse = WebUtils.toHttp(response); 316 httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 317 String authcHeader = getAuthcScheme() + " realm=\"" + getApplicationName() + "\""; 318 httpResponse.setHeader(AUTHENTICATE_HEADER, authcHeader); 319 return false; 320 } 321 322 /** 323 * Creates an AuthenticationToken for use during login attempt with the provided credentials in the http header. 324 * <p/> 325 * This implementation: 326 * <ol><li>acquires the username and password based on the request's 327 * {@link #getAuthzHeader(ServletRequest) authorization header} via the 328 * {@link #getPrincipalsAndCredentials(String, ServletRequest) getPrincipalsAndCredentials} method</li> 329 * <li>The return value of that method is converted to an <code>AuthenticationToken</code> via the 330 * {@link #createToken(String, String, ServletRequest, ServletResponse) createToken} method</li> 331 * <li>The created <code>AuthenticationToken</code> is returned.</li> 332 * </ol> 333 * 334 * @param request incoming ServletRequest 335 * @param response outgoing ServletResponse 336 * @return the AuthenticationToken used to execute the login attempt 337 */ 338 protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) { 339 String authorizationHeader = getAuthzHeader(request); 340 if (authorizationHeader == null || authorizationHeader.length() == 0) { 341 // Create an empty authentication token since there is no 342 // Authorization header. 343 return createToken("", "", request, response); 344 } 345 346 log.debug("Attempting to execute login with auth header"); 347 348 String[] prinCred = getPrincipalsAndCredentials(authorizationHeader, request); 349 if (prinCred == null || prinCred.length < 2) { 350 // Create an authentication token with an empty password, 351 // since one hasn't been provided in the request. 352 String username = prinCred == null || prinCred.length == 0 ? "" : prinCred[0]; 353 return createToken(username, "", request, response); 354 } 355 356 String username = prinCred[0]; 357 String password = prinCred[1]; 358 359 return createToken(username, password, request, response); 360 } 361 362 /** 363 * Returns the username obtained from the 364 * {@link #getAuthzHeader(ServletRequest) authorizationHeader}. 365 * <p/> 366 * Once the {@code authzHeader} is split per the RFC (based on the space character ' '), the resulting split tokens 367 * are translated into the username/password pair by the 368 * {@link #getPrincipalsAndCredentials(String, String) getPrincipalsAndCredentials(scheme,encoded)} method. 369 * 370 * @param authorizationHeader the authorization header obtained from the request. 371 * @param request the incoming ServletRequest 372 * @return the username (index 0)/password pair (index 1) submitted by the user for the given header value and request. 373 * @see #getAuthzHeader(ServletRequest) 374 */ 375 protected String[] getPrincipalsAndCredentials(String authorizationHeader, ServletRequest request) { 376 if (authorizationHeader == null) { 377 return null; 378 } 379 String[] authTokens = authorizationHeader.split(" "); 380 if (authTokens == null || authTokens.length < 2) { 381 return null; 382 } 383 return getPrincipalsAndCredentials(authTokens[0], authTokens[1]); 384 } 385 386 /** 387 * Returns a String[] containing credential parts parsed fom the "Authorization" header. 388 * 389 * @param scheme the {@link #getAuthcScheme() authcScheme} found in the request 390 * {@link #getAuthzHeader(ServletRequest) authzHeader}. It is ignored by this implementation, 391 * but available to overriding implementations should they find it useful. 392 * @param value the raw string value from the "Authorization" header. 393 */ 394 abstract String[] getPrincipalsAndCredentials(String scheme, String value); 395 }