Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
BasicHttpAuthenticationFilter |
|
| 2.2;2.2 |
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.codec.Base64; | |
23 | import org.apache.shiro.web.util.WebUtils; | |
24 | import org.slf4j.Logger; | |
25 | import org.slf4j.LoggerFactory; | |
26 | ||
27 | import javax.servlet.ServletRequest; | |
28 | import javax.servlet.ServletResponse; | |
29 | import javax.servlet.http.HttpServletRequest; | |
30 | import javax.servlet.http.HttpServletResponse; | |
31 | import java.util.Locale; | |
32 | ||
33 | ||
34 | /** | |
35 | * Requires the requesting user to be {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated} for the | |
36 | * request to continue, and if they're not, requires the user to login via the HTTP Basic protocol-specific challenge. | |
37 | * Upon successful login, they're allowed to continue on to the requested resource/url. | |
38 | * <p/> | |
39 | * This implementation is a 'clean room' Java implementation of Basic HTTP Authentication specification per | |
40 | * <a href="ftp://ftp.isi.edu/in-notes/rfc2617.txt">RFC 2617</a>. | |
41 | * <p/> | |
42 | * Basic authentication functions as follows: | |
43 | * <ol> | |
44 | * <li>A request comes in for a resource that requires authentication.</li> | |
45 | * <li>The server replies with a 401 response status, sets the <code>WWW-Authenticate</code> header, and the contents of a | |
46 | * page informing the user that the incoming resource requires authentication.</li> | |
47 | * <li>Upon receiving this <code>WWW-Authenticate</code> challenge from the server, the client then takes a | |
48 | * username and a password and puts them in the following format: | |
49 | * <p><code>username:password</code></p></li> | |
50 | * <li>This token is then base 64 encoded.</li> | |
51 | * <li>The client then sends another request for the same resource with the following header:<br/> | |
52 | * <p><code>Authorization: Basic <em>Base64_encoded_username_and_password</em></code></p></li> | |
53 | * </ol> | |
54 | * The {@link #onAccessDenied(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method will | |
55 | * only be called if the subject making the request is not | |
56 | * {@link org.apache.shiro.subject.Subject#isAuthenticated() authenticated} | |
57 | * | |
58 | * @see <a href="ftp://ftp.isi.edu/in-notes/rfc2617.txt">RFC 2617</a> | |
59 | * @see <a href="http://en.wikipedia.org/wiki/Basic_access_authentication">Basic Access Authentication</a> | |
60 | * @since 0.9 | |
61 | */ | |
62 | 51 | public class BasicHttpAuthenticationFilter extends AuthenticatingFilter { |
63 | ||
64 | /** | |
65 | * This class's private logger. | |
66 | */ | |
67 | 1 | private static final Logger log = LoggerFactory.getLogger(BasicHttpAuthenticationFilter.class); |
68 | ||
69 | /** | |
70 | * HTTP Authorization header, equal to <code>Authorization</code> | |
71 | */ | |
72 | protected static final String AUTHORIZATION_HEADER = "Authorization"; | |
73 | ||
74 | /** | |
75 | * HTTP Authentication header, equal to <code>WWW-Authenticate</code> | |
76 | */ | |
77 | protected static final String AUTHENTICATE_HEADER = "WWW-Authenticate"; | |
78 | ||
79 | /** | |
80 | * The name that is displayed during the challenge process of authentication, defauls to <code>application</code> | |
81 | * and can be overridden by the {@link #setApplicationName(String) setApplicationName} method. | |
82 | */ | |
83 | 51 | private String applicationName = "application"; |
84 | ||
85 | /** | |
86 | * The authcScheme to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code> | |
87 | */ | |
88 | 51 | private String authcScheme = HttpServletRequest.BASIC_AUTH; |
89 | ||
90 | /** | |
91 | * The authzScheme value to look for in the <code>Authorization</code> header, defaults to <code>BASIC</code> | |
92 | */ | |
93 | 51 | private String authzScheme = HttpServletRequest.BASIC_AUTH; |
94 | ||
95 | /** | |
96 | * Returns the name to use in the ServletResponse's <b><code>WWW-Authenticate</code></b> header. | |
97 | * <p/> | |
98 | * Per RFC 2617, this name name is displayed to the end user when they are asked to authenticate. Unless overridden | |
99 | * 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 | 0 | 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 "application" | |
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="<b>Awesome Webapp</b>"</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 | 0 | this.applicationName = applicationName; |
128 | 0 | } |
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 | 0 | 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 | 0 | this.authzScheme = authzScheme; |
154 | 0 | } |
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 | 0 | 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 | 0 | this.authcScheme = authcScheme; |
178 | 0 | } |
179 | ||
180 | /** | |
181 | * Processes unauthenticated requests. It handles the two-stage request/challenge authentication protocol. | |
182 | * | |
183 | * @param request incoming ServletRequest | |
184 | * @param response outgoing ServletResponse | |
185 | * @return true if the request should be processed; false if the request should not continue to be processed | |
186 | */ | |
187 | protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { | |
188 | 0 | boolean loggedIn = false; //false by default or we wouldn't be in this method |
189 | 0 | if (isLoginAttempt(request, response)) { |
190 | 0 | loggedIn = executeLogin(request, response); |
191 | } | |
192 | 0 | if (!loggedIn) { |
193 | 0 | sendChallenge(request, response); |
194 | } | |
195 | 0 | return loggedIn; |
196 | } | |
197 | ||
198 | /** | |
199 | * Determines whether the incoming request is an attempt to log in. | |
200 | * <p/> | |
201 | * The default implementation obtains the value of the request's | |
202 | * {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER}, and if it is not <code>null</code>, delegates | |
203 | * to {@link #isLoginAttempt(String) isLoginAttempt(authzHeaderValue)}. If the header is <code>null</code>, | |
204 | * <code>false</code> is returned. | |
205 | * | |
206 | * @param request incoming ServletRequest | |
207 | * @param response outgoing ServletResponse | |
208 | * @return true if the incoming request is an attempt to log in based, false otherwise | |
209 | */ | |
210 | protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { | |
211 | 0 | String authzHeader = getAuthzHeader(request); |
212 | 0 | return authzHeader != null && isLoginAttempt(authzHeader); |
213 | } | |
214 | ||
215 | /** | |
216 | * Delegates to {@link #isLoginAttempt(javax.servlet.ServletRequest, javax.servlet.ServletResponse) isLoginAttempt}. | |
217 | */ | |
218 | @Override | |
219 | protected final boolean isLoginRequest(ServletRequest request, ServletResponse response) { | |
220 | 0 | return this.isLoginAttempt(request, response); |
221 | } | |
222 | ||
223 | /** | |
224 | * Returns the {@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER} from the specified ServletRequest. | |
225 | * <p/> | |
226 | * This implementation merely casts the request to an <code>HttpServletRequest</code> and returns the header: | |
227 | * <p/> | |
228 | * <code>HttpServletRequest httpRequest = {@link WebUtils#toHttp(javax.servlet.ServletRequest) toHttp(reaquest)};<br/> | |
229 | * return httpRequest.getHeader({@link #AUTHORIZATION_HEADER AUTHORIZATION_HEADER});</code> | |
230 | * | |
231 | * @param request the incoming <code>ServletRequest</code> | |
232 | * @return the <code>Authorization</code> header's value. | |
233 | */ | |
234 | protected String getAuthzHeader(ServletRequest request) { | |
235 | 4 | HttpServletRequest httpRequest = WebUtils.toHttp(request); |
236 | 4 | return httpRequest.getHeader(AUTHORIZATION_HEADER); |
237 | } | |
238 | ||
239 | /** | |
240 | * Default implementation that returns <code>true</code> if the specified <code>authzHeader</code> | |
241 | * starts with the same (case-insensitive) characters specified by the | |
242 | * {@link #getAuthzScheme() authzScheme}, <code>false</code> otherwise. | |
243 | * <p/> | |
244 | * That is: | |
245 | * <p/> | |
246 | * <code>String authzScheme = getAuthzScheme().toLowerCase();<br/> | |
247 | * return authzHeader.toLowerCase().startsWith(authzScheme);</code> | |
248 | * | |
249 | * @param authzHeader the 'Authorization' header value (guaranteed to be non-null if the | |
250 | * {@link #isLoginAttempt(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method is not overriden). | |
251 | * @return <code>true</code> if the authzHeader value matches that configured as defined by | |
252 | * the {@link #getAuthzScheme() authzScheme}. | |
253 | */ | |
254 | protected boolean isLoginAttempt(String authzHeader) { | |
255 | //SHIRO-415: use English Locale: | |
256 | 0 | String authzScheme = getAuthzScheme().toLowerCase(Locale.ENGLISH); |
257 | 0 | return authzHeader.toLowerCase(Locale.ENGLISH).startsWith(authzScheme); |
258 | } | |
259 | ||
260 | /** | |
261 | * Builds the challenge for authorization by setting a HTTP <code>401</code> (Unauthorized) status as well as the | |
262 | * response's {@link #AUTHENTICATE_HEADER AUTHENTICATE_HEADER}. | |
263 | * <p/> | |
264 | * The header value constructed is equal to: | |
265 | * <p/> | |
266 | * <code>{@link #getAuthcScheme() getAuthcScheme()} + " realm=\"" + {@link #getApplicationName() getApplicationName()} + "\"";</code> | |
267 | * | |
268 | * @param request incoming ServletRequest, ignored by this implementation | |
269 | * @param response outgoing ServletResponse | |
270 | * @return false - this sends the challenge to be sent back | |
271 | */ | |
272 | protected boolean sendChallenge(ServletRequest request, ServletResponse response) { | |
273 | 0 | if (log.isDebugEnabled()) { |
274 | 0 | log.debug("Authentication required: sending 401 Authentication challenge response."); |
275 | } | |
276 | 0 | HttpServletResponse httpResponse = WebUtils.toHttp(response); |
277 | 0 | httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); |
278 | 0 | String authcHeader = getAuthcScheme() + " realm=\"" + getApplicationName() + "\""; |
279 | 0 | httpResponse.setHeader(AUTHENTICATE_HEADER, authcHeader); |
280 | 0 | return false; |
281 | } | |
282 | ||
283 | /** | |
284 | * Creates an AuthenticationToken for use during login attempt with the provided credentials in the http header. | |
285 | * <p/> | |
286 | * This implementation: | |
287 | * <ol><li>acquires the username and password based on the request's | |
288 | * {@link #getAuthzHeader(javax.servlet.ServletRequest) authorization header} via the | |
289 | * {@link #getPrincipalsAndCredentials(String, javax.servlet.ServletRequest) getPrincipalsAndCredentials} method</li> | |
290 | * <li>The return value of that method is converted to an <code>AuthenticationToken</code> via the | |
291 | * {@link #createToken(String, String, javax.servlet.ServletRequest, javax.servlet.ServletResponse) createToken} method</li> | |
292 | * <li>The created <code>AuthenticationToken</code> is returned.</li> | |
293 | * </ol> | |
294 | * | |
295 | * @param request incoming ServletRequest | |
296 | * @param response outgoing ServletResponse | |
297 | * @return the AuthenticationToken used to execute the login attempt | |
298 | */ | |
299 | protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) { | |
300 | 4 | String authorizationHeader = getAuthzHeader(request); |
301 | 4 | if (authorizationHeader == null || authorizationHeader.length() == 0) { |
302 | // Create an empty authentication token since there is no | |
303 | // Authorization header. | |
304 | 1 | return createToken("", "", request, response); |
305 | } | |
306 | ||
307 | 3 | if (log.isDebugEnabled()) { |
308 | 3 | log.debug("Attempting to execute login with headers [" + authorizationHeader + "]"); |
309 | } | |
310 | ||
311 | 3 | String[] prinCred = getPrincipalsAndCredentials(authorizationHeader, request); |
312 | 3 | if (prinCred == null || prinCred.length < 2) { |
313 | // Create an authentication token with an empty password, | |
314 | // since one hasn't been provided in the request. | |
315 | 0 | String username = prinCred == null || prinCred.length == 0 ? "" : prinCred[0]; |
316 | 0 | return createToken(username, "", request, response); |
317 | } | |
318 | ||
319 | 3 | String username = prinCred[0]; |
320 | 3 | String password = prinCred[1]; |
321 | ||
322 | 3 | return createToken(username, password, request, response); |
323 | } | |
324 | ||
325 | /** | |
326 | * Returns the username obtained from the | |
327 | * {@link #getAuthzHeader(javax.servlet.ServletRequest) authorizationHeader}. | |
328 | * <p/> | |
329 | * Once the {@code authzHeader} is split per the RFC (based on the space character ' '), the resulting split tokens | |
330 | * are translated into the username/password pair by the | |
331 | * {@link #getPrincipalsAndCredentials(String, String) getPrincipalsAndCredentials(scheme,encoded)} method. | |
332 | * | |
333 | * @param authorizationHeader the authorization header obtained from the request. | |
334 | * @param request the incoming ServletRequest | |
335 | * @return the username (index 0)/password pair (index 1) submitted by the user for the given header value and request. | |
336 | * @see #getAuthzHeader(javax.servlet.ServletRequest) | |
337 | */ | |
338 | protected String[] getPrincipalsAndCredentials(String authorizationHeader, ServletRequest request) { | |
339 | 3 | if (authorizationHeader == null) { |
340 | 0 | return null; |
341 | } | |
342 | 3 | String[] authTokens = authorizationHeader.split(" "); |
343 | 3 | if (authTokens == null || authTokens.length < 2) { |
344 | 0 | return null; |
345 | } | |
346 | 3 | return getPrincipalsAndCredentials(authTokens[0], authTokens[1]); |
347 | } | |
348 | ||
349 | /** | |
350 | * Returns the username and password pair based on the specified <code>encoded</code> String obtained from | |
351 | * the request's authorization header. | |
352 | * <p/> | |
353 | * Per RFC 2617, the default implementation first Base64 decodes the string and then splits the resulting decoded | |
354 | * string into two based on the ":" character. That is: | |
355 | * <p/> | |
356 | * <code>String decoded = Base64.decodeToString(encoded);<br/> | |
357 | * return decoded.split(":");</code> | |
358 | * | |
359 | * @param scheme the {@link #getAuthcScheme() authcScheme} found in the request | |
360 | * {@link #getAuthzHeader(javax.servlet.ServletRequest) authzHeader}. It is ignored by this implementation, | |
361 | * but available to overriding implementations should they find it useful. | |
362 | * @param encoded the Base64-encoded username:password value found after the scheme in the header | |
363 | * @return the username (index 0)/password (index 1) pair obtained from the encoded header data. | |
364 | */ | |
365 | protected String[] getPrincipalsAndCredentials(String scheme, String encoded) { | |
366 | 3 | String decoded = Base64.decodeToString(encoded); |
367 | 3 | return decoded.split(":", 2); |
368 | } | |
369 | } |