March 30, 2011

Spring Security by example: user in the backend, testing

This is a part of a simple Spring Security tutorial:

1. Set up and form authentication
2. User in the backend (getting logged user, authentication, testing)
3. Securing web resources
4. Securing methods
5. OpenID (login via gmail)
6. OAuth2 (login via Facebook)
7. Writing on Facebook wall with Spring Social

While login by form, OpenID, OAuth2 and so on may be cool, we may often need to do login/logout in the backend. Finally, we will need a way to get currently logged user. Working with Spring Security, we may do all of that using two classes: SecurityContextHolder and AuthenticationManager. Instead of explaining their API (docs are better) let me show you an implementation of an easy to use service. Our interface looks like this:

   
public interface UserLoginService {
    /**
     * Heavyweight method to get logged authentication.
     * Remember that this method may be touching the database, ergo it's heavy.
     * Use getLoggedUserDetails for fast and light access to logged authentication data.
     */
    User getLoggedUser();

    /**
     * Lightweight method to get currently logged authentication details
     */
    AuthenticationUserDetails getLoggedUserDetails();

    /**
     * This method should be used only to login right after registration
     */
    boolean login(Long userId);

    boolean login(String login, String password);
    void logout();
    boolean isLoggedIn();
}

You should notice that we have two methods to get currently logged user. It may be important to NOT touch the database (or other storage) every time you only want to show login.

        
public class SpringSecurityUserLoginService implements UserLoginService {
    private UserRepository userRepository;
    private AuthenticationManager authenticationManager;
    private static final String internalHashKeyForAutomaticLoginAfterRegistration = "magicInternalHashKeyForAutomaticLoginAfterRegistration";

    public SpringSecurityUserLoginService(UserRepository userRepository, AuthenticationManager authenticationManager) {
        this.userRepository = userRepository;
        this.authenticationManager = authenticationManager;
    }

    @Override
    public boolean login(String login, String password) {
        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(login, password));
        boolean isAuthenticated = isAuthenticated(authentication);
        if (isAuthenticated) {
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        return isAuthenticated;
    }

    @Override
    public boolean login(Long userId) {
        boolean isLoginSuccesfull = false;
        User user = userRepository.findById(userId);
        if (user != null) {
            AuthenticationUserDetails userDetails = new AuthenticationUserDetails(user);
            final RememberMeAuthenticationToken rememberMeAuthenticationToken = new RememberMeAuthenticationToken(internalHashKeyForAutomaticLoginAfterRegistration, userDetails, null);
            rememberMeAuthenticationToken.setAuthenticated(true);
            SecurityContextHolder.getContext().setAuthentication(rememberMeAuthenticationToken);
            isLoginSuccesfull = true;
        }
        return isLoginSuccesfull;
    }


    @Override
    public void logout() {
        SecurityContextHolder.getContext().setAuthentication(null);
    }

    @Override
    public boolean isLoggedIn() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return isAuthenticated(authentication);
    }

    @Override
    public AuthenticationUserDetails getLoggedUserDetails() {
        AuthenticationUserDetails loggedUserDetails = null;
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (isAuthenticated(authentication)) {
            Object principal = authentication.getPrincipal();
            if (principal instanceof AuthenticationUserDetails) {
                loggedUserDetails = ((AuthenticationUserDetails) principal);
            } else {
                throw new ThingThatShouldNotBeException("Expected class of authentication principal is AuthenticationUserDetails. Given: " + principal.getClass());
            }
        }
        return loggedUserDetails;
    }


    private boolean isAuthenticated(Authentication authentication) {
        return authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated();
    }
    
    @Override
    public User getLoggedUser() {
        User loggedUser = null;
        AuthenticationUserDetails userDetails = getLoggedUserDetails();
        if (userDetails != null) {            
            loggedUser = userRepository.findById(userDetails.getId());
        }
        return loggedUser;
    }
}
UserRepository is just a permanent storage interface
        
public interface UserRepository {
    User findByLogin(String login);
    User save(User user);
    User findById(Long userId);
    User findByEmail(String email);   
    User findByLoginOpenId(String loginOpenId);
    User findByFacebookId(Long facebookId);
}
Since the implementation is not reusable (depends too much on your domain model and database access) I'll skip it.

Having such a service, it's easy to create a base for integration tests:

        
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:/ioc/testIntegrationContext.xml"})
@Ignore
public abstract class IntegrationTest extends AbstractTransactionalJUnit4SpringContextTests {
    @Resource(name="sessionFactory")
    protected SessionFactory sessionFactory;

    @Resource(name = "userLoginService")
    protected UserLoginService userLoginService;

    @After
    public void tearDown() {
        pushToDatabase();      
        userLoginService.logout();
    }

    protected void loginDefaultUser() {
        userLoginService.login(DbConst.user1Login, DbConst.user1Password);
    }

    protected void login(String login, String password) {
        userLoginService.login(login, password);
    }

    protected void pushToDatabase() {
        sessionFactory.getCurrentSession().flush();
    }

    protected void clearCache() {
        sessionFactory.getCurrentSession().clear();
    }

    protected User getValidUserFromDb() {
        return (User)sessionFactory.getCurrentSession().get(User.class, DbConst.user1Id);
    }    

    @Override
    public void setDataSource(@Qualifier(value = "dataSource") DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    }
}
The last override is due to the fact, that I usually have more than one DataSource in the application, so automatic wiring by type won't work. I also like my integration tests to actually push the SQL to the database (with rollback on the end), otherwise I may not notice database schema being not up to date (I usually have a single instance of the database for integration test by all developers, with preset data (DbConst class) and no commits, which simplifies a lot of setup. Other times I use embedded DB like H2, one per developer).

Now it's easy to write integration tests, which are, unfortunatelly, sometimes required (does load time weaving work? are your HQL queries correct? all Wicket tests and so on).

Next: Securing web resources

4 comments:

  1. My friend (rpt) who doesn't have a google account asked me to send a comment instead of him:

    "Hm.. great post but I wonder, what You think about thesis that, tests are made only by ones who don't belive in their code. Ones who belive do not write test."

    ReplyDelete
  2. Nice article, but I can't find the UsernamePasswordAuthenticationToken source code. What it exactly does.

    ReplyDelete
    Replies
    1. There you go: http://static.springsource.org/spring-security/site/docs/3.2.x/apidocs/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.html

      Delete