Thursday, June 26, 2014

Connecting Spring Security Authentication to Domain Entities

Introduction

With about every consumer facing web application today the need to allow users to create and maintain accounts within the web application is a requirement.

Spring Roo provides the ability to integrate Spring Security into web applications readily via a simple installation command.  By default the scaffold-ed implementation provides a rudimentary authentication implementation configured in the applicationContext-security.xml file.  It looks like this;

    <!-- Configure Authentication mechanism -->
    <authentication-manager alias="authenticationManager">
        <!-- SHA-256 values can be produced using 'echo -n your_desired_password | sha256sum' (using normal *nix environments) -->
        <authentication-provider>
            <password-encoder hash="sha-256" />
            <user-service>
                <user name="admin" password="8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918" authorities="ROLE_ADMIN" />
                <user name="user" password="04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb" authorities="ROLE_USER" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

    

This of course is inadequate for production use, except for exceptional cases.  What we need is a convenient way to integrate the web applications entity model, again scaffold-ed by Roo, with the Spring Security infrastructure.

What we need is for Spring security to authenticate using our web application entity model.  Doing this is simple, there are two main parts of the solution.  First, we need to implement the user data interface that consumed by Spring security to access the application domain model.  And course we need to reconfigure the security application context to use that implementation.

Spring security uses a component that implements UserDetailService as the source of user information.  We need to provide an implementation that uses our web applications entity model as the source of user data.  Again there are two tasks needed to accomplish this.  First we need to provide a implementation of the expected component.  And we must design our application user entity (or AppUser) to provide the needed data.

The AppUser must provide minimal authentication data, username and password.  But there are additional features available like, account, credential locking, and expiration features that the application may want to integrate with.  Supporting a majority of these features can be done trivially utilizing Roo's scaffolding, with the exception of passwords.  But why should that be a surprise.

Encoding Passwords

Everybody knows clear text passwords should never be stored.  When authenticating Spring Security expects the password provided by our application to be encoded.  To encode passwords we need to use the same password encoder implementation that  the security infrastructure uses.  There are multiple implementations of encoders some more easily cracked then others.  But of course higher security usually translates in more computing and operating costs.  However keep in mind changing the algorithm used by the application will invalidate all existing passwords so careful up front selection is advised since changing this once in service will impact user experience.

When saving passwords in the AppUser object we need to encode the password.  Via Spring configuration we can set the encoder that the security infrastructure will utilize.  Additionally, that will allow auto wiring the encode into application code.  Using the encoder is simply just a matter of passing the clear text password to the encode method to obtain the encoded password.  From this point it is just a matter of including the encoding functionality when storing passwords.

Here is my controller handles the user registration.  Note there are two parts of the code that participate in the encoding password process.  First the auto wiring of the 'encoder' bean into the controller so it can be accessed.  Then later in the code when setting the password the encoder is utilized to do the encoding.

package com.repik.buddyframework.web;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;

import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.roo.addon.web.mvc.controller.scaffold.RooWebScaffold;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.repik.buddyframework.domain.AppUser;
import com.repik.buddyframework.domain.Profile;
import com.repik.buddyframework.domain.ProfilePreferences;

@RequestMapping("/appusers")
@Controller
@RooWebScaffold(path = "appusers", formBackingObject = AppUser.class)
public class AppUserController {

 @Autowired
 private PasswordEncoder encoder ;
 
 @RequestMapping(value = "/register", method = RequestMethod.POST, produces = "text/html")
    public String register(@Valid AppUser appUser, BindingResult bindingResult, Model uiModel, HttpServletRequest httpServletRequest) {
        if (! bindingResult.hasErrors()
          && appUser.getPassword().equals(appUser.getConfirmPassword())
          && appUser.getEmail().equals(appUser.getConfirmEmail())) {
         try {
             uiModel.asMap().clear();
             appUser.setAccountNotExpired(Boolean.TRUE);
             appUser.setAccountNotLocked(Boolean.TRUE);
             appUser.setAuthorities( "Guest");
             appUser.setCredentialsNotExpired(Boolean.TRUE);
             appUser.setEnabled(Boolean.TRUE);
             
             // set the password to the encoded hash value
             appUser.setPassword( encoder.encode( appUser.getPassword()));
             
             appUser.persist();
             return "redirect:/login" ;
         }
         catch ( JpaSystemException jse ) {
          Throwable cause = jse.getCause() ;
          boolean handled = false ;
          while ( cause != null ) {
           if ( cause instanceof ConstraintViolationException ) {
                  bindingResult.addError( new FieldError("appUser", "username", "username already taken" ));
                  cause = null ;
                  handled = true ;
           }
           else {
            cause = cause.getCause() ;
           }
          }
          
          if ( ! handled )
           throw jse ;
         }
        }

        populateEditForm(uiModel, appUser); 
        return "appusers/register";
    }
 
    @RequestMapping(value ="/register", params = "form", produces = "text/html")
    public String registerForm(Model uiModel) {
        populateEditForm(uiModel, new AppUser());
        return "appusers/register";
    }

    
}

Implementing the UserDetailsService

Next we need to provide an implementation of the UserDetailsService.  Spring Security uses this component to obtain user details.  We our implementation to access the application user domain object rather then the default implementation.

The implementation is pretty simple.  The UserDetailsService interface requires a single method, loadUserByUserName, where the usage is a user name is provided as a string and the component should return a UserDetails object for that user.  While is was tempting during implementation to allow the AppUser domain object itself implement this interface, instead to keep things simple this implementation uses a data transfer object instead.

The logic just involves finding the AppUser by username, then if found populate a User instance to return to the Spring Security infrastructure.  The first part of the implementation will be to create a finder to allow finding AppUsers by username.  This is done will in the Roo shell with the following command;

focus --class ~.domain.AppUser
finder add --finderName findAppUsersByUsernameEquals

Now the UserDetailsService implementation can be completed;

package com.repik.buddyframework.domain;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.persistence.TypedQuery;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

public class AppUserDetailsService implements UserDetailsService {

 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  
  TypedQuery<AppUser> appUsers = AppUser.findAppUsersByUsernameEquals(username);
  AppUser appUser = appUsers.getSingleResult() ;
  return appUser == null 
    ? null 
    : new User(appUser.getUsername(), 
      appUser.getPassword(), 
      appUser.getEnabled(), 
      appUser.getAccountNotExpired(),
      appUser.getCredentialsNotExpired(),
      appUser.getAccountNotLocked(),
      getAuthorities( appUser.getAuthorities())) ; 
 }
 
 private Collection<? extends GrantedAuthority> getAuthorities( String authorities ) {
  if ( authorities == null ) {
   return null ;
  }
  
  List<GrantedAuthority> result = new ArrayList<GrantedAuthority>() ;
  
  String[] tokens = authorities.split( "\\W" ) ;
  for ( int i = 0 ; i < tokens.length ; i++ ) {
   result.add( new SimpleGrantedAuthority(tokens[ i ])) ;
  }
  
  return result ;
  
 }


Configuring the Security Application Context

All now that remains is to configure Spring security to utilize the new implementation of the UserDetailService.  The configuration is contained in the 'applicationContext-security.xml' file.  Specifically towards the end of the file we need to configure the authentication provider to use our implementation of the UserDetailService.  Note also that the encoder bean is defined here allowing the use within the application context.

    <!-- Configure Authentication mechanism -->
    <authentication-manager alias="authenticationManager">
  <authentication-provider user-service-ref="customUserDetailsService">
   <password-encoder ref="encoder"/>
  </authentication-provider>
    </authentication-manager>

    <beans:bean id="encoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/&gt
    
<beans:bean id="customUserDetailsService" class="com.repik.buddyframework.domain.AppUserDetailsService" />

Now the implementation is complete.  Most of our effort in the implementation here really centered around integrating our AppUser domain object to provide the user data conform to that required Spring Security.

No comments:

Post a Comment