View Javadoc

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