001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.web.filter.authc;
020
021import org.apache.shiro.authc.AuthenticationToken;
022import org.apache.shiro.codec.Base64;
023import org.apache.shiro.web.util.WebUtils;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027import javax.servlet.ServletRequest;
028import javax.servlet.ServletResponse;
029import javax.servlet.http.HttpServletRequest;
030import javax.servlet.http.HttpServletResponse;
031import java.util.HashSet;
032import java.util.Locale;
033import java.util.Set;
034
035
036/**
037 * Requires the requesting user to be {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated} for the
038 * request to continue, and if they're not, requires the user to login via the HTTP Basic protocol-specific challenge.
039 * Upon successful login, they're allowed to continue on to the requested resource/url.
040 * <p/>
041 * This implementation is a 'clean room' Java implementation of Basic HTTP Authentication specification per
042 * <a href="ftp://ftp.isi.edu/in-notes/rfc2617.txt">RFC 2617</a>.
043 * <p/>
044 * Basic authentication functions as follows:
045 * <ol>
046 * <li>A request comes in for a resource that requires authentication.</li>
047 * <li>The server replies with a 401 response status, sets the <code>WWW-Authenticate</code> header, and the contents of a
048 * page informing the user that the incoming resource requires authentication.</li>
049 * <li>Upon receiving this <code>WWW-Authenticate</code> challenge from the server, the client then takes a
050 * username and a password and puts them in the following format:
051 * <p><code>username:password</code></p></li>
052 * <li>This token is then base 64 encoded.</li>
053 * <li>The client then sends another request for the same resource with the following header:<br/>
054 * <p><code>Authorization: Basic <em>Base64_encoded_username_and_password</em></code></p></li>
055 * </ol>
056 * The {@link #onAccessDenied(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method will
057 * only be called if the subject making the request is not
058 * {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated}
059 *
060 * @see <a href="ftp://ftp.isi.edu/in-notes/rfc2617.txt">RFC 2617</a>
061 * @see <a href="http://en.wikipedia.org/wiki/Basic_access_authentication">Basic Access Authentication</a>
062 * @since 0.9
063 */
064public class BasicHttpAuthenticationFilter extends AuthenticatingFilter {
065
066    /**
067     * This class's private logger.
068     */
069    private static final Logger log = LoggerFactory.getLogger(BasicHttpAuthenticationFilter.class);
070
071    /**
072     * HTTP Authorization header, equal to <code>Authorization</code>
073     */
074    protected static final String AUTHORIZATION_HEADER = "Authorization";
075
076    /**
077     * HTTP Authentication header, equal to <code>WWW-Authenticate</code>
078     */
079    protected static final String AUTHENTICATE_HEADER = "WWW-Authenticate";
080
081    /**
082     * The name that is displayed during the challenge process of authentication, defauls to <code>application</code>
083     * and can be overridden by the {@link #setApplicationName(String) setApplicationName} method.
084     */
085    private String applicationName = "application";
086
087    /**
088     * The authcScheme to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code>
089     */
090    private String authcScheme = HttpServletRequest.BASIC_AUTH;
091
092    /**
093     * The authzScheme value to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code>
094     */
095    private String authzScheme = HttpServletRequest.BASIC_AUTH;
096
097    /**
098     * Returns the name to use in the ServletResponse's <b><code>WWW-Authenticate</code></b> header.
099     * <p/>
100     * Per RFC 2617, this name name is displayed to the end user when they are asked to authenticate.  Unless overridden
101     * by the {@link #setApplicationName(String) setApplicationName(String)} method, the default value is 'application'.
102     * <p/>
103     * Please see {@link #setApplicationName(String) setApplicationName(String)} for an example of how this functions.
104     *
105     * @return the name to use in the ServletResponse's 'WWW-Authenticate' header.
106     */
107    public String getApplicationName() {
108        return applicationName;
109    }
110
111    /**
112     * Sets the name to use in the ServletResponse's <b><code>WWW-Authenticate</code></b> header.
113     * <p/>
114     * Per RFC 2617, this name name is displayed to the end user when they are asked to authenticate.  Unless overridden
115     * by this method, the default value is &quot;application&quot;
116     * <p/>
117     * For example, setting this property to the value <b><code>Awesome Webapp</code></b> will result in the
118     * following header:
119     * <p/>
120     * <code>WWW-Authenticate: Basic realm=&quot;<b>Awesome Webapp</b>&quot;</code>
121     * <p/>
122     * Side note: As you can see from the header text, the HTTP Basic specification calls
123     * this the authentication 'realm', but we call this the 'applicationName' instead to avoid confusion with
124     * Shiro's Realm constructs.
125     *
126     * @param applicationName the name to use in the ServletResponse's 'WWW-Authenticate' header.
127     */
128    public void setApplicationName(String applicationName) {
129        this.applicationName = applicationName;
130    }
131
132    /**
133     * Returns the HTTP <b><code>Authorization</code></b> header value that this filter will respond to as indicating
134     * a login request.
135     * <p/>
136     * Unless overridden by the {@link #setAuthzScheme(String) setAuthzScheme(String)} method, the
137     * default value is <code>BASIC</code>.
138     *
139     * @return the Http 'Authorization' header value that this filter will respond to as indicating a login request
140     */
141    public String getAuthzScheme() {
142        return authzScheme;
143    }
144
145    /**
146     * Sets the HTTP <b><code>Authorization</code></b> header value that this filter will respond to as indicating a
147     * login request.
148     * <p/>
149     * Unless overridden by this method, the default value is <code>BASIC</code>
150     *
151     * @param authzScheme the HTTP <code>Authorization</code> header value that this filter will respond to as
152     *                    indicating a login request.
153     */
154    public void setAuthzScheme(String authzScheme) {
155        this.authzScheme = authzScheme;
156    }
157
158    /**
159     * Returns the HTTP <b><code>WWW-Authenticate</code></b> header scheme that this filter will use when sending
160     * the HTTP Basic challenge response.  The default value is <code>BASIC</code>.
161     *
162     * @return the HTTP <code>WWW-Authenticate</code> header scheme that this filter will use when sending the HTTP
163     *         Basic challenge response.
164     * @see #sendChallenge
165     */
166    public String getAuthcScheme() {
167        return authcScheme;
168    }
169
170    /**
171     * Sets the HTTP <b><code>WWW-Authenticate</code></b> header scheme that this filter will use when sending the
172     * HTTP Basic challenge response.  The default value is <code>BASIC</code>.
173     *
174     * @param authcScheme the HTTP <code>WWW-Authenticate</code> header scheme that this filter will use when
175     *                    sending the Http Basic challenge response.
176     * @see #sendChallenge
177     */
178    public void setAuthcScheme(String authcScheme) {
179        this.authcScheme = authcScheme;
180    }
181    
182    /**
183     * The Basic authentication filter can be configured with a list of HTTP methods to which it should apply. This
184     * method ensures that authentication is <em>only</em> required for those HTTP methods specified. For example,
185     * if you had the configuration:
186     * <pre>
187     *    [urls]
188     *    /basic/** = authcBasic[POST,PUT,DELETE]
189     * </pre>
190     * then a GET request would not required authentication but a POST would.
191     * @param request The current HTTP servlet request.
192     * @param response The current HTTP servlet response.
193     * @param mappedValue The array of configured HTTP methods as strings. This is empty if no methods are configured.
194     */
195    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
196        HttpServletRequest httpRequest = WebUtils.toHttp(request);
197        String httpMethod = httpRequest.getMethod();
198        
199        // Check whether the current request's method requires authentication.
200        // If no methods have been configured, then all of them require auth,
201        // otherwise only the declared ones need authentication.
202
203        Set<String> methods = httpMethodsFromOptions((String[])mappedValue);
204        boolean authcRequired = methods.size() == 0;
205        for (String m : methods) {
206            if (httpMethod.toUpperCase(Locale.ENGLISH).equals(m)) { // list of methods is in upper case
207                authcRequired = true;
208                break;
209            }
210        }
211        
212        if (authcRequired) {
213            return super.isAccessAllowed(request, response, mappedValue);
214        }
215        else {
216            return true;
217        }
218    }
219
220    private Set<String> httpMethodsFromOptions(String[] options) {
221        Set<String> methods = new HashSet<String>();
222
223        if (options != null) {
224            for (String option : options) {
225                // to be backwards compatible with 1.3, we can ONLY check for known args
226                // ideally we would just validate HTTP methods, but someone could already be using this for webdav
227                if (!option.equalsIgnoreCase(PERMISSIVE)) {
228                    methods.add(option.toUpperCase(Locale.ENGLISH));
229                }
230            }
231        }
232        return methods;
233    }
234
235    /**
236     * Processes unauthenticated requests. It handles the two-stage request/challenge authentication protocol.
237     *
238     * @param request  incoming ServletRequest
239     * @param response outgoing ServletResponse
240     * @return true if the request should be processed; false if the request should not continue to be processed
241     */
242    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
243        boolean loggedIn = false; //false by default or we wouldn't be in this method
244        if (isLoginAttempt(request, response)) {
245            loggedIn = executeLogin(request, response);
246        }
247        if (!loggedIn) {
248            sendChallenge(request, response);
249        }
250        return loggedIn;
251    }
252
253    /**
254     * Determines whether the incoming request is an attempt to log in.
255     * <p/>
256     * The default implementation obtains the value of the request's
257     * {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER}, and if it is not <code>null</code>, delegates
258     * to {@link #isLoginAttempt(String) isLoginAttempt(authzHeaderValue)}. If the header is <code>null</code>,
259     * <code>false</code> is returned.
260     *
261     * @param request  incoming ServletRequest
262     * @param response outgoing ServletResponse
263     * @return true if the incoming request is an attempt to log in based, false otherwise
264     */
265    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
266        String authzHeader = getAuthzHeader(request);
267        return authzHeader != null && isLoginAttempt(authzHeader);
268    }
269
270    /**
271     * Delegates to {@link #isLoginAttempt(javax.servlet.ServletRequest, javax.servlet.ServletResponse) isLoginAttempt}.
272     */
273    @Override
274    protected final boolean isLoginRequest(ServletRequest request, ServletResponse response) {
275        return this.isLoginAttempt(request, response);
276    }
277
278    /**
279     * Returns the {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER} from the specified ServletRequest.
280     * <p/>
281     * This implementation merely casts the request to an <code>HttpServletRequest</code> and returns the header:
282     * <p/>
283     * <code>HttpServletRequest httpRequest = {@link WebUtils#toHttp(javax.servlet.ServletRequest) toHttp(reaquest)};<br/>
284     * return httpRequest.getHeader({@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER});</code>
285     *
286     * @param request the incoming <code>ServletRequest</code>
287     * @return the <code>Authorization</code> header's value.
288     */
289    protected String getAuthzHeader(ServletRequest request) {
290        HttpServletRequest httpRequest = WebUtils.toHttp(request);
291        return httpRequest.getHeader(AUTHORIZATION_HEADER);
292    }
293
294    /**
295     * Default implementation that returns <code>true</code> if the specified <code>authzHeader</code>
296     * starts with the same (case-insensitive) characters specified by the
297     * {@link #getAuthzScheme() authzScheme}, <code>false</code> otherwise.
298     * <p/>
299     * That is:
300     * <p/>
301     * <code>String authzScheme = getAuthzScheme().toLowerCase();<br/>
302     * return authzHeader.toLowerCase().startsWith(authzScheme);</code>
303     *
304     * @param authzHeader the 'Authorization' header value (guaranteed to be non-null if the
305     *                    {@link #isLoginAttempt(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method is not overriden).
306     * @return <code>true</code> if the authzHeader value matches that configured as defined by
307     *         the {@link #getAuthzScheme() authzScheme}.
308     */
309    protected boolean isLoginAttempt(String authzHeader) {
310        //SHIRO-415: use English Locale:
311        String authzScheme = getAuthzScheme().toLowerCase(Locale.ENGLISH);
312        return authzHeader.toLowerCase(Locale.ENGLISH).startsWith(authzScheme);
313    }
314
315    /**
316     * Builds the challenge for authorization by setting a HTTP <code>401</code> (Unauthorized) status as well as the
317     * response's {@link #AUTHENTICATE_HEADER AUTHENTICATE_HEADER}.
318     * <p/>
319     * The header value constructed is equal to:
320     * <p/>
321     * <code>{@link #getAuthcScheme() getAuthcScheme()} + " realm=\"" + {@link #getApplicationName() getApplicationName()} + "\"";</code>
322     *
323     * @param request  incoming ServletRequest, ignored by this implementation
324     * @param response outgoing ServletResponse
325     * @return false - this sends the challenge to be sent back
326     */
327    protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
328        log.debug("Authentication required: sending 401 Authentication challenge response.");
329
330        HttpServletResponse httpResponse = WebUtils.toHttp(response);
331        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
332        String authcHeader = getAuthcScheme() + " realm=\"" + getApplicationName() + "\"";
333        httpResponse.setHeader(AUTHENTICATE_HEADER, authcHeader);
334        return false;
335    }
336
337    /**
338     * Creates an AuthenticationToken for use during login attempt with the provided credentials in the http header.
339     * <p/>
340     * This implementation:
341     * <ol><li>acquires the username and password based on the request's
342     * {@link #getAuthzHeader(javax.servlet.ServletRequest) authorization header} via the
343     * {@link #getPrincipalsAndCredentials(String, javax.servlet.ServletRequest) getPrincipalsAndCredentials} method</li>
344     * <li>The return value of that method is converted to an <code>AuthenticationToken</code> via the
345     * {@link #createToken(String, String, javax.servlet.ServletRequest, javax.servlet.ServletResponse) createToken} method</li>
346     * <li>The created <code>AuthenticationToken</code> is returned.</li>
347     * </ol>
348     *
349     * @param request  incoming ServletRequest
350     * @param response outgoing ServletResponse
351     * @return the AuthenticationToken used to execute the login attempt
352     */
353    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
354        String authorizationHeader = getAuthzHeader(request);
355        if (authorizationHeader == null || authorizationHeader.length() == 0) {
356            // Create an empty authentication token since there is no
357            // Authorization header.
358            return createToken("", "", request, response);
359        }
360
361        log.debug("Attempting to execute login with auth header");
362
363        String[] prinCred = getPrincipalsAndCredentials(authorizationHeader, request);
364        if (prinCred == null || prinCred.length < 2) {
365            // Create an authentication token with an empty password,
366            // since one hasn't been provided in the request.
367            String username = prinCred == null || prinCred.length == 0 ? "" : prinCred[0];
368            return createToken(username, "", request, response);
369        }
370
371        String username = prinCred[0];
372        String password = prinCred[1];
373
374        return createToken(username, password, request, response);
375    }
376
377    /**
378     * Returns the username obtained from the
379     * {@link #getAuthzHeader(javax.servlet.ServletRequest) authorizationHeader}.
380     * <p/>
381     * Once the {@code authzHeader} is split per the RFC (based on the space character ' '), the resulting split tokens
382     * are translated into the username/password pair by the
383     * {@link #getPrincipalsAndCredentials(String, String) getPrincipalsAndCredentials(scheme,encoded)} method.
384     *
385     * @param authorizationHeader the authorization header obtained from the request.
386     * @param request             the incoming ServletRequest
387     * @return the username (index 0)/password pair (index 1) submitted by the user for the given header value and request.
388     * @see #getAuthzHeader(javax.servlet.ServletRequest)
389     */
390    protected String[] getPrincipalsAndCredentials(String authorizationHeader, ServletRequest request) {
391        if (authorizationHeader == null) {
392            return null;
393        }
394        String[] authTokens = authorizationHeader.split(" ");
395        if (authTokens == null || authTokens.length < 2) {
396            return null;
397        }
398        return getPrincipalsAndCredentials(authTokens[0], authTokens[1]);
399    }
400
401    /**
402     * Returns the username and password pair based on the specified <code>encoded</code> String obtained from
403     * the request's authorization header.
404     * <p/>
405     * Per RFC 2617, the default implementation first Base64 decodes the string and then splits the resulting decoded
406     * string into two based on the ":" character.  That is:
407     * <p/>
408     * <code>String decoded = Base64.decodeToString(encoded);<br/>
409     * return decoded.split(":");</code>
410     *
411     * @param scheme  the {@link #getAuthcScheme() authcScheme} found in the request
412     *                {@link #getAuthzHeader(javax.servlet.ServletRequest) authzHeader}.  It is ignored by this implementation,
413     *                but available to overriding implementations should they find it useful.
414     * @param encoded the Base64-encoded username:password value found after the scheme in the header
415     * @return the username (index 0)/password (index 1) pair obtained from the encoded header data.
416     */
417    protected String[] getPrincipalsAndCredentials(String scheme, String encoded) {
418        String decoded = Base64.decodeToString(encoded);
419        return decoded.split(":", 2);
420    }
421}