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.acegisecurity.providers.dao;
17  
18  import org.acegisecurity.AccountExpiredException;
19  import org.acegisecurity.AcegiMessageSource;
20  import org.acegisecurity.Authentication;
21  import org.acegisecurity.AuthenticationException;
22  import org.acegisecurity.BadCredentialsException;
23  import org.acegisecurity.CredentialsExpiredException;
24  import org.acegisecurity.DisabledException;
25  import org.acegisecurity.LockedException;
26  
27  import org.acegisecurity.providers.AuthenticationProvider;
28  import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
29  import org.acegisecurity.providers.dao.cache.NullUserCache;
30  
31  import org.acegisecurity.userdetails.UserDetails;
32  import org.acegisecurity.userdetails.UserDetailsService;
33  import org.acegisecurity.userdetails.UsernameNotFoundException;
34  import org.acegisecurity.userdetails.UserDetailsChecker;
35  
36  import org.springframework.beans.factory.InitializingBean;
37  
38  import org.springframework.context.MessageSource;
39  import org.springframework.context.MessageSourceAware;
40  import org.springframework.context.support.MessageSourceAccessor;
41  
42  import org.springframework.util.Assert;
43  
44  
45  /**
46   * A base {@link AuthenticationProvider} that allows subclasses to override and work with {@link
47   * org.acegisecurity.userdetails.UserDetails} objects. The class is designed to respond to {@link
48   * UsernamePasswordAuthenticationToken} authentication requests.
49   *
50   * <p>
51   * Upon successful validation, a <code>UsernamePasswordAuthenticationToken</code> will be created and returned to the
52   * caller. The token will include as its principal either a <code>String</code> representation of the username, or the
53   * {@link UserDetails} that was returned from the authentication repository. Using <code>String</code> is appropriate
54   * if a container adapter is being used, as it expects <code>String</code> representations of the username.
55   * Using <code>UserDetails</code> is appropriate if you require access to additional properties of the authenticated
56   * user, such as email addresses, human-friendly names etc. As container adapters are not recommended to be used,
57   * and <code>UserDetails</code> implementations provide additional flexibility, by default a <code>UserDetails</code>
58   * is returned. To override this
59   * default, set the {@link #setForcePrincipalAsString} to <code>true</code>.
60   * </p>
61   *  <p>Caching is handled via the <code>UserDetails</code> object being placed in the {@link UserCache}. This
62   * ensures that subsequent requests with the same username can be validated without needing to query the {@link
63   * UserDetailsService}. It should be noted that if a user appears to present an incorrect password, the {@link
64   * UserDetailsService} will be queried to confirm the most up-to-date password was used for comparison.</p>
65   *
66   * @author Ben Alex
67   * @version $Id: AbstractUserDetailsAuthenticationProvider.java 2654 2008-02-18 20:44:09Z luke_t $
68   */
69  public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean,
70          MessageSourceAware {
71      //~ Instance fields ================================================================================================
72  
73      protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
74      private UserCache userCache = new NullUserCache();
75      private boolean forcePrincipalAsString = false;
76      protected boolean hideUserNotFoundExceptions = true;
77      private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
78      private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
79  
80      //~ Methods ========================================================================================================
81  
82      /**
83       * Allows subclasses to perform any additional checks of a returned (or cached) <code>UserDetails</code>
84       * for a given authentication request. Generally a subclass will at least compare the {@link
85       * Authentication#getCredentials()} with a {@link UserDetails#getPassword()}. If custom logic is needed to compare
86       * additional properties of <code>UserDetails</code> and/or <code>UsernamePasswordAuthenticationToken</code>,
87       * these should also appear in this method.
88       *
89       * @param userDetails as retrieved from the {@link #retrieveUser(String, UsernamePasswordAuthenticationToken)} or
90       *        <code>UserCache</code>
91       * @param authentication the current request that needs to be authenticated
92       *
93       * @throws AuthenticationException AuthenticationException if the credentials could not be validated (generally a
94       *         <code>BadCredentialsException</code>, an <code>AuthenticationServiceException</code>)
95       */
96      protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
97          UsernamePasswordAuthenticationToken authentication)
98          throws AuthenticationException;
99  
100     public final void afterPropertiesSet() throws Exception {
101         Assert.notNull(this.userCache, "A user cache must be set");
102         Assert.notNull(this.messages, "A message source must be set");
103         doAfterPropertiesSet();
104     }
105 
106     public Authentication authenticate(Authentication authentication)
107         throws AuthenticationException {
108         Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
109             messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
110                 "Only UsernamePasswordAuthenticationToken is supported"));
111 
112         // Determine username
113         String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
114 
115         boolean cacheWasUsed = true;
116         UserDetails user = this.userCache.getUserFromCache(username);
117 
118         if (user == null) {
119             cacheWasUsed = false;
120 
121             try {
122                 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
123             } catch (UsernameNotFoundException notFound) {
124                 if (hideUserNotFoundExceptions) {
125                     throw new BadCredentialsException(messages.getMessage(
126                             "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
127                 } else {
128                     throw notFound;
129                 }
130             }
131 
132             Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
133         }
134 
135         preAuthenticationChecks.check(user);
136 
137         // This check must come here, as we don't want to tell users
138         // about account status unless they presented the correct credentials
139         try {
140             additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
141         } catch (AuthenticationException exception) {
142             if (cacheWasUsed) {
143                 // There was a problem, so try again after checking
144                 // we're using latest data (ie not from the cache)
145                 cacheWasUsed = false;
146                 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
147                 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
148             } else {
149                 throw exception;
150             }
151         }
152 
153         postAuthenticationChecks.check(user);
154 
155         if (!cacheWasUsed) {
156             this.userCache.putUserInCache(user);
157         }
158 
159         Object principalToReturn = user;
160 
161         if (forcePrincipalAsString) {
162             principalToReturn = user.getUsername();
163         }
164 
165         return createSuccessAuthentication(principalToReturn, authentication, user);
166     }
167 
168     /**
169      * Creates a successful {@link Authentication} object.<p>Protected so subclasses can override.</p>
170      *  <p>Subclasses will usually store the original credentials the user supplied (not salted or encoded
171      * passwords) in the returned <code>Authentication</code> object.</p>
172      *
173      * @param principal that should be the principal in the returned object (defined by the {@link
174      *        #isForcePrincipalAsString()} method)
175      * @param authentication that was presented to the provider for validation
176      * @param user that was loaded by the implementation
177      *
178      * @return the successful authentication token
179      */
180     protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
181         UserDetails user) {
182         // Ensure we return the original credentials the user supplied,
183         // so subsequent attempts are successful even with encoded passwords.
184         // Also ensure we return the original getDetails(), so that future
185         // authentication events after cache expiry contain the details
186         UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
187                 authentication.getCredentials(), user.getAuthorities());
188         result.setDetails(authentication.getDetails());
189 
190         return result;
191     }
192 
193     protected void doAfterPropertiesSet() throws Exception {}
194 
195     public UserCache getUserCache() {
196         return userCache;
197     }
198 
199     public boolean isForcePrincipalAsString() {
200         return forcePrincipalAsString;
201     }
202 
203     public boolean isHideUserNotFoundExceptions() {
204         return hideUserNotFoundExceptions;
205     }
206 
207     /**
208      * Allows subclasses to actually retrieve the <code>UserDetails</code> from an implementation-specific
209      * location, with the option of throwing an <code>AuthenticationException</code> immediately if the presented
210      * credentials are incorrect (this is especially useful if it is necessary to bind to a resource as the user in
211      * order to obtain or generate a <code>UserDetails</code>).<p>Subclasses are not required to perform any
212      * caching, as the <code>AbstractUserDetailsAuthenticationProvider</code> will by default cache the
213      * <code>UserDetails</code>. The caching of <code>UserDetails</code> does present additional complexity as this
214      * means subsequent requests that rely on the cache will need to still have their credentials validated, even if
215      * the correctness of credentials was assured by subclasses adopting a binding-based strategy in this method.
216      * Accordingly it is important that subclasses either disable caching (if they want to ensure that this method is
217      * the only method that is capable of authenticating a request, as no <code>UserDetails</code> will ever be
218      * cached) or ensure subclasses implement {@link #additionalAuthenticationChecks(UserDetails,
219      * UsernamePasswordAuthenticationToken)} to compare the credentials of a cached <code>UserDetails</code> with
220      * subsequent authentication requests.</p>
221      *  <p>Most of the time subclasses will not perform credentials inspection in this method, instead
222      * performing it in {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)} so
223      * that code related to credentials validation need not be duplicated across two methods.</p>
224      *
225      * @param username The username to retrieve
226      * @param authentication The authentication request, which subclasses <em>may</em> need to perform a binding-based
227      *        retrieval of the <code>UserDetails</code>
228      *
229      * @return the user information (never <code>null</code> - instead an exception should the thrown)
230      *
231      * @throws AuthenticationException if the credentials could not be validated (generally a
232      *         <code>BadCredentialsException</code>, an <code>AuthenticationServiceException</code> or
233      *         <code>UsernameNotFoundException</code>)
234      */
235     protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
236         throws AuthenticationException;
237 
238     public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
239         this.forcePrincipalAsString = forcePrincipalAsString;
240     }
241 
242     /**
243      * By default the <code>AbstractUserDetailsAuthenticationProvider</code> throws a
244      * <code>BadCredentialsException</code> if a username is not found or the password is incorrect. Setting this
245      * property to <code>false</code> will cause <code>UsernameNotFoundException</code>s to be thrown instead for the
246      * former. Note this is considered less secure than throwing <code>BadCredentialsException</code> for both
247      * exceptions.
248      *
249      * @param hideUserNotFoundExceptions set to <code>false</code> if you wish <code>UsernameNotFoundException</code>s
250      *        to be thrown instead of the non-specific <code>BadCredentialsException</code> (defaults to
251      *        <code>true</code>)
252      */
253     public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
254         this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
255     }
256 
257     public void setMessageSource(MessageSource messageSource) {
258         this.messages = new MessageSourceAccessor(messageSource);
259     }
260 
261     public void setUserCache(UserCache userCache) {
262         this.userCache = userCache;
263     }
264 
265     protected UserDetailsChecker getPreAuthenticationChecks() {
266         return preAuthenticationChecks;
267     }
268 
269     /**
270      * Sets the policy will be used to verify the status of the loaded <tt>UserDetails</tt> <em>before</em>
271      * validation of the credentials takes place.
272      *
273      * @param preAuthenticationChecks strategy to be invoked prior to authentication. 
274      */
275     public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
276         this.preAuthenticationChecks = preAuthenticationChecks;
277     }
278 
279     protected UserDetailsChecker getPostAuthenticationChecks() {
280         return postAuthenticationChecks;
281     }
282 
283     public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
284         this.postAuthenticationChecks = postAuthenticationChecks;
285     }    
286 
287     public boolean supports(Class authentication) {
288         return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
289     }
290 
291     private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
292         public void check(UserDetails user) {
293             if (!user.isAccountNonLocked()) {
294                 throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
295                         "User account is locked"), user);
296             }
297 
298             if (!user.isEnabled()) {
299                 throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled",
300                         "User is disabled"), user);
301             }
302 
303             if (!user.isAccountNonExpired()) {
304                 throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired",
305                         "User account has expired"), user);
306             }
307         }
308     }
309 
310     private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
311         public void check(UserDetails user) {
312             if (!user.isCredentialsNonExpired()) {
313                 throw new CredentialsExpiredException(messages.getMessage(
314                         "AbstractUserDetailsAuthenticationProvider.credentialsExpired",
315                         "User credentials have expired"), user);
316             }
317         }
318     }
319 }