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 "application"
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="<b>Awesome Webapp</b>"</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 }