View Javadoc

1   /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.springframework.security.ui;
17  
18  import org.springframework.security.SpringSecurityMessageSource;
19  import org.springframework.security.Authentication;
20  import org.springframework.security.AuthenticationException;
21  import org.springframework.security.AuthenticationManager;
22  import org.springframework.security.util.RedirectUtils;
23  import org.springframework.security.util.SessionUtils;
24  import org.springframework.security.util.UrlUtils;
25  
26  import org.springframework.security.concurrent.SessionRegistry;
27  import org.springframework.security.context.SecurityContextHolder;
28  
29  import org.springframework.security.event.authentication.InteractiveAuthenticationSuccessEvent;
30  
31  import org.springframework.security.ui.rememberme.NullRememberMeServices;
32  import org.springframework.security.ui.rememberme.RememberMeServices;
33  import org.springframework.security.ui.savedrequest.SavedRequest;
34  
35  import org.springframework.beans.factory.InitializingBean;
36  
37  import org.springframework.context.ApplicationEventPublisher;
38  import org.springframework.context.ApplicationEventPublisherAware;
39  import org.springframework.context.MessageSource;
40  import org.springframework.context.MessageSourceAware;
41  import org.springframework.context.support.MessageSourceAccessor;
42  
43  import org.springframework.util.Assert;
44  
45  import java.io.IOException;
46  
47  import java.util.Properties;
48  
49  import javax.servlet.FilterChain;
50  import javax.servlet.ServletException;
51  import javax.servlet.http.HttpServletRequest;
52  import javax.servlet.http.HttpServletResponse;
53  import javax.servlet.http.HttpSession;
54  
55  /**
56   * Abstract processor of browser-based HTTP-based authentication requests.
57   * <p>
58   * This filter is responsible for processing authentication requests. If
59   * authentication is successful, the resulting {@link Authentication} object
60   * will be placed into the <code>SecurityContext</code>, which is guaranteed
61   * to have already been created by an earlier filter.
62   * <p>
63   * If authentication fails, the <code>AuthenticationException</code> will be
64   * placed into the <code>HttpSession</code> with the attribute defined by
65   * {@link #SPRING_SECURITY_LAST_EXCEPTION_KEY}.
66   * <p>
67   * To use this filter, it is necessary to specify the following properties:
68   * <ul>
69   * <li><code>defaultTargetUrl</code> indicates the URL that should be used
70   * for redirection if the <code>HttpSession</code> attribute named
71   * {@link #SPRING_SECURITY_SAVED_REQUEST_KEY} does not indicate the target URL once
72   * authentication is completed successfully. eg: <code>/</code>. The
73   * <code>defaultTargetUrl</code> will be treated as relative to the web-app's
74   * context path, and should include the leading <code>/</code>.
75   * Alternatively, inclusion of a scheme name (eg http:// or https://) as the
76   * prefix will denote a fully-qualified URL and this is also supported. More
77   * complex behaviour can be implemented by using a customised {@link TargetUrlResolver}.</li>
78   * <li><code>authenticationFailureUrl</code> (optional) indicates the URL that should be
79   * used for redirection if the authentication request fails. eg:
80   * <code>/login.jsp?login_error=1</code>. If not configured, <tt>sendError</tt> will be
81   * called on the response, with the error code SC_UNAUTHORIZED.</li>
82   * <li><code>filterProcessesUrl</code> indicates the URL that this filter
83   * will respond to. This parameter varies by subclass.</li>
84   * <li><code>alwaysUseDefaultTargetUrl</code> causes successful
85   * authentication to always redirect to the <code>defaultTargetUrl</code>,
86   * even if the <code>HttpSession</code> attribute named {@link
87   * # SPRING_SECURITY_SAVED_REQUEST_KEY} defines the intended target URL.</li>
88   * </ul>
89   * <p>
90   * To configure this filter to redirect to specific pages as the result of
91   * specific {@link AuthenticationException}s you can do the following.
92   * Configure the <code>exceptionMappings</code> property in your application
93   * xml. This property is a java.util.Properties object that maps a
94   * fully-qualified exception class name to a redirection url target. For
95   * example:
96   *
97   * <pre>
98   *  &lt;property name=&quot;exceptionMappings&quot;&gt;
99   *    &lt;props&gt;
100  *      &lt;prop&gt; key=&quot;org.springframework.security.BadCredentialsException&quot;&gt;/bad_credentials.jsp&lt;/prop&gt;
101  *    &lt;/props&gt;
102  *  &lt;/property&gt;
103  * </pre>
104  *
105  * The example above would redirect all
106  * {@link org.springframework.security.BadCredentialsException}s thrown, to a page in the
107  * web-application called /bad_credentials.jsp.
108  * <p>
109  * Any {@link AuthenticationException} thrown that cannot be matched in the
110  * <code>exceptionMappings</code> will be redirected to the
111  * <code>authenticationFailureUrl</code>
112  * <p>
113  * If authentication is successful, an {@link
114  * org.springframework.security.event.authentication.InteractiveAuthenticationSuccessEvent}
115  * will be published to the application context. No events will be published if
116  * authentication was unsuccessful, because this would generally be recorded via
117  * an <code>AuthenticationManager</code>-specific application event.
118  * <p>
119  * The filter has an optional attribute <tt>invalidateSessionOnSuccessfulAuthentication</tt> that will invalidate
120  * the current session on successful authentication. This is to protect against session fixation attacks (see
121  * <a href="http://en.wikipedia.org/wiki/Session_fixation">this Wikipedia article</a> for more information).
122  * The behaviour is turned off by default. Additionally there is a property <tt>migrateInvalidatedSessionAttributes</tt>
123  * which tells if on session invalidation we are to migrate all session attributes from the old session to a newly
124  * created one. This is turned on by default, but not used unless <tt>invalidateSessionOnSuccessfulAuthentication</tt>
125  * is true.
126  *
127  * @author Ben Alex
128  * @version $Id: AbstractProcessingFilter.java 3187 2008-07-15 14:52:13Z luke_t $
129  */
130 public abstract class AbstractProcessingFilter extends SpringSecurityFilter implements InitializingBean,
131         ApplicationEventPublisherAware, MessageSourceAware {
132     //~ Static fields/initializers =====================================================================================
133 
134     public static final String SPRING_SECURITY_SAVED_REQUEST_KEY = "SPRING_SECURITY_SAVED_REQUEST_KEY";
135 
136     public static final String SPRING_SECURITY_LAST_EXCEPTION_KEY = "SPRING_SECURITY_LAST_EXCEPTION";
137 
138     //~ Instance fields ================================================================================================
139 
140     protected ApplicationEventPublisher eventPublisher;
141 
142     protected AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
143 
144     private AuthenticationManager authenticationManager;
145 
146     protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
147 
148     private Properties exceptionMappings = new Properties();
149 
150     /** 
151      * Delay use of NullRememberMeServices until initialization so that namespace has a chance to inject
152      * the RememberMeServices implementation into custom implementations.
153      */ 
154     private RememberMeServices rememberMeServices = null;
155 
156     private TargetUrlResolver targetUrlResolver = new TargetUrlResolverImpl();
157     
158     /** Where to redirect the browser to if authentication fails */
159     private String authenticationFailureUrl;
160 
161     /**
162      * Where to redirect the browser to if authentication is successful but
163      * SPRING_SECURITY_SAVED_REQUEST_KEY is <code>null</code>
164      */
165     private String defaultTargetUrl;
166 
167     /**
168      * The URL destination that this filter intercepts and processes (usually
169      * something like <code>/j_spring_security_check</code>)
170      */
171     private String filterProcessesUrl = getDefaultFilterProcessesUrl();
172 
173     /**
174      * If <code>true</code>, will always redirect to the value of
175      * {@link #getDefaultTargetUrl} upon successful authentication, irrespective
176      * of the page that caused the authentication request (defaults to
177      * <code>false</code>).
178      */
179     private boolean alwaysUseDefaultTargetUrl = false;
180 
181     /**
182      * Indicates if the filter chain should be continued prior to delegation to
183      * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse,
184      * Authentication)}, which may be useful in certain environment (eg
185      * Tapestry). Defaults to <code>false</code>.
186      */
187     private boolean continueChainBeforeSuccessfulAuthentication = false;
188 
189     /**
190      * If true, causes any redirection URLs to be calculated minus the protocol
191      * and context path (defaults to false).
192      */
193     private boolean useRelativeContext = false;
194 
195 
196     /**
197      * Tells if we on successful authentication should invalidate the
198      * current session. This is a common guard against session fixation attacks.
199      * Defaults to <code>false</code>.
200      */
201     private boolean invalidateSessionOnSuccessfulAuthentication = false;
202 
203     /**
204      * If {@link #invalidateSessionOnSuccessfulAuthentication} is true, this
205      * flag indicates that the session attributes of the session to be invalidated
206      * are to be migrated to the new session. Defaults to <code>true</code> since
207      * nothing will happpen unless {@link #invalidateSessionOnSuccessfulAuthentication}
208      * is true.
209      */
210     private boolean migrateInvalidatedSessionAttributes = true;
211 
212     private boolean allowSessionCreation = true;
213     
214     private boolean serverSideRedirect = false;
215     
216     private SessionRegistry sessionRegistry;
217 
218     //~ Methods ========================================================================================================
219 
220     public void afterPropertiesSet() throws Exception {
221         Assert.hasLength(filterProcessesUrl, "filterProcessesUrl must be specified");
222         Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), filterProcessesUrl + " isn't a valid redirect URL");        
223         Assert.hasLength(defaultTargetUrl, "defaultTargetUrl must be specified");
224         Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultTargetUrl), defaultTargetUrl + " isn't a valid redirect URL");        
225         Assert.isTrue(UrlUtils.isValidRedirectUrl(authenticationFailureUrl), authenticationFailureUrl + " isn't a valid redirect URL");
226         Assert.notNull(authenticationManager, "authenticationManager must be specified");
227         Assert.notNull(targetUrlResolver, "targetUrlResolver cannot be null");
228         
229         if (rememberMeServices == null) {
230             rememberMeServices = new NullRememberMeServices();
231         }
232     }
233 
234     /**
235      * Performs actual authentication.
236      *
237      * @param request from which to extract parameters and perform the
238      * authentication
239      *
240      * @return the authenticated user
241      *
242      * @throws AuthenticationException if authentication fails
243      */
244     public abstract Authentication attemptAuthentication(HttpServletRequest request) throws AuthenticationException;
245 
246     public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException,
247             ServletException {
248 
249         if (requiresAuthentication(request, response)) {
250             if (logger.isDebugEnabled()) {
251                 logger.debug("Request is to process authentication");
252             }
253 
254             Authentication authResult;
255 
256             try {
257                 onPreAuthentication(request, response);
258                 authResult = attemptAuthentication(request);
259             }
260             catch (AuthenticationException failed) {
261                 // Authentication failed
262                 unsuccessfulAuthentication(request, response, failed);
263 
264                 return;
265             }
266 
267             // Authentication success
268             if (continueChainBeforeSuccessfulAuthentication) {
269                 chain.doFilter(request, response);
270             }
271 
272             successfulAuthentication(request, response, authResult);
273 
274             return;
275         }
276 
277         chain.doFilter(request, response);
278     }
279 
280     public static String obtainFullSavedRequestUrl(HttpServletRequest request) {
281         SavedRequest savedRequest = getSavedRequest(request);
282         
283         return savedRequest == null ? null : savedRequest.getFullRequestUrl();
284     }
285 
286     private static SavedRequest getSavedRequest(HttpServletRequest request) {
287         HttpSession session = request.getSession(false);
288 
289         if (session == null) {
290             return null;
291         }
292 
293         SavedRequest savedRequest = (SavedRequest) session.getAttribute(SPRING_SECURITY_SAVED_REQUEST_KEY);
294 
295         return savedRequest;
296      }
297     
298     protected void onPreAuthentication(HttpServletRequest request, HttpServletResponse response)
299             throws AuthenticationException, IOException {
300     }
301 
302     protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
303             Authentication authResult) throws IOException {
304     }
305 
306     protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
307             AuthenticationException failed) throws IOException {
308     }
309 
310     /**
311      * <p>
312      * Indicates whether this filter should attempt to process a login request
313      * for the current invocation.
314      * </p>
315      * <p>
316      * It strips any parameters from the "path" section of the request URL (such
317      * as the jsessionid parameter in
318      * <em>http://host/myapp/index.html;jsessionid=blah</em>) before matching
319      * against the <code>filterProcessesUrl</code> property.
320      * </p>
321      * <p>
322      * Subclasses may override for special requirements, such as Tapestry
323      * integration.
324      * </p>
325      *
326      * @param request as received from the filter chain
327      * @param response as received from the filter chain
328      *
329      * @return <code>true</code> if the filter should attempt authentication,
330      * <code>false</code> otherwise
331      */
332     protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
333         String uri = request.getRequestURI();
334         int pathParamIndex = uri.indexOf(';');
335 
336         if (pathParamIndex > 0) {
337             // strip everything after the first semi-colon
338             uri = uri.substring(0, pathParamIndex);
339         }
340 
341         if ("".equals(request.getContextPath())) {
342             return uri.endsWith(filterProcessesUrl);
343         }
344 
345         return uri.endsWith(request.getContextPath() + filterProcessesUrl);
346     }
347 
348     protected void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url)
349             throws IOException {
350 
351         RedirectUtils.sendRedirect(request, response, url, useRelativeContext);
352     }
353 
354     protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
355             Authentication authResult) throws IOException, ServletException {
356         if (logger.isDebugEnabled()) {
357             logger.debug("Authentication success: " + authResult.toString());
358         }
359 
360         SecurityContextHolder.getContext().setAuthentication(authResult);
361 
362         if (logger.isDebugEnabled()) {
363             logger.debug("Updated SecurityContextHolder to contain the following Authentication: '" + authResult + "'");
364         }
365 
366         if (invalidateSessionOnSuccessfulAuthentication) {
367             SessionUtils.startNewSessionIfRequired(request, migrateInvalidatedSessionAttributes, sessionRegistry);
368         }
369 
370         String targetUrl = determineTargetUrl(request);
371 
372         if (logger.isDebugEnabled()) {
373             logger.debug("Redirecting to target URL from HTTP Session (or default): " + targetUrl);
374         }
375 
376         onSuccessfulAuthentication(request, response, authResult);
377 
378         rememberMeServices.loginSuccess(request, response, authResult);
379 
380         // Fire event
381         if (this.eventPublisher != null) {
382             eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
383         }
384 
385         sendRedirect(request, response, targetUrl);
386     }
387 
388     protected String determineTargetUrl(HttpServletRequest request) {
389         // Don't attempt to obtain the url from the saved request if alwaysUsedefaultTargetUrl is set
390         String targetUrl = alwaysUseDefaultTargetUrl ? null : 
391             targetUrlResolver.determineTargetUrl(getSavedRequest(request), request, SecurityContextHolder.getContext().getAuthentication());
392 
393         if (targetUrl == null) {
394             targetUrl = getDefaultTargetUrl();
395         }
396 
397         return targetUrl;
398     }
399 
400     protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
401             AuthenticationException failed) throws IOException, ServletException {
402         SecurityContextHolder.getContext().setAuthentication(null);
403 
404         if (logger.isDebugEnabled()) {
405             logger.debug("Updated SecurityContextHolder to contain null Authentication");
406         }
407 
408         String failureUrl = determineFailureUrl(request, failed);
409 
410         if (logger.isDebugEnabled()) {
411             logger.debug("Authentication request failed: " + failed.toString());
412         }
413 
414         try {
415             HttpSession session = request.getSession(false);
416 
417             if (session != null || allowSessionCreation) {
418                 request.getSession().setAttribute(SPRING_SECURITY_LAST_EXCEPTION_KEY, failed);
419             }
420         }
421         catch (Exception ignored) {
422         }
423 
424         onUnsuccessfulAuthentication(request, response, failed);
425 
426         rememberMeServices.loginFail(request, response);
427         
428         if (failureUrl == null) {
429             response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed:" + failed.getMessage());
430         } else if (serverSideRedirect){
431             request.getRequestDispatcher(failureUrl).forward(request, response);            
432         } else {
433             sendRedirect(request, response, failureUrl);
434         }
435     }
436 
437     protected String determineFailureUrl(HttpServletRequest request, AuthenticationException failed) {
438         return exceptionMappings.getProperty(failed.getClass().getName(), authenticationFailureUrl);
439     }
440 
441     public String getAuthenticationFailureUrl() {
442         return authenticationFailureUrl;
443     }
444 
445     public void setAuthenticationFailureUrl(String authenticationFailureUrl) {
446         this.authenticationFailureUrl = authenticationFailureUrl;
447     }
448 
449     protected AuthenticationManager getAuthenticationManager() {
450         return authenticationManager;
451     }
452 
453     public void setAuthenticationManager(AuthenticationManager authenticationManager) {
454         this.authenticationManager = authenticationManager;
455     }
456 
457     /**
458      * Specifies the default <code>filterProcessesUrl</code> for the
459      * implementation.
460      *
461      * @return the default <code>filterProcessesUrl</code>
462      */
463     public abstract String getDefaultFilterProcessesUrl();
464 
465     /**
466      * Supplies the default target Url that will be used if no saved request is
467      * found or the <tt>alwaysUseDefaultTargetUrl</tt> propert is set to true.
468      * Override this method of you want to provide a customized default Url (for
469      * example if you want different Urls depending on the authorities of the
470      * user who has just logged in).
471      *
472      * @return the defaultTargetUrl property
473      */
474     public String getDefaultTargetUrl() {
475         return defaultTargetUrl;
476     }
477 
478     public void setDefaultTargetUrl(String defaultTargetUrl) {
479         Assert.isTrue(defaultTargetUrl.startsWith("/") | defaultTargetUrl.startsWith("http"),
480                 "defaultTarget must start with '/' or with 'http(s)'");
481         this.defaultTargetUrl = defaultTargetUrl;
482     }
483 
484     Properties getExceptionMappings() {
485         return new Properties(exceptionMappings);
486     }
487 
488     public void setExceptionMappings(Properties exceptionMappings) {
489         this.exceptionMappings = exceptionMappings;
490     }
491 
492     public String getFilterProcessesUrl() {
493         return filterProcessesUrl;
494     }
495 
496     public void setFilterProcessesUrl(String filterProcessesUrl) {
497         this.filterProcessesUrl = filterProcessesUrl;
498     }
499 
500     public RememberMeServices getRememberMeServices() {
501         return rememberMeServices;
502     }
503 
504     public void setRememberMeServices(RememberMeServices rememberMeServices) {
505         this.rememberMeServices = rememberMeServices;
506     }
507 
508     boolean isAlwaysUseDefaultTargetUrl() {
509         return alwaysUseDefaultTargetUrl;
510     }
511 
512     public void setAlwaysUseDefaultTargetUrl(boolean alwaysUseDefaultTargetUrl) {
513         this.alwaysUseDefaultTargetUrl = alwaysUseDefaultTargetUrl;
514     }
515 
516     public void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
517         this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication;
518     }
519 
520     public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
521         this.eventPublisher = eventPublisher;
522     }
523 
524     public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
525         Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
526         this.authenticationDetailsSource = authenticationDetailsSource;
527     }
528 
529     public void setMessageSource(MessageSource messageSource) {
530         this.messages = new MessageSourceAccessor(messageSource);
531     }
532 
533     public void setInvalidateSessionOnSuccessfulAuthentication(boolean invalidateSessionOnSuccessfulAuthentication) {
534         this.invalidateSessionOnSuccessfulAuthentication = invalidateSessionOnSuccessfulAuthentication;
535     }
536 
537     public void setMigrateInvalidatedSessionAttributes(boolean migrateInvalidatedSessionAttributes) {
538         this.migrateInvalidatedSessionAttributes = migrateInvalidatedSessionAttributes;
539     }
540 
541     public AuthenticationDetailsSource getAuthenticationDetailsSource() {
542         // Required due to SEC-310
543         return authenticationDetailsSource;
544     }
545 
546     public void setUseRelativeContext(boolean useRelativeContext) {
547         this.useRelativeContext = useRelativeContext;
548     }
549 
550     protected boolean getAllowSessionCreation() {
551         return allowSessionCreation;
552     }
553 
554     public void setAllowSessionCreation(boolean allowSessionCreation) {
555         this.allowSessionCreation = allowSessionCreation;
556     }
557 
558     /**
559      * @return the targetUrlResolver
560      */
561     protected TargetUrlResolver getTargetUrlResolver() {
562         return targetUrlResolver;
563     }
564 
565     /**
566      * @param targetUrlResolver the targetUrlResolver to set
567      */
568     public void setTargetUrlResolver(TargetUrlResolver targetUrlResolver) {
569         this.targetUrlResolver = targetUrlResolver;
570     }
571 
572     /**
573      * Tells if we are to do a server side include of the error URL instead of a 302 redirect.
574      *
575      * @param serverSideRedirect
576      */    
577     public void setServerSideRedirect(boolean serverSideRedirect) {
578         this.serverSideRedirect = serverSideRedirect;
579     }
580 
581     /**
582      * The session registry needs to be set if session fixation attack protection is in use (and concurrent 
583      * session control is enabled).
584      */
585     public void setSessionRegistry(SessionRegistry sessionRegistry) {
586         this.sessionRegistry = sessionRegistry;
587     }
588 }