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.dao;
17  
18  import org.springframework.security.AccountExpiredException;
19  import org.springframework.security.SpringSecurityMessageSource;
20  import org.springframework.security.Authentication;
21  import org.springframework.security.AuthenticationException;
22  import org.springframework.security.BadCredentialsException;
23  import org.springframework.security.CredentialsExpiredException;
24  import org.springframework.security.DisabledException;
25  import org.springframework.security.LockedException;
26  
27  import org.springframework.security.providers.AuthenticationProvider;
28  import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
29  import org.springframework.security.providers.dao.cache.NullUserCache;
30  
31  import org.springframework.security.userdetails.UserDetails;
32  import org.springframework.security.userdetails.UserDetailsService;
33  import org.springframework.security.userdetails.UsernameNotFoundException;
34  import org.springframework.security.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.springframework.security.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   * 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 2856 2008-04-04 19:40:28Z luke_t $
68   */
69  public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean,
70          MessageSourceAware {
71      //~ Instance fields ================================================================================================
72  
73      protected MessageSourceAccessor messages = SpringSecurityMessageSource.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) throws AuthenticationException {
107         Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
108             messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
109                 "Only UsernamePasswordAuthenticationToken is supported"));
110 
111         // Determine username
112         String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
113 
114         boolean cacheWasUsed = true;
115         UserDetails user = this.userCache.getUserFromCache(username);
116 
117         if (user == null) {
118             cacheWasUsed = false;
119 
120             try {
121                 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
122             } catch (UsernameNotFoundException notFound) {
123                 if (hideUserNotFoundExceptions) {
124                     throw new BadCredentialsException(messages.getMessage(
125                             "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
126                 } else {
127                     throw notFound;
128                 }
129             }
130 
131             Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
132         }
133 
134         preAuthenticationChecks.check(user);
135         
136         try {
137             additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
138         } catch (AuthenticationException exception) {
139             if (cacheWasUsed) {
140                 // There was a problem, so try again after checking
141                 // we're using latest data (ie not from the cache)
142                 cacheWasUsed = false;
143                 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
144                 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
145             } else {
146                 throw exception;
147             }
148         }
149 
150         postAuthenticationChecks.check(user);
151 
152         if (!cacheWasUsed) {
153             this.userCache.putUserInCache(user);
154         }
155 
156         Object principalToReturn = user;
157 
158         if (forcePrincipalAsString) {
159             principalToReturn = user.getUsername();
160         }
161 
162         return createSuccessAuthentication(principalToReturn, authentication, user);
163     }
164 
165     /**
166      * Creates a successful {@link Authentication} object.<p>Protected so subclasses can override.</p>
167      *  <p>Subclasses will usually store the original credentials the user supplied (not salted or encoded
168      * passwords) in the returned <code>Authentication</code> object.</p>
169      *
170      * @param principal that should be the principal in the returned object (defined by the {@link
171      *        #isForcePrincipalAsString()} method)
172      * @param authentication that was presented to the provider for validation
173      * @param user that was loaded by the implementation
174      *
175      * @return the successful authentication token
176      */
177     protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
178         UserDetails user) {
179         // Ensure we return the original credentials the user supplied,
180         // so subsequent attempts are successful even with encoded passwords.
181         // Also ensure we return the original getDetails(), so that future
182         // authentication events after cache expiry contain the details
183         UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
184                 authentication.getCredentials(), user.getAuthorities());
185         result.setDetails(authentication.getDetails());
186 
187         return result;
188     }
189 
190     protected void doAfterPropertiesSet() throws Exception {}
191 
192     public UserCache getUserCache() {
193         return userCache;
194     }
195 
196     public boolean isForcePrincipalAsString() {
197         return forcePrincipalAsString;
198     }
199 
200     public boolean isHideUserNotFoundExceptions() {
201         return hideUserNotFoundExceptions;
202     }
203 
204     /**
205      * Allows subclasses to actually retrieve the <code>UserDetails</code> from an implementation-specific
206      * location, with the option of throwing an <code>AuthenticationException</code> immediately if the presented
207      * credentials are incorrect (this is especially useful if it is necessary to bind to a resource as the user in
208      * order to obtain or generate a <code>UserDetails</code>).<p>Subclasses are not required to perform any
209      * caching, as the <code>AbstractUserDetailsAuthenticationProvider</code> will by default cache the
210      * <code>UserDetails</code>. The caching of <code>UserDetails</code> does present additional complexity as this
211      * means subsequent requests that rely on the cache will need to still have their credentials validated, even if
212      * the correctness of credentials was assured by subclasses adopting a binding-based strategy in this method.
213      * Accordingly it is important that subclasses either disable caching (if they want to ensure that this method is
214      * the only method that is capable of authenticating a request, as no <code>UserDetails</code> will ever be
215      * cached) or ensure subclasses implement {@link #additionalAuthenticationChecks(UserDetails,
216      * UsernamePasswordAuthenticationToken)} to compare the credentials of a cached <code>UserDetails</code> with
217      * subsequent authentication requests.</p>
218      *  <p>Most of the time subclasses will not perform credentials inspection in this method, instead
219      * performing it in {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)} so
220      * that code related to credentials validation need not be duplicated across two methods.</p>
221      *
222      * @param username The username to retrieve
223      * @param authentication The authentication request, which subclasses <em>may</em> need to perform a binding-based
224      *        retrieval of the <code>UserDetails</code>
225      *
226      * @return the user information (never <code>null</code> - instead an exception should the thrown)
227      *
228      * @throws AuthenticationException if the credentials could not be validated (generally a
229      *         <code>BadCredentialsException</code>, an <code>AuthenticationServiceException</code> or
230      *         <code>UsernameNotFoundException</code>)
231      */
232     protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
233         throws AuthenticationException;
234 
235     public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
236         this.forcePrincipalAsString = forcePrincipalAsString;
237     }
238 
239     /**
240      * By default the <code>AbstractUserDetailsAuthenticationProvider</code> throws a
241      * <code>BadCredentialsException</code> if a username is not found or the password is incorrect. Setting this
242      * property to <code>false</code> will cause <code>UsernameNotFoundException</code>s to be thrown instead for the
243      * former. Note this is considered less secure than throwing <code>BadCredentialsException</code> for both
244      * exceptions.
245      *
246      * @param hideUserNotFoundExceptions set to <code>false</code> if you wish <code>UsernameNotFoundException</code>s
247      *        to be thrown instead of the non-specific <code>BadCredentialsException</code> (defaults to
248      *        <code>true</code>)
249      */
250     public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
251         this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
252     }
253 
254     public void setMessageSource(MessageSource messageSource) {
255         this.messages = new MessageSourceAccessor(messageSource);
256     }
257 
258     public void setUserCache(UserCache userCache) {
259         this.userCache = userCache;
260     }
261 
262     public boolean supports(Class authentication) {
263         return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
264     }
265 
266     protected UserDetailsChecker getPreAuthenticationChecks() {
267         return preAuthenticationChecks;
268     }
269 
270     /**
271      * Sets the policy will be used to verify the status of the loaded <tt>UserDetails</tt> <em>before</em>
272      * validation of the credentials takes place.
273      *
274      * @param preAuthenticationChecks strategy to be invoked prior to authentication. 
275      */
276     public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
277         this.preAuthenticationChecks = preAuthenticationChecks;
278     }
279 
280     protected UserDetailsChecker getPostAuthenticationChecks() {
281         return postAuthenticationChecks;
282     }
283 
284     public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
285         this.postAuthenticationChecks = postAuthenticationChecks;
286     }
287 
288     private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
289         public void check(UserDetails user) {
290             if (!user.isAccountNonLocked()) {
291                 throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
292                         "User account is locked"), user);
293             }
294 
295             if (!user.isEnabled()) {
296                 throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled",
297                         "User is disabled"), user);
298             }
299 
300             if (!user.isAccountNonExpired()) {
301                 throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired",
302                         "User account has expired"), user);
303             }
304         }
305     }
306 
307     private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
308         public void check(UserDetails user) {
309             if (!user.isCredentialsNonExpired()) {
310                 throw new CredentialsExpiredException(messages.getMessage(
311                         "AbstractUserDetailsAuthenticationProvider.credentialsExpired",
312                         "User credentials have expired"), user);
313             }
314         }
315     }
316 }