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.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 &quot;application&quot;
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=&quot;<b>Awesome Webapp</b>&quot;</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 }