View Javadoc

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  public class BasicHttpAuthenticationFilter extends AuthenticatingFilter {
63  
64      /**
65       * This class's private logger.
66       */
67      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      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      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      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         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         this.applicationName = applicationName;
128     }
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         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         this.authzScheme = authzScheme;
154     }
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         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         this.authcScheme = authcScheme;
178     }
179     
180     /**
181      * The Basic authentication filter can be configured with a list of HTTP methods to which it should apply. This
182      * method ensures that authentication is <em>only</em> required for those HTTP methods specified. For example,
183      * if you had the configuration:
184      * <pre>
185      *    [urls]
186      *    /basic/** = authcBasic[POST,PUT,DELETE]
187      * </pre>
188      * then a GET request would not required authentication but a POST would.
189      * @param request The current HTTP servlet request.
190      * @param response The current HTTP servlet response.
191      * @param mappedValue The array of configured HTTP methods as strings. This is empty if no methods are configured.
192      */
193     protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
194         HttpServletRequest httpRequest = WebUtils.toHttp(request);
195         String httpMethod = httpRequest.getMethod();
196         
197         // Check whether the current request's method requires authentication.
198         // If no methods have been configured, then all of them require auth,
199         // otherwise only the declared ones need authentication.
200         String[] methods = (String[]) (mappedValue == null ? new String[0] : mappedValue);
201         boolean authcRequired = methods.length == 0;
202         for (String m : methods) {
203             if (httpMethod.equalsIgnoreCase(m)) {
204                 authcRequired = true;
205                 break;
206             }
207         }
208         
209         if (authcRequired) {
210             return super.isAccessAllowed(request, response, mappedValue);
211         }
212         else {
213             return true;
214         }
215     }
216 
217     /**
218      * Processes unauthenticated requests. It handles the two-stage request/challenge authentication protocol.
219      *
220      * @param request  incoming ServletRequest
221      * @param response outgoing ServletResponse
222      * @return true if the request should be processed; false if the request should not continue to be processed
223      */
224     protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
225         boolean loggedIn = false; //false by default or we wouldn't be in this method
226         if (isLoginAttempt(request, response)) {
227             loggedIn = executeLogin(request, response);
228         }
229         if (!loggedIn) {
230             sendChallenge(request, response);
231         }
232         return loggedIn;
233     }
234 
235     /**
236      * Determines whether the incoming request is an attempt to log in.
237      * <p/>
238      * The default implementation obtains the value of the request's
239      * {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER}, and if it is not <code>null</code>, delegates
240      * to {@link #isLoginAttempt(String) isLoginAttempt(authzHeaderValue)}. If the header is <code>null</code>,
241      * <code>false</code> is returned.
242      *
243      * @param request  incoming ServletRequest
244      * @param response outgoing ServletResponse
245      * @return true if the incoming request is an attempt to log in based, false otherwise
246      */
247     protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
248         String authzHeader = getAuthzHeader(request);
249         return authzHeader != null && isLoginAttempt(authzHeader);
250     }
251 
252     /**
253      * Delegates to {@link #isLoginAttempt(javax.servlet.ServletRequest, javax.servlet.ServletResponse) isLoginAttempt}.
254      */
255     @Override
256     protected final boolean isLoginRequest(ServletRequest request, ServletResponse response) {
257         return this.isLoginAttempt(request, response);
258     }
259 
260     /**
261      * Returns the {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER} from the specified ServletRequest.
262      * <p/>
263      * This implementation merely casts the request to an <code>HttpServletRequest</code> and returns the header:
264      * <p/>
265      * <code>HttpServletRequest httpRequest = {@link WebUtils#toHttp(javax.servlet.ServletRequest) toHttp(reaquest)};<br/>
266      * return httpRequest.getHeader({@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER});</code>
267      *
268      * @param request the incoming <code>ServletRequest</code>
269      * @return the <code>Authorization</code> header's value.
270      */
271     protected String getAuthzHeader(ServletRequest request) {
272         HttpServletRequest httpRequest = WebUtils.toHttp(request);
273         return httpRequest.getHeader(AUTHORIZATION_HEADER);
274     }
275 
276     /**
277      * Default implementation that returns <code>true</code> if the specified <code>authzHeader</code>
278      * starts with the same (case-insensitive) characters specified by the
279      * {@link #getAuthzScheme() authzScheme}, <code>false</code> otherwise.
280      * <p/>
281      * That is:
282      * <p/>
283      * <code>String authzScheme = getAuthzScheme().toLowerCase();<br/>
284      * return authzHeader.toLowerCase().startsWith(authzScheme);</code>
285      *
286      * @param authzHeader the 'Authorization' header value (guaranteed to be non-null if the
287      *                    {@link #isLoginAttempt(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method is not overriden).
288      * @return <code>true</code> if the authzHeader value matches that configured as defined by
289      *         the {@link #getAuthzScheme() authzScheme}.
290      */
291     protected boolean isLoginAttempt(String authzHeader) {
292         //SHIRO-415: use English Locale:
293         String authzScheme = getAuthzScheme().toLowerCase(Locale.ENGLISH);
294         return authzHeader.toLowerCase(Locale.ENGLISH).startsWith(authzScheme);
295     }
296 
297     /**
298      * Builds the challenge for authorization by setting a HTTP <code>401</code> (Unauthorized) status as well as the
299      * response's {@link #AUTHENTICATE_HEADER AUTHENTICATE_HEADER}.
300      * <p/>
301      * The header value constructed is equal to:
302      * <p/>
303      * <code>{@link #getAuthcScheme() getAuthcScheme()} + " realm=\"" + {@link #getApplicationName() getApplicationName()} + "\"";</code>
304      *
305      * @param request  incoming ServletRequest, ignored by this implementation
306      * @param response outgoing ServletResponse
307      * @return false - this sends the challenge to be sent back
308      */
309     protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
310         if (log.isDebugEnabled()) {
311             log.debug("Authentication required: sending 401 Authentication challenge response.");
312         }
313         HttpServletResponse httpResponse = WebUtils.toHttp(response);
314         httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
315         String authcHeader = getAuthcScheme() + " realm=\"" + getApplicationName() + "\"";
316         httpResponse.setHeader(AUTHENTICATE_HEADER, authcHeader);
317         return false;
318     }
319 
320     /**
321      * Creates an AuthenticationToken for use during login attempt with the provided credentials in the http header.
322      * <p/>
323      * This implementation:
324      * <ol><li>acquires the username and password based on the request's
325      * {@link #getAuthzHeader(javax.servlet.ServletRequest) authorization header} via the
326      * {@link #getPrincipalsAndCredentials(String, javax.servlet.ServletRequest) getPrincipalsAndCredentials} method</li>
327      * <li>The return value of that method is converted to an <code>AuthenticationToken</code> via the
328      * {@link #createToken(String, String, javax.servlet.ServletRequest, javax.servlet.ServletResponse) createToken} method</li>
329      * <li>The created <code>AuthenticationToken</code> is returned.</li>
330      * </ol>
331      *
332      * @param request  incoming ServletRequest
333      * @param response outgoing ServletResponse
334      * @return the AuthenticationToken used to execute the login attempt
335      */
336     protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
337         String authorizationHeader = getAuthzHeader(request);
338         if (authorizationHeader == null || authorizationHeader.length() == 0) {
339             // Create an empty authentication token since there is no
340             // Authorization header.
341             return createToken("", "", request, response);
342         }
343 
344         if (log.isDebugEnabled()) {
345             log.debug("Attempting to execute login with headers [" + authorizationHeader + "]");
346         }
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(javax.servlet.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(javax.servlet.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 the username and password pair based on the specified <code>encoded</code> String obtained from
388      * the request's authorization header.
389      * <p/>
390      * Per RFC 2617, the default implementation first Base64 decodes the string and then splits the resulting decoded
391      * string into two based on the ":" character.  That is:
392      * <p/>
393      * <code>String decoded = Base64.decodeToString(encoded);<br/>
394      * return decoded.split(":");</code>
395      *
396      * @param scheme  the {@link #getAuthcScheme() authcScheme} found in the request
397      *                {@link #getAuthzHeader(javax.servlet.ServletRequest) authzHeader}.  It is ignored by this implementation,
398      *                but available to overriding implementations should they find it useful.
399      * @param encoded the Base64-encoded username:password value found after the scheme in the header
400      * @return the username (index 0)/password (index 1) pair obtained from the encoded header data.
401      */
402     protected String[] getPrincipalsAndCredentials(String scheme, String encoded) {
403         String decoded = Base64.decodeToString(encoded);
404         return decoded.split(":", 2);
405     }
406 }