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.providers;
17  
18  import org.springframework.security.AbstractAuthenticationManager;
19  import org.springframework.security.AccountExpiredException;
20  import org.springframework.security.SpringSecurityMessageSource;
21  import org.springframework.security.Authentication;
22  import org.springframework.security.AuthenticationException;
23  import org.springframework.security.AuthenticationServiceException;
24  import org.springframework.security.BadCredentialsException;
25  import org.springframework.security.CredentialsExpiredException;
26  import org.springframework.security.DisabledException;
27  import org.springframework.security.LockedException;
28  import org.springframework.security.AccountStatusException;
29  import org.springframework.security.concurrent.ConcurrentLoginException;
30  import org.springframework.security.concurrent.ConcurrentSessionController;
31  import org.springframework.security.concurrent.NullConcurrentSessionController;
32  import org.springframework.security.event.authentication.AbstractAuthenticationEvent;
33  import org.springframework.security.event.authentication.AuthenticationFailureBadCredentialsEvent;
34  import org.springframework.security.event.authentication.AuthenticationFailureConcurrentLoginEvent;
35  import org.springframework.security.event.authentication.AuthenticationFailureCredentialsExpiredEvent;
36  import org.springframework.security.event.authentication.AuthenticationFailureDisabledEvent;
37  import org.springframework.security.event.authentication.AuthenticationFailureExpiredEvent;
38  import org.springframework.security.event.authentication.AuthenticationFailureLockedEvent;
39  import org.springframework.security.event.authentication.AuthenticationFailureProviderNotFoundEvent;
40  import org.springframework.security.event.authentication.AuthenticationFailureProxyUntrustedEvent;
41  import org.springframework.security.event.authentication.AuthenticationFailureServiceExceptionEvent;
42  import org.springframework.security.event.authentication.AuthenticationSuccessEvent;
43  import org.springframework.security.userdetails.UsernameNotFoundException;
44  
45  import org.springframework.beans.factory.InitializingBean;
46  import org.springframework.context.ApplicationEvent;
47  import org.springframework.context.ApplicationEventPublisher;
48  import org.springframework.context.ApplicationEventPublisherAware;
49  import org.springframework.context.MessageSource;
50  import org.springframework.context.MessageSourceAware;
51  import org.springframework.context.support.MessageSourceAccessor;
52  import org.springframework.util.Assert;
53  
54  import org.apache.commons.logging.Log;
55  import org.apache.commons.logging.LogFactory;
56  
57  import java.lang.reflect.Constructor;
58  import java.lang.reflect.InvocationTargetException;
59  
60  import java.util.Iterator;
61  import java.util.List;
62  import java.util.Properties;
63  
64  
65  /**
66   * Iterates an {@link Authentication} request through a list of {@link AuthenticationProvider}s.
67   *
68   * Can optionally be configured with a {@link ConcurrentSessionController} to limit the number of sessions a user can
69   * have.
70   * <p>
71   * <tt>AuthenticationProvider</tt>s are usually tried in order until one provides a non-null response.
72   * A non-null response indicates the provider had authority to decide on the authentication request and no further
73   * providers are tried.
74   * If a subsequent provider successfully authenticates the request, the earlier authentication exception is disregarded
75   * and the successful authentication will be used. If no subsequent provider provides a non-null response, or a new
76   * <code>AuthenticationException</code>, the last <code>AuthenticationException</code> received will be used.
77   * If no provider returns a non-null response, or indicates it can even process an <code>Authentication</code>,
78   * the <code>ProviderManager</code> will throw a <code>ProviderNotFoundException</code>.
79   * <p>
80   * The exception to this process is when a provider throws an {@link AccountStatusException} or if the configured
81   * concurrent session controller throws a {@link ConcurrentLoginException}. In both these cases, no further providers
82   * in the list will be queried.
83   *
84   * <p>
85   * If a valid <code>Authentication</code> is returned by an <code>AuthenticationProvider</code>, the
86   * <code>ProviderManager</code> will publish an
87   * {@link org.springframework.security.event.authentication.AuthenticationSuccessEvent}. If an
88   * <code>AuthenticationException</code> is detected, the final <code>AuthenticationException</code> thrown will be
89   * used to publish an appropriate failure event. By default <code>ProviderManager</code> maps common exceptions to
90   * events, but this can be fine-tuned by providing a new <code>exceptionMappings</code><code>java.util.Properties</code>
91   * object. In the properties object, each of the keys represent the fully qualified classname of the exception, and
92   * each of the values represent the name of an event class which subclasses
93   * {@link org.springframework.security.event.authentication.AbstractAuthenticationFailureEvent}
94   * and provides its constructor.
95   *
96   *
97   * @author Ben Alex
98   * @version $Id: ProviderManager.java 3194 2008-07-30 11:01:23Z luke_t $
99   * @see ConcurrentSessionController
100  */
101 public class ProviderManager extends AbstractAuthenticationManager implements InitializingBean, MessageSourceAware,
102         ApplicationEventPublisherAware  {
103     //~ Static fields/initializers =====================================================================================
104 
105     private static final Log logger = LogFactory.getLog(ProviderManager.class);
106     private static final Properties DEFAULT_EXCEPTION_MAPPINGS = new Properties();
107 
108     //~ Instance fields ================================================================================================
109 
110     private ApplicationEventPublisher applicationEventPublisher;
111     private ConcurrentSessionController sessionController = new NullConcurrentSessionController();
112     private List providers;
113     protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
114     private Properties exceptionMappings = new Properties();
115     private Properties additionalExceptionMappings = new Properties();
116 
117     static {
118         DEFAULT_EXCEPTION_MAPPINGS.put(AccountExpiredException.class.getName(),
119                 AuthenticationFailureExpiredEvent.class.getName());
120         DEFAULT_EXCEPTION_MAPPINGS.put(AuthenticationServiceException.class.getName(),
121                 AuthenticationFailureServiceExceptionEvent.class.getName());
122         DEFAULT_EXCEPTION_MAPPINGS.put(LockedException.class.getName(),
123                 AuthenticationFailureLockedEvent.class.getName());
124         DEFAULT_EXCEPTION_MAPPINGS.put(CredentialsExpiredException.class.getName(),
125                 AuthenticationFailureCredentialsExpiredEvent.class.getName());
126         DEFAULT_EXCEPTION_MAPPINGS.put(DisabledException.class.getName(),
127                 AuthenticationFailureDisabledEvent.class.getName());
128         DEFAULT_EXCEPTION_MAPPINGS.put(BadCredentialsException.class.getName(),
129                 AuthenticationFailureBadCredentialsEvent.class.getName());
130         DEFAULT_EXCEPTION_MAPPINGS.put(UsernameNotFoundException.class.getName(),
131                 AuthenticationFailureBadCredentialsEvent.class.getName());
132         DEFAULT_EXCEPTION_MAPPINGS.put(ConcurrentLoginException.class.getName(),
133                 AuthenticationFailureConcurrentLoginEvent.class.getName());
134         DEFAULT_EXCEPTION_MAPPINGS.put(ProviderNotFoundException.class.getName(),
135                 AuthenticationFailureProviderNotFoundEvent.class.getName());
136         DEFAULT_EXCEPTION_MAPPINGS.put("org.springframework.security.providers.cas.ProxyUntrustedException",
137                 AuthenticationFailureProxyUntrustedEvent.class.getName());
138     }
139 
140     public ProviderManager() {
141         exceptionMappings.putAll(DEFAULT_EXCEPTION_MAPPINGS);
142     }
143 
144     //~ Methods ========================================================================================================
145 
146     public void afterPropertiesSet() throws Exception {
147         Assert.notNull(this.messages, "A message source must be set");
148         exceptionMappings.putAll(additionalExceptionMappings);
149     }
150 
151     /**
152      * Attempts to authenticate the passed {@link Authentication} object.
153      * <p>
154      * The list of {@link AuthenticationProvider}s will be successively tried until an
155      * <code>AuthenticationProvider</code> indicates it is  capable of authenticating the type of
156      * <code>Authentication</code> object passed. Authentication will then be attempted with that
157      * <code>AuthenticationProvider</code>.
158      * <p>
159      * If more than one <code>AuthenticationProvider</code> supports the passed <code>Authentication</code>
160      * object, only the first <code>AuthenticationProvider</code> tried will determine the result. No subsequent
161      * <code>AuthenticationProvider</code>s will be tried.
162      *
163      * @param authentication the authentication request object.
164      *
165      * @return a fully authenticated object including credentials.
166      *
167      * @throws AuthenticationException if authentication fails.
168      */
169     public Authentication doAuthentication(Authentication authentication) throws AuthenticationException {
170         Iterator iter = getProviders().iterator();
171 
172         Class toTest = authentication.getClass();
173 
174         AuthenticationException lastException = null;
175 
176         while (iter.hasNext()) {
177             AuthenticationProvider provider = (AuthenticationProvider) iter.next();
178 
179             if (!provider.supports(toTest)) {
180                 continue;
181             }
182 
183             logger.debug("Authentication attempt using " + provider.getClass().getName());
184 
185             Authentication result;
186 
187             try {
188                 result = provider.authenticate(authentication);
189 
190                 if (result != null) {
191                     copyDetails(authentication, result);
192                     sessionController.checkAuthenticationAllowed(result);
193                 }
194             } catch (AuthenticationException ae) {
195                 lastException = ae;
196                 result = null;
197             }
198 
199             // SEC-546: Avoid polling additional providers if auth failure is due to invalid account status or
200             // disallowed concurrent login.
201             if (lastException instanceof AccountStatusException || lastException instanceof ConcurrentLoginException) {
202                 break;
203             }
204 
205             if (result != null) {
206                 sessionController.registerSuccessfulAuthentication(result);
207                 publishEvent(new AuthenticationSuccessEvent(result));
208 
209                 return result;
210             }
211         }
212 
213         if (lastException == null) {
214             lastException = new ProviderNotFoundException(messages.getMessage("ProviderManager.providerNotFound",
215                         new Object[] {toTest.getName()}, "No AuthenticationProvider found for {0}"));
216         }
217 
218         publishAuthenticationFailure(lastException, authentication);
219 
220         throw lastException;
221     }
222 
223     private void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
224         String className = exceptionMappings.getProperty(exception.getClass().getName());
225         AbstractAuthenticationEvent event = null;
226 
227         if (className != null) {
228             try {
229                 Class clazz = getClass().getClassLoader().loadClass(className);
230                 Constructor constructor = clazz.getConstructor(new Class[] {
231                             Authentication.class, AuthenticationException.class
232                         });
233                 Object obj = constructor.newInstance(new Object[] {authentication, exception});
234                 Assert.isInstanceOf(AbstractAuthenticationEvent.class, obj, "Must be an AbstractAuthenticationEvent");
235                 event = (AbstractAuthenticationEvent) obj;
236             } catch (ClassNotFoundException ignored) {}
237             catch (NoSuchMethodException ignored) {}
238             catch (IllegalAccessException ignored) {}
239             catch (InstantiationException ignored) {}
240             catch (InvocationTargetException ignored) {}
241         }
242 
243         if (event != null) {
244             publishEvent(event);
245         } else {
246             if (logger.isDebugEnabled()) {
247                 logger.debug("No event was found for the exception " + exception.getClass().getName());
248             }
249         }
250 
251     }
252 
253     /**
254      * Copies the authentication details from a source Authentication object to a destination one, provided the
255      * latter does not already have one set.
256      *
257      * @param source source authentication
258      * @param dest the destination authentication object
259      */
260     private void copyDetails(Authentication source, Authentication dest) {
261         if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
262             AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
263 
264             token.setDetails(source.getDetails());
265         }
266     }
267 
268     public List getProviders() {
269         if (providers == null || providers.size() == 0) {
270             throw new IllegalArgumentException("A list of AuthenticationProviders is required");
271         }
272 
273         return providers;
274     }
275 
276     /**
277      * The configured {@link ConcurrentSessionController} is returned or the {@link
278      * NullConcurrentSessionController} if a specific one has not been set.
279      *
280      * @return {@link ConcurrentSessionController} instance
281      */
282     public ConcurrentSessionController getSessionController() {
283         return sessionController;
284     }
285 
286     public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
287         this.applicationEventPublisher = applicationEventPublisher;
288     }
289 
290     public void setMessageSource(MessageSource messageSource) {
291         this.messages = new MessageSourceAccessor(messageSource);
292     }
293 
294     /**
295      * Sets the {@link AuthenticationProvider} objects to be used for authentication.
296      *
297      * @param providers the list of authentication providers which will be used to process authentication requests.
298      *
299      * @throws IllegalArgumentException if the list is empty or null, or any of the elements in the list is not an
300      * AuthenticationProvider instance.
301      */
302     public void setProviders(List providers) {
303         Assert.notEmpty(providers, "A list of AuthenticationProviders is required");
304         Iterator iter = providers.iterator();
305 
306         while (iter.hasNext()) {
307             Object currentObject = iter.next();
308             Assert.isInstanceOf(AuthenticationProvider.class, currentObject,
309                     "Can only provide AuthenticationProvider instances");
310         }
311 
312         this.providers = providers;
313     }
314 
315     /**
316      * Set the {@link ConcurrentSessionController} to be used for limiting users' sessions. The {@link
317      * NullConcurrentSessionController} is used by default.
318      *
319      * @param sessionController {@link ConcurrentSessionController}
320      */
321     public void setSessionController(ConcurrentSessionController sessionController) {
322         this.sessionController = sessionController;
323     }
324 
325     private void publishEvent(ApplicationEvent event) {
326         if (applicationEventPublisher != null) {
327             applicationEventPublisher.publishEvent(event);
328         }
329     }
330 
331     /**
332      * Sets additional exception to event mappings. These are automatically merged with the default
333      * exception to event mappings that <code>ProviderManager</code> defines.
334      *
335      * @param additionalExceptionMappings where keys are the fully-qualified string name of the exception class and the
336      * values are the fully-qualified string name of the event class to fire.
337      */
338     public void setAdditionalExceptionMappings(Properties additionalExceptionMappings) {
339         this.additionalExceptionMappings = additionalExceptionMappings;
340     }
341 }