Coverage Report - org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
 
Classes in this File Line Coverage Branch Coverage Complexity
BasicHttpAuthenticationFilter
42%
23/54
30%
9/30
2.2
 
 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.codec.Base64;
 23  
 import org.apache.shiro.web.util.WebUtils;
 24  
 import org.slf4j.Logger;
 25  
 import org.slf4j.LoggerFactory;
 26  
 
 27  
 import javax.servlet.ServletRequest;
 28  
 import javax.servlet.ServletResponse;
 29  
 import javax.servlet.http.HttpServletRequest;
 30  
 import javax.servlet.http.HttpServletResponse;
 31  
 import java.util.Locale;
 32  
 
 33  
 
 34  
 /**
 35  
  * Requires the requesting user to be {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated} for the
 36  
  * request to continue, and if they're not, requires the user to login via the HTTP Basic protocol-specific challenge.
 37  
  * Upon successful login, they're allowed to continue on to the requested resource/url.
 38  
  * <p/>
 39  
  * This implementation is a 'clean room' Java implementation of Basic HTTP Authentication specification per
 40  
  * <a href="ftp://ftp.isi.edu/in-notes/rfc2617.txt">RFC 2617</a>.
 41  
  * <p/>
 42  
  * Basic authentication functions as follows:
 43  
  * <ol>
 44  
  * <li>A request comes in for a resource that requires authentication.</li>
 45  
  * <li>The server replies with a 401 response status, sets the <code>WWW-Authenticate</code> header, and the contents of a
 46  
  * page informing the user that the incoming resource requires authentication.</li>
 47  
  * <li>Upon receiving this <code>WWW-Authenticate</code> challenge from the server, the client then takes a
 48  
  * username and a password and puts them in the following format:
 49  
  * <p><code>username:password</code></p></li>
 50  
  * <li>This token is then base 64 encoded.</li>
 51  
  * <li>The client then sends another request for the same resource with the following header:<br/>
 52  
  * <p><code>Authorization: Basic <em>Base64_encoded_username_and_password</em></code></p></li>
 53  
  * </ol>
 54  
  * The {@link #onAccessDenied(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method will
 55  
  * only be called if the subject making the request is not
 56  
  * {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated}
 57  
  *
 58  
  * @see <a href="ftp://ftp.isi.edu/in-notes/rfc2617.txt">RFC 2617</a>
 59  
  * @see <a href="http://en.wikipedia.org/wiki/Basic_access_authentication">Basic Access Authentication</a>
 60  
  * @since 0.9
 61  
  */
 62  51
 public class BasicHttpAuthenticationFilter extends AuthenticatingFilter {
 63  
 
 64  
     /**
 65  
      * This class's private logger.
 66  
      */
 67  1
     private static final Logger log = LoggerFactory.getLogger(BasicHttpAuthenticationFilter.class);
 68  
 
 69  
     /**
 70  
      * HTTP Authorization header, equal to <code>Authorization</code>
 71  
      */
 72  
     protected static final String AUTHORIZATION_HEADER = "Authorization";
 73  
 
 74  
     /**
 75  
      * HTTP Authentication header, equal to <code>WWW-Authenticate</code>
 76  
      */
 77  
     protected static final String AUTHENTICATE_HEADER = "WWW-Authenticate";
 78  
 
 79  
     /**
 80  
      * The name that is displayed during the challenge process of authentication, defauls to <code>application</code>
 81  
      * and can be overridden by the {@link #setApplicationName(String) setApplicationName} method.
 82  
      */
 83  51
     private String applicationName = "application";
 84  
 
 85  
     /**
 86  
      * The authcScheme to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code>
 87  
      */
 88  51
     private String authcScheme = HttpServletRequest.BASIC_AUTH;
 89  
 
 90  
     /**
 91  
      * The authzScheme value to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code>
 92  
      */
 93  51
     private String authzScheme = HttpServletRequest.BASIC_AUTH;
 94  
 
 95  
     /**
 96  
      * Returns the name to use in the ServletResponse's <b><code>WWW-Authenticate</code></b> header.
 97  
      * <p/>
 98  
      * Per RFC 2617, this name name is displayed to the end user when they are asked to authenticate.  Unless overridden
 99  
      * by the {@link #setApplicationName(String) setApplicationName(String)} method, the default value is 'application'.
 100  
      * <p/>
 101  
      * Please see {@link #setApplicationName(String) setApplicationName(String)} for an example of how this functions.
 102  
      *
 103  
      * @return the name to use in the ServletResponse's 'WWW-Authenticate' header.
 104  
      */
 105  
     public String getApplicationName() {
 106  0
         return applicationName;
 107  
     }
 108  
 
 109  
     /**
 110  
      * Sets the name to use in the ServletResponse's <b><code>WWW-Authenticate</code></b> header.
 111  
      * <p/>
 112  
      * Per RFC 2617, this name name is displayed to the end user when they are asked to authenticate.  Unless overridden
 113  
      * by this method, the default value is &quot;application&quot;
 114  
      * <p/>
 115  
      * For example, setting this property to the value <b><code>Awesome Webapp</code></b> will result in the
 116  
      * following header:
 117  
      * <p/>
 118  
      * <code>WWW-Authenticate: Basic realm=&quot;<b>Awesome Webapp</b>&quot;</code>
 119  
      * <p/>
 120  
      * Side note: As you can see from the header text, the HTTP Basic specification calls
 121  
      * this the authentication 'realm', but we call this the 'applicationName' instead to avoid confusion with
 122  
      * Shiro's Realm constructs.
 123  
      *
 124  
      * @param applicationName the name to use in the ServletResponse's 'WWW-Authenticate' header.
 125  
      */
 126  
     public void setApplicationName(String applicationName) {
 127  0
         this.applicationName = applicationName;
 128  0
     }
 129  
 
 130  
     /**
 131  
      * Returns the HTTP <b><code>Authorization</code></b> header value that this filter will respond to as indicating
 132  
      * a login request.
 133  
      * <p/>
 134  
      * Unless overridden by the {@link #setAuthzScheme(String) setAuthzScheme(String)} method, the
 135  
      * default value is <code>BASIC</code>.
 136  
      *
 137  
      * @return the Http 'Authorization' header value that this filter will respond to as indicating a login request
 138  
      */
 139  
     public String getAuthzScheme() {
 140  0
         return authzScheme;
 141  
     }
 142  
 
 143  
     /**
 144  
      * Sets the HTTP <b><code>Authorization</code></b> header value that this filter will respond to as indicating a
 145  
      * login request.
 146  
      * <p/>
 147  
      * Unless overridden by this method, the default value is <code>BASIC</code>
 148  
      *
 149  
      * @param authzScheme the HTTP <code>Authorization</code> header value that this filter will respond to as
 150  
      *                    indicating a login request.
 151  
      */
 152  
     public void setAuthzScheme(String authzScheme) {
 153  0
         this.authzScheme = authzScheme;
 154  0
     }
 155  
 
 156  
     /**
 157  
      * Returns the HTTP <b><code>WWW-Authenticate</code></b> header scheme that this filter will use when sending
 158  
      * the HTTP Basic challenge response.  The default value is <code>BASIC</code>.
 159  
      *
 160  
      * @return the HTTP <code>WWW-Authenticate</code> header scheme that this filter will use when sending the HTTP
 161  
      *         Basic challenge response.
 162  
      * @see #sendChallenge
 163  
      */
 164  
     public String getAuthcScheme() {
 165  0
         return authcScheme;
 166  
     }
 167  
 
 168  
     /**
 169  
      * Sets the HTTP <b><code>WWW-Authenticate</code></b> header scheme that this filter will use when sending the
 170  
      * HTTP Basic challenge response.  The default value is <code>BASIC</code>.
 171  
      *
 172  
      * @param authcScheme the HTTP <code>WWW-Authenticate</code> header scheme that this filter will use when
 173  
      *                    sending the Http Basic challenge response.
 174  
      * @see #sendChallenge
 175  
      */
 176  
     public void setAuthcScheme(String authcScheme) {
 177  0
         this.authcScheme = authcScheme;
 178  0
     }
 179  
 
 180  
     /**
 181  
      * Processes unauthenticated requests. It handles the two-stage request/challenge authentication protocol.
 182  
      *
 183  
      * @param request  incoming ServletRequest
 184  
      * @param response outgoing ServletResponse
 185  
      * @return true if the request should be processed; false if the request should not continue to be processed
 186  
      */
 187  
     protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
 188  0
         boolean loggedIn = false; //false by default or we wouldn't be in this method
 189  0
         if (isLoginAttempt(request, response)) {
 190  0
             loggedIn = executeLogin(request, response);
 191  
         }
 192  0
         if (!loggedIn) {
 193  0
             sendChallenge(request, response);
 194  
         }
 195  0
         return loggedIn;
 196  
     }
 197  
 
 198  
     /**
 199  
      * Determines whether the incoming request is an attempt to log in.
 200  
      * <p/>
 201  
      * The default implementation obtains the value of the request's
 202  
      * {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER}, and if it is not <code>null</code>, delegates
 203  
      * to {@link #isLoginAttempt(String) isLoginAttempt(authzHeaderValue)}. If the header is <code>null</code>,
 204  
      * <code>false</code> is returned.
 205  
      *
 206  
      * @param request  incoming ServletRequest
 207  
      * @param response outgoing ServletResponse
 208  
      * @return true if the incoming request is an attempt to log in based, false otherwise
 209  
      */
 210  
     protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
 211  0
         String authzHeader = getAuthzHeader(request);
 212  0
         return authzHeader != null && isLoginAttempt(authzHeader);
 213  
     }
 214  
 
 215  
     /**
 216  
      * Delegates to {@link #isLoginAttempt(javax.servlet.ServletRequest, javax.servlet.ServletResponse) isLoginAttempt}.
 217  
      */
 218  
     @Override
 219  
     protected final boolean isLoginRequest(ServletRequest request, ServletResponse response) {
 220  0
         return this.isLoginAttempt(request, response);
 221  
     }
 222  
 
 223  
     /**
 224  
      * Returns the {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER} from the specified ServletRequest.
 225  
      * <p/>
 226  
      * This implementation merely casts the request to an <code>HttpServletRequest</code> and returns the header:
 227  
      * <p/>
 228  
      * <code>HttpServletRequest httpRequest = {@link WebUtils#toHttp(javax.servlet.ServletRequest) toHttp(reaquest)};<br/>
 229  
      * return httpRequest.getHeader({@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER});</code>
 230  
      *
 231  
      * @param request the incoming <code>ServletRequest</code>
 232  
      * @return the <code>Authorization</code> header's value.
 233  
      */
 234  
     protected String getAuthzHeader(ServletRequest request) {
 235  4
         HttpServletRequest httpRequest = WebUtils.toHttp(request);
 236  4
         return httpRequest.getHeader(AUTHORIZATION_HEADER);
 237  
     }
 238  
 
 239  
     /**
 240  
      * Default implementation that returns <code>true</code> if the specified <code>authzHeader</code>
 241  
      * starts with the same (case-insensitive) characters specified by the
 242  
      * {@link #getAuthzScheme() authzScheme}, <code>false</code> otherwise.
 243  
      * <p/>
 244  
      * That is:
 245  
      * <p/>
 246  
      * <code>String authzScheme = getAuthzScheme().toLowerCase();<br/>
 247  
      * return authzHeader.toLowerCase().startsWith(authzScheme);</code>
 248  
      *
 249  
      * @param authzHeader the 'Authorization' header value (guaranteed to be non-null if the
 250  
      *                    {@link #isLoginAttempt(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method is not overriden).
 251  
      * @return <code>true</code> if the authzHeader value matches that configured as defined by
 252  
      *         the {@link #getAuthzScheme() authzScheme}.
 253  
      */
 254  
     protected boolean isLoginAttempt(String authzHeader) {
 255  
         //SHIRO-415: use English Locale:
 256  0
         String authzScheme = getAuthzScheme().toLowerCase(Locale.ENGLISH);
 257  0
         return authzHeader.toLowerCase(Locale.ENGLISH).startsWith(authzScheme);
 258  
     }
 259  
 
 260  
     /**
 261  
      * Builds the challenge for authorization by setting a HTTP <code>401</code> (Unauthorized) status as well as the
 262  
      * response's {@link #AUTHENTICATE_HEADER AUTHENTICATE_HEADER}.
 263  
      * <p/>
 264  
      * The header value constructed is equal to:
 265  
      * <p/>
 266  
      * <code>{@link #getAuthcScheme() getAuthcScheme()} + " realm=\"" + {@link #getApplicationName() getApplicationName()} + "\"";</code>
 267  
      *
 268  
      * @param request  incoming ServletRequest, ignored by this implementation
 269  
      * @param response outgoing ServletResponse
 270  
      * @return false - this sends the challenge to be sent back
 271  
      */
 272  
     protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
 273  0
         if (log.isDebugEnabled()) {
 274  0
             log.debug("Authentication required: sending 401 Authentication challenge response.");
 275  
         }
 276  0
         HttpServletResponse httpResponse = WebUtils.toHttp(response);
 277  0
         httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
 278  0
         String authcHeader = getAuthcScheme() + " realm=\"" + getApplicationName() + "\"";
 279  0
         httpResponse.setHeader(AUTHENTICATE_HEADER, authcHeader);
 280  0
         return false;
 281  
     }
 282  
 
 283  
     /**
 284  
      * Creates an AuthenticationToken for use during login attempt with the provided credentials in the http header.
 285  
      * <p/>
 286  
      * This implementation:
 287  
      * <ol><li>acquires the username and password based on the request's
 288  
      * {@link #getAuthzHeader(javax.servlet.ServletRequest) authorization header} via the
 289  
      * {@link #getPrincipalsAndCredentials(String, javax.servlet.ServletRequest) getPrincipalsAndCredentials} method</li>
 290  
      * <li>The return value of that method is converted to an <code>AuthenticationToken</code> via the
 291  
      * {@link #createToken(String, String, javax.servlet.ServletRequest, javax.servlet.ServletResponse) createToken} method</li>
 292  
      * <li>The created <code>AuthenticationToken</code> is returned.</li>
 293  
      * </ol>
 294  
      *
 295  
      * @param request  incoming ServletRequest
 296  
      * @param response outgoing ServletResponse
 297  
      * @return the AuthenticationToken used to execute the login attempt
 298  
      */
 299  
     protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
 300  4
         String authorizationHeader = getAuthzHeader(request);
 301  4
         if (authorizationHeader == null || authorizationHeader.length() == 0) {
 302  
             // Create an empty authentication token since there is no
 303  
             // Authorization header.
 304  1
             return createToken("", "", request, response);
 305  
         }
 306  
 
 307  3
         if (log.isDebugEnabled()) {
 308  3
             log.debug("Attempting to execute login with headers [" + authorizationHeader + "]");
 309  
         }
 310  
 
 311  3
         String[] prinCred = getPrincipalsAndCredentials(authorizationHeader, request);
 312  3
         if (prinCred == null || prinCred.length < 2) {
 313  
             // Create an authentication token with an empty password,
 314  
             // since one hasn't been provided in the request.
 315  0
             String username = prinCred == null || prinCred.length == 0 ? "" : prinCred[0];
 316  0
             return createToken(username, "", request, response);
 317  
         }
 318  
 
 319  3
         String username = prinCred[0];
 320  3
         String password = prinCred[1];
 321  
 
 322  3
         return createToken(username, password, request, response);
 323  
     }
 324  
 
 325  
     /**
 326  
      * Returns the username obtained from the
 327  
      * {@link #getAuthzHeader(javax.servlet.ServletRequest) authorizationHeader}.
 328  
      * <p/>
 329  
      * Once the {@code authzHeader} is split per the RFC (based on the space character ' '), the resulting split tokens
 330  
      * are translated into the username/password pair by the
 331  
      * {@link #getPrincipalsAndCredentials(String, String) getPrincipalsAndCredentials(scheme,encoded)} method.
 332  
      *
 333  
      * @param authorizationHeader the authorization header obtained from the request.
 334  
      * @param request             the incoming ServletRequest
 335  
      * @return the username (index 0)/password pair (index 1) submitted by the user for the given header value and request.
 336  
      * @see #getAuthzHeader(javax.servlet.ServletRequest)
 337  
      */
 338  
     protected String[] getPrincipalsAndCredentials(String authorizationHeader, ServletRequest request) {
 339  3
         if (authorizationHeader == null) {
 340  0
             return null;
 341  
         }
 342  3
         String[] authTokens = authorizationHeader.split(" ");
 343  3
         if (authTokens == null || authTokens.length < 2) {
 344  0
             return null;
 345  
         }
 346  3
         return getPrincipalsAndCredentials(authTokens[0], authTokens[1]);
 347  
     }
 348  
 
 349  
     /**
 350  
      * Returns the username and password pair based on the specified <code>encoded</code> String obtained from
 351  
      * the request's authorization header.
 352  
      * <p/>
 353  
      * Per RFC 2617, the default implementation first Base64 decodes the string and then splits the resulting decoded
 354  
      * string into two based on the ":" character.  That is:
 355  
      * <p/>
 356  
      * <code>String decoded = Base64.decodeToString(encoded);<br/>
 357  
      * return decoded.split(":");</code>
 358  
      *
 359  
      * @param scheme  the {@link #getAuthcScheme() authcScheme} found in the request
 360  
      *                {@link #getAuthzHeader(javax.servlet.ServletRequest) authzHeader}.  It is ignored by this implementation,
 361  
      *                but available to overriding implementations should they find it useful.
 362  
      * @param encoded the Base64-encoded username:password value found after the scheme in the header
 363  
      * @return the username (index 0)/password (index 1) pair obtained from the encoded header data.
 364  
      */
 365  
     protected String[] getPrincipalsAndCredentials(String scheme, String encoded) {
 366  3
         String decoded = Base64.decodeToString(encoded);
 367  3
         return decoded.split(":", 2);
 368  
     }
 369  
 }