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     */
019    package org.tynamo.security.shiro.authc;
020    
021    import javax.servlet.ServletRequest;
022    import javax.servlet.ServletResponse;
023    import javax.servlet.http.HttpServletRequest;
024    import javax.servlet.http.HttpServletResponse;
025    
026    import org.apache.shiro.authc.AuthenticationToken;
027    import org.apache.shiro.codec.Base64;
028    import org.apache.shiro.web.util.WebUtils;
029    import org.slf4j.Logger;
030    import org.slf4j.LoggerFactory;
031    
032    
033    /**
034     * Requires the requesting user to be {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated} for the
035     * request to continue, and if they're not, requires the user to login via the HTTP Basic protocol-specific challenge.
036     * Upon successful login, they're allowed to continue on to the requested resource/url.
037     * <p/>
038     * This implementation is a 'clean room' Java implementation of Basic HTTP Authentication specification per
039     * <a href="ftp://ftp.isi.edu/in-notes/rfc2617.txt">RFC 2617</a>.
040     * <p/>
041     * Basic authentication functions as follows:
042     * <ol>
043     * <li>A request comes in for a resource that requires authentication.</li>
044     * <li>The server replies with a 401 response status, sets the <code>WWW-Authenticate</code> header, and the contents of a
045     * page informing the user that the incoming resource requires authentication.</li>
046     * <li>Upon receiving this <code>WWW-Authenticate</code> challenge from the server, the client then takes a
047     * username and a password and puts them in the following format:
048     * <p><code>username:password</code></p></li>
049     * <li>This token is then base 64 encoded.</li>
050     * <li>The client then sends another request for the same resource with the following header:<br/>
051     * <p><code>Authorization: Basic <em>Base64_encoded_username_and_password</em></code></p></li>
052     * </ol>
053     * The {@link #onAccessDenied(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method will
054     * only be called if the subject making the request is not
055     * {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated}
056     *
057     * @see <a href="ftp://ftp.isi.edu/in-notes/rfc2617.txt">RFC 2617</a>
058     * @see <a href="http://en.wikipedia.org/wiki/Basic_access_authentication">Basic Access Authentication</a>
059     * @since 0.4.0
060     */
061    public class BasicHttpAuthenticationFilter extends AuthenticatingFilter {
062    
063        /**
064         * This class's private logger.
065         */
066        private static final Logger log = LoggerFactory.getLogger(BasicHttpAuthenticationFilter.class);
067    
068        /**
069         * HTTP Authorization header, equal to <code>Authorization</code>
070         */
071        protected static final String AUTHORIZATION_HEADER = "Authorization";
072    
073        /**
074         * HTTP Authentication header, equal to <code>WWW-Authenticate</code>
075         */
076        protected static final String AUTHENTICATE_HEADER = "WWW-Authenticate";
077    
078        /**
079         * The name that is displayed during the challenge process of authentication, defauls to <code>application</code>
080         * and can be overridden by the {@link #setApplicationName(String) setApplicationName} method.
081         */
082        private String applicationName = "application";
083    
084        /**
085         * The authcScheme to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code>
086         */
087        private String authcScheme = HttpServletRequest.BASIC_AUTH;
088    
089        /**
090         * The authzScheme value to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code>
091         */
092        private String authzScheme = HttpServletRequest.BASIC_AUTH;
093    
094        /**
095         * Returns the name to use in the ServletResponse's <b><code>WWW-Authenticate</code></b> header.
096         * <p/>
097         * Per RFC 2617, this name name is displayed to the end user when they are asked to authenticate.  Unless overridden
098         * by the {@link #setApplicationName(String) setApplicationName(String)} method, the default value is 'application'.
099         * <p/>
100         * Please see {@link #setApplicationName(String) setApplicationName(String)} for an example of how this functions.
101         *
102         * @return the name to use in the ServletResponse's 'WWW-Authenticate' header.
103         */
104        public String getApplicationName() {
105            return applicationName;
106        }
107    
108        /**
109         * Sets the name to use in the ServletResponse's <b><code>WWW-Authenticate</code></b> header.
110         * <p/>
111         * Per RFC 2617, this name name is displayed to the end user when they are asked to authenticate.  Unless overridden
112         * by this method, the default value is &quot;application&quot;
113         * <p/>
114         * For example, setting this property to the value <b><code>Awesome Webapp</code></b> will result in the
115         * following header:
116         * <p/>
117         * <code>WWW-Authenticate: Basic realm=&quot;<b>Awesome Webapp</b>&quot;</code>
118         * <p/>
119         * Side note: As you can see from the header text, the HTTP Basic specification calls
120         * this the authentication 'realm', but we call this the 'applicationName' instead to avoid confusion with
121         * Shiro's Realm constructs.
122         *
123         * @param applicationName the name to use in the ServletResponse's 'WWW-Authenticate' header.
124         */
125        public void setApplicationName(String applicationName) {
126            this.applicationName = applicationName;
127        }
128    
129        /**
130         * Returns the HTTP <b><code>Authorization</code></b> header value that this filter will respond to as indicating
131         * a login request.
132         * <p/>
133         * Unless overridden by the {@link #setAuthzScheme(String) setAuthzScheme(String)} method, the
134         * default value is <code>BASIC</code>.
135         *
136         * @return the Http 'Authorization' header value that this filter will respond to as indicating a login request
137         */
138        public String getAuthzScheme() {
139            return authzScheme;
140        }
141    
142        /**
143         * Sets the HTTP <b><code>Authorization</code></b> header value that this filter will respond to as indicating a
144         * login request.
145         * <p/>
146         * Unless overridden by this method, the default value is <code>BASIC</code>
147         *
148         * @param authzScheme the HTTP <code>Authorization</code> header value that this filter will respond to as
149         *                    indicating a login request.
150         */
151        public void setAuthzScheme(String authzScheme) {
152            this.authzScheme = authzScheme;
153        }
154    
155        /**
156         * Returns the HTTP <b><code>WWW-Authenticate</code></b> header scheme that this filter will use when sending
157         * the HTTP Basic challenge response.  The default value is <code>BASIC</code>.
158         *
159         * @return the HTTP <code>WWW-Authenticate</code> header scheme that this filter will use when sending the HTTP
160         *         Basic challenge response.
161         * @see #sendChallenge
162         */
163        public String getAuthcScheme() {
164            return authcScheme;
165        }
166    
167        /**
168         * Sets the HTTP <b><code>WWW-Authenticate</code></b> header scheme that this filter will use when sending the
169         * HTTP Basic challenge response.  The default value is <code>BASIC</code>.
170         *
171         * @param authcScheme the HTTP <code>WWW-Authenticate</code> header scheme that this filter will use when
172         *                    sending the Http Basic challenge response.
173         * @see #sendChallenge
174         */
175        public void setAuthcScheme(String authcScheme) {
176            this.authcScheme = authcScheme;
177        }
178    
179        /**
180         * Processes unauthenticated requests. It handles the two-stage request/challenge authentication protocol.
181         *
182         * @param request  incoming ServletRequest
183         * @param response outgoing ServletResponse
184         * @return true if the request should be processed; false if the request should not continue to be processed
185         */
186        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
187            boolean loggedIn = false; //false by default or we wouldn't be in this method
188            if (isLoginAttempt(request, response)) {
189                loggedIn = executeLogin(request, response);
190            }
191            if (!loggedIn) {
192                sendChallenge(request, response);
193            }
194            return loggedIn;
195        }
196    
197        /**
198         * Determines whether the incoming request is an attempt to log in.
199         * <p/>
200         * The default implementation obtains the value of the request's
201         * {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER}, and if it is not <code>null</code>, delegates
202         * to {@link #isLoginAttempt(String) isLoginAttempt(authzHeaderValue)}. If the header is <code>null</code>,
203         * <code>false</code> is returned.
204         *
205         * @param request  incoming ServletRequest
206         * @param response outgoing ServletResponse
207         * @return true if the incoming request is an attempt to log in based, false otherwise
208         */
209        protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
210            String authzHeader = getAuthzHeader(request);
211            return authzHeader != null && isLoginAttempt(authzHeader);
212        }
213    
214        /**
215         * Returns the {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER} from the specified ServletRequest.
216         * <p/>
217         * This implementation merely casts the request to an <code>HttpServletRequest</code> and returns the header:
218         * <p/>
219         * <code>HttpServletRequest httpRequest = {@link WebUtils#toHttp(javax.servlet.ServletRequest) toHttp(reaquest)};<br/>
220         * return httpRequest.getHeader({@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER});</code>
221         *
222         * @param request the incoming <code>ServletRequest</code>
223         * @return the <code>Authorization</code> header's value.
224         */
225        protected String getAuthzHeader(ServletRequest request) {
226            HttpServletRequest httpRequest = WebUtils.toHttp(request);
227            return httpRequest.getHeader(AUTHORIZATION_HEADER);
228        }
229    
230        /**
231         * Default implementation that returns <code>true</code> if the specified <code>authzHeader</code>
232         * starts with the same (case-insensitive) characters specified by the
233         * {@link #getAuthzScheme() authzScheme}, <code>false</code> otherwise.
234         * <p/>
235         * That is:
236         * <p/>
237         * <code>String authzScheme = getAuthzScheme().toLowerCase();<br/>
238         * return authzHeader.toLowerCase().startsWith(authzScheme);</code>
239         *
240         * @param authzHeader the 'Authorization' header value (guaranteed to be non-null if the
241         *                    {@link #isLoginAttempt(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method is not overriden).
242         * @return <code>true</code> if the authzHeader value matches that configured as defined by
243         *         the {@link #getAuthzScheme() authzScheme}.
244         */
245        protected boolean isLoginAttempt(String authzHeader) {
246            String authzScheme = getAuthzScheme().toLowerCase();
247            return authzHeader.toLowerCase().startsWith(authzScheme);
248        }
249    
250        /**
251         * Builds the challenge for authorization by setting a HTTP <code>401</code> (Unauthorized) status as well as the
252         * response's {@link #AUTHENTICATE_HEADER AUTHENTICATE_HEADER}.
253         * <p/>
254         * The header value constructed is equal to:
255         * <p/>
256         * <code>{@link #getAuthcScheme() getAuthcScheme()} + " realm=\"" + {@link #getApplicationName() getApplicationName()} + "\"";</code>
257         *
258         * @param request  incoming ServletRequest, ignored by this implementation
259         * @param response outgoing ServletResponse
260         * @return false - this sends the challenge to be sent back
261         */
262        protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
263            if (log.isDebugEnabled()) {
264                log.debug("Authentication required: sending 401 Authentication challenge response.");
265            }
266            HttpServletResponse httpResponse = WebUtils.toHttp(response);
267            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
268            String authcHeader = getAuthcScheme() + " realm=\"" + getApplicationName() + "\"";
269            httpResponse.setHeader(AUTHENTICATE_HEADER, authcHeader);
270            return false;
271        }
272    
273        /**
274         * Creates an AuthenticationToken for use during login attempt with the provided credentials in the http header.
275         * <p/>
276         * This implementation:
277         * <ol><li>acquires the username and password based on the request's
278         * {@link #getAuthzHeader(javax.servlet.ServletRequest) authorization header} via the
279         * {@link #getPrincipalsAndCredentials(String, javax.servlet.ServletRequest) getPrincipalsAndCredentials} method</li>
280         * <li>The return value of that method is converted to an <code>AuthenticationToken</code> via the
281         * {@link #createToken(String, String, javax.servlet.ServletRequest, javax.servlet.ServletResponse) createToken} method</li>
282         * <li>The created <code>AuthenticationToken</code> is returned.</li>
283         * </ol>
284         *
285         * @param request  incoming ServletRequest
286         * @param response outgoing ServletResponse
287         * @return the AuthenticationToken used to execute the login attempt
288         */
289        protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
290            String authorizationHeader = getAuthzHeader(request);
291            if (authorizationHeader == null || authorizationHeader.length() == 0) {
292                // Create an empty authentication token since there is no
293                // Authorization header.
294                return createToken("", "", request, response);
295            }
296    
297            if (log.isDebugEnabled()) {
298                log.debug("Attempting to execute login with headers [" + authorizationHeader + "]");
299            }
300    
301            String[] prinCred = getPrincipalsAndCredentials(authorizationHeader, request);
302            if (prinCred == null || prinCred.length < 2) {
303                // Create an authentication token with an empty password,
304                // since one hasn't been provided in the request.
305                String username = prinCred == null || prinCred.length == 0 ? "" : prinCred[0];
306                return createToken(username, "", request, response);
307            }
308    
309            String username = prinCred[0];
310            String password = prinCred[1];
311    
312            return createToken(username, password, request, response);
313        }
314    
315        /**
316         * Returns the username obtained from the
317         * {@link #getAuthzHeader(javax.servlet.ServletRequest) authorizationHeader}.
318         * <p/>
319         * Once the {@code authzHeader} is split per the RFC (based on the space character ' '), the resulting split tokens
320         * are translated into the username/password pair by the
321         * {@link #getPrincipalsAndCredentials(String, String) getPrincipalsAndCredentials(scheme,encoded)} method.
322         *
323         * @param authorizationHeader the authorization header obtained from the request.
324         * @param request             the incoming ServletRequest
325         * @return the username (index 0)/password pair (index 1) submitted by the user for the given header value and request.
326         * @see #getAuthzHeader(javax.servlet.ServletRequest)
327         */
328        protected String[] getPrincipalsAndCredentials(String authorizationHeader, ServletRequest request) {
329            if (authorizationHeader == null) {
330                return null;
331            }
332            String[] authTokens = authorizationHeader.split(" ");
333            if (authTokens == null || authTokens.length < 2) {
334                return null;
335            }
336            return getPrincipalsAndCredentials(authTokens[0], authTokens[1]);
337        }
338    
339        /**
340         * Returns the username and password pair based on the specified <code>encoded</code> String obtained from
341         * the request's authorization header.
342         * <p/>
343         * Per RFC 2617, the default implementation first Base64 decodes the string and then splits the resulting decoded
344         * string into two based on the ":" character.  That is:
345         * <p/>
346         * <code>String decoded = Base64.decodeToString(encoded);<br/>
347         * return decoded.split(":");</code>
348         *
349         * @param scheme  the {@link #getAuthcScheme() authcScheme} found in the request
350         *                {@link #getAuthzHeader(javax.servlet.ServletRequest) authzHeader}.  It is ignored by this implementation,
351         *                but available to overriding implementations should they find it useful.
352         * @param encoded the Base64-encoded username:password value found after the scheme in the header
353         * @return the username (index 0)/password (index 1) pair obtained from the encoded header data.
354         */
355        protected String[] getPrincipalsAndCredentials(String scheme, String encoded) {
356            String decoded = Base64.decodeToString(encoded);
357            return decoded.split(":");
358        }
359    }