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.Locale;
032
033
034/**
035 * Requires the requesting user to be {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated} for the
036 * request to continue, and if they're not, requires the user to login via the HTTP Basic protocol-specific challenge.
037 * Upon successful login, they're allowed to continue on to the requested resource/url.
038 * <p/>
039 * This implementation is a 'clean room' Java implementation of Basic HTTP Authentication specification per
040 * <a href="ftp://ftp.isi.edu/in-notes/rfc2617.txt">RFC 2617</a>.
041 * <p/>
042 * Basic authentication functions as follows:
043 * <ol>
044 * <li>A request comes in for a resource that requires authentication.</li>
045 * <li>The server replies with a 401 response status, sets the <code>WWW-Authenticate</code> header, and the contents of a
046 * page informing the user that the incoming resource requires authentication.</li>
047 * <li>Upon receiving this <code>WWW-Authenticate</code> challenge from the server, the client then takes a
048 * username and a password and puts them in the following format:
049 * <p><code>username:password</code></p></li>
050 * <li>This token is then base 64 encoded.</li>
051 * <li>The client then sends another request for the same resource with the following header:<br/>
052 * <p><code>Authorization: Basic <em>Base64_encoded_username_and_password</em></code></p></li>
053 * </ol>
054 * The {@link #onAccessDenied(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method will
055 * only be called if the subject making the request is not
056 * {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated}
057 *
058 * @see <a href="ftp://ftp.isi.edu/in-notes/rfc2617.txt">RFC 2617</a>
059 * @see <a href="http://en.wikipedia.org/wiki/Basic_access_authentication">Basic Access Authentication</a>
060 * @since 0.9
061 */
062public class BasicHttpAuthenticationFilter extends AuthenticatingFilter {
063
064    /**
065     * This class's private logger.
066     */
067    private static final Logger log = LoggerFactory.getLogger(BasicHttpAuthenticationFilter.class);
068
069    /**
070     * HTTP Authorization header, equal to <code>Authorization</code>
071     */
072    protected static final String AUTHORIZATION_HEADER = "Authorization";
073
074    /**
075     * HTTP Authentication header, equal to <code>WWW-Authenticate</code>
076     */
077    protected static final String AUTHENTICATE_HEADER = "WWW-Authenticate";
078
079    /**
080     * The name that is displayed during the challenge process of authentication, defauls to <code>application</code>
081     * and can be overridden by the {@link #setApplicationName(String) setApplicationName} method.
082     */
083    private String applicationName = "application";
084
085    /**
086     * The authcScheme to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code>
087     */
088    private String authcScheme = HttpServletRequest.BASIC_AUTH;
089
090    /**
091     * The authzScheme value to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code>
092     */
093    private String authzScheme = HttpServletRequest.BASIC_AUTH;
094
095    /**
096     * Returns the name to use in the ServletResponse's <b><code>WWW-Authenticate</code></b> header.
097     * <p/>
098     * Per RFC 2617, this name name is displayed to the end user when they are asked to authenticate.  Unless overridden
099     * 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}