Thursday, July 4, 2013

JSF 2, Spring security integration with Remember-Me

Spring Security is a very powerful security framework. You can use it to manage application access both by defining Authentication and Authorization policies at both the web request level and at method invocation level. At the first level (request) Spring Security uses servlet filters. At the latter level (method), Spring Security uses Spring AOP-proxying objects and applying advice that ensures a particular user has the good rights to invoke a method.

1) Spring security Maven dependencies

To use the Spring Security framework at request level, we need to declare the "springSecurityFilterChain" at our web.xml. But first here are minimal dependencies to add in pom.xml file:


 org.springframework.security
 spring-security-core
 3.1.4.RELEASE



 org.springframework.security
 spring-security-web
 3.1.4.RELEASE



 org.springframework.security
 spring-security-config
 3.1.4.RELEASE

2) Spring security config

Now in web.xml:

 springSecurityFilterChain
 org.springframework.web.filter.DelegatingFilterProxy



 springSecurityFilterChain
 /*

The DelegatingFilterProxy filter by itself doesn't do much work. Instead, and as its name says, it delegates work to a defined bean in application context that will handle requests. For a matter of simplicity, I will separate my Spring files, into general application context beans and security ones. So in web.xml file, declare a context-param like this one:

 contextConfigLocation
 
 /WEB-INF/spring-security.xml
 /WEB-INF/applicationContext.xml
 

If defined, Spring will use this param to read config files and load defined beans. Now in spring-security.xml fil, we will define the specialized filter that will handle our security stuff:


 
  
   
  
 
 
  
 


Unlike in applicationContext file, here we are using beans:bean elements, this is because I chose the Spring Security namespace to be the default one. Notice that in the first declared bean, we are using the same bean name as the filter-name in web.xml for the DelegatingFilterProxy filter. This is very important. In fact, when finding a DelegatingFilterProxy filter, Spring security will automatically create a filter bean whose ID is: springSecurityFilterChain. This is the bean that will be responsible for security chaining. Once we defined which beans should be used for our security, we need to specify which URLs must be secured and which ones can be with public access:



 
 
 
 
 
 
 
 
 
 

Here for example, we decided that only users that have "ROLE_ADMIN" as role to access all "/admin/blah" pages. We also, excluded all paths containing "/javax.faces.resource/" from security checks. In fact, we don't want Spring Security to filter CSS or Javascript files. Now in the "form-login" element, we defined "/login" as login-page. Spring security will redirect non logged-in users to this page. remember-me and access-denied-handler will be discussed later, just ignore them by now.

3) Spring security authentication manager with custom user details

When designed, Spring Security was aimed to fetch by itself user credentials from DB or other data sources (LDAP etc...). To do that, you have to follow forms naming. Personnally, I prefer to manage Spring Security Context manually. This means, that it's up to me to tell Spring Security that a user has logged in. But even, in this way, you still need to define an authentication-manager:

    
    

Now you may notice that I gave to the authentication provider bean a reference to user-service. Well this is a " reference to a user-service (or UserDetailsService bean) Id". The custom user details service bean, must implement the UserDetailsService interface, which declares a single method to be implemented:
UserDetails loadUserByUsername(String username)
                               throws UsernameNotFoundException
So here is our CustomUserDetailsService class:
package com.raissi.security;

import javax.inject.Inject;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.raissi.model.User;
import com.raissi.service.UserService;

@Service("customUserDetailsService")
public class CustomUserDetailsService implements UserDetailsService{

 @Inject
 private UserService userService;
 
 @Override
 public UserDetails loadUserByUsername(String login)
   throws UsernameNotFoundException {
  System.out.println("Trying to fetch user with login: "+login);
  final User user = userService.findUserByLoginOrEmail(login);
  if(user == null){
   throw new UsernameNotFoundException("User not found");
  }
  UserDetails details = new CustomUserDetails(user);
  Authentication authentication =  new UsernamePasswordAuthenticationToken(details, null, details.getAuthorities());
  SecurityContextHolder.getContext().setAuthentication(authentication);
  return details;
 }

}
The CustomUserDetails class by itself is implementing the UserDetails interface:
package com.raissi.security;

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

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.raissi.model.User;
import com.raissi.util.UserRole;

public class CustomUserDetails implements UserDetails{
 private static final long serialVersionUID = 2270217112782820892L;
 private User user;
 
 
 public CustomUserDetails(User user) {
  super();
  this.user = user;
 }
 @Override
 public boolean isEnabled() {
  return true;
 }
 @Override
 public boolean isCredentialsNonExpired() {
  return true;
 }
 @Override
 public boolean isAccountNonLocked() {
  return true;
 }
 @Override
 public boolean isAccountNonExpired() {
  return true;
 }
 @Override
 public String getUsername() {
  return user.getLogin();
 }
 
 @Override
 public String getPassword() {
  return user.getPassword();
 }
 @Override
 public Collection getAuthorities() {
  List auths = new ArrayList();
  if(user.hasRole(UserRole.ADMIN.toString())){
   auths.add(new Authority("ROLE_ADMIN")); //Role here, like "admin"
   auths.add(new Authority("ROLE_USER"));
  }else {
   auths.add(new Authority("ROLE_USER")); //Role here, like "admin"
  }
  return auths;
 }
}
Next thing to do, is to define our GrantedAuthority objects. In fact, these objects will be used by Spring Security to know which role does user have(Remember in first step, we defined roles for users to access particular pages). So here are our very simple Authority implementation:
package com.raissi.security;

import org.springframework.security.core.GrantedAuthority;

public class Authority implements GrantedAuthority{
  private static final long serialVersionUID = 9170140593525051237L;

  private String authority;

  public Authority(String authority) {
    super();
    this.authority = authority;
  }

  @Override
  public String getAuthority() {
    return authority;
  }
  @Override
  public String toString() {
    return "Authority [authority=" + authority + "]";
  }

}

Until now, we are just doing Spring Security in the standard way. The only difference we will make, is how setting the security context will go. In second part of this series, we defined a ManagedBean (Named UserManagedBean) that handles user login via "login()" method. Now, in that method, just add following lines, before returning home page:
List auths = new ArrayList();
  if(user.hasRole(UserRole.ADMIN.toString())){
   auths.add(new Authority("ROLE_ADMIN")); //Role here, like "admin"
   auths.add(new Authority("ROLE_USER"));
  }else {
   auths.add(new Authority("ROLE_USER")); //Role here, like "admin"
  }
  
  Authentication authentication =  new UsernamePasswordAuthenticationToken(new CustomUserDetails(user), null, auths);
  SecurityContextHolder.getContext().setAuthentication(authentication);  
Et voilà! Now if you start server, and try to access any page except the login page, you will be redirected to login page.

4) Spring security custom access denied page

For now, if you login as simple user with "ROLE_USER" to the application, and then try to access an admin page, you will get an ugly Tomcat 403 page, which mentions that you are not authorized to see this page. A good point would be to customize this page. Remember from part 2 in this article that we used a "access-denied-handler" balise in our config. Well, this refers to a custom bean that we are defining to handle non authorized access: 
package com.raissi.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

/**
 * Custom Access denied handler, called in the spring-security config
 *
 */
@Component("customAccessDeniedHandler")
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
 
 public CustomAccessDeniedHandler() {
 }
 
 @Override
 public void handle(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException, ServletException {
    response.sendRedirect(request.getContextPath()+"/accessdenied");
    request.getSession().setAttribute("message",
  "You do not have permission to access this page!");
 
 }
 
}
As you can see, here we are just implementing the AccessDeniedHandler interface which defines a single method. This method will be called by Spring Security every time a non authorized user is trying to access some forbidden places.
In this method, we are redirecting user to a path named "accessdenied" under root path. So using Prettyfaces we will define this path. If you are not using Prettyfaces, you are free to define the path you want to use.

    
    

For the accessdenied.xhtml page, it will just contain a simple message indicating that user is not authorized to see this content. You can use the message we set in session attribute "message".

5) Spring Security Remember-Me with JSF 2

Almost every login based application in the world offers to user the possibility to stay logged in the system when connecting from the same machine and same browser. This is as you may guessed, the "Remember-Me' feature, and happily it's supported by Spring Security. 
To do this, we will use a DB (yes, a DB, for the first time in these series) to store data related to user being logged in and his remember me cookie.
Please follow next part to continue with remember me implementation.

No comments:

Post a Comment