Monday, July 8, 2013

JSF 2, Spring Security integration with Remember-Me (Contd.)

In last part, we said that we are going to use a database to store Remember-Me cookie's related data. This data consists of the username used to login, a series identifier and a token and a timestamp to maintain the last login to the app. For more details about the principles behind stored data, please refer to this article. Fortunately, for us, Spring Security will handle this stuff. We just need to give him the right configs.

1) Configuring database

First thing to do, is to prepare our database. In this series, I will be using MySQL as a DB. 
So let's create our table to store mentioned data:
create table persistent_logins (username varchar(64) not null,
      series varchar(64) primary key,
      token varchar(64) not null,
      last_used timestamp not null)
Next, we configure a datasource to be used, not only by Spring Security, but also by future persistence frameworks (Hibernate, Spring Data etc...). In applicationContext.xml add following:




First line, is just telling Spring about a properties file contained in classpath of our application and named "application.properties". This file, will contain config params that will help system administrators to deploy our application without any need to recompile whole application when a server password is changed:
db.driverClass=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/db_name_here
db.username=dbUserName
db.password=dbUserPassword
db.showSql=true

In next lines, we are just defining a Spring bean that will be our used JDBC datasource implementation. We need to pass it params like db url, username, driver class etc... Don't forget to add dependencies of your DB driver in POM file, for MySQL, just add following:


        mysql
 mysql-connector-java
 5.1.6

2) Remember-Me stuff

In previous part, we added a remember-me element to http config element. That will tell Spring Security to try to fetch user from our system whenever his browser send a valid cookie value.
Now we need to update that remember-me element so it references a new defined Spring bean:




     
    
    
    
    

The remember-me services bean must define an implementation of a remember-me strategy. Here I am going to use one of the two implementations Spring Security offers. For the PersistentTokenBasedRememberMeServices to work, we must give it: a tokenRepository which is a bean we will define and that will store tokens in previously created table, a userDetailsService (and we already defined one in previous part), a key that will be used to encrypt/decrypt sent cookie to client browser.  The two other properties are optional. And their names are just meaningful.
Now we define the tokenRepository bean:


 
     
     

We just give it the name of already configured datasource. The other property is just to prevent it to drop/create the table on each server start.

We need also to define an authentication provider related to the Remember-Me concept and register it:

 
   
   

And we need to register it in the authentication-manager already defined, here is the entire declaration:

    
     
    
    

And the last config we need is to define a Remember-Me filter that should intercept HTTP requests and validate cookies whenever needed:

     
     

Notice how we gave it references to our defined rememberMeServices and authenticationManager.

3) JSF 2 Remember-Me part

Now that everything is configured as expected, let's implement the JSF part of the app that should tell Spring Security when to remember a user, and when not.
First thing to do, is to inject the rememberMeServices service in the UserManagedBean managed bean:
@Inject
@Named("rememberMeServices")
private RememberMeServices rememberMeServices;
Next, in the login method in UserManagedBean class, we just add the following code:

HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();  
HttpServletResponse response = (HttpServletResponse) FacesContext.getCurrentInstance().getExternalContext().getResponse();
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
    @Override 
    public String getParameter(String name) { return "true"; }            
};  
rememberMeServices.loginSuccess(wrapper, response, authentication);
So what does this code do ? From the Spring Security docs, the RememberMeServices#loginSuccess method must be called "whenever an interactive authentication attempt is successful". And since we are doing login manually, then, we should invoke it manually. As for its role, this class "examines the incoming request and checks for the presence of the configured "remember me" parameter. If it's present, or if alwaysRemember is set to true, calls onLoginSucces". The onLoginSucces method by itself is responsible for creating a new persistent login token with a new series number. Then it stores the data in the persistent token repository and adds the corresponding cookie to the response. The loginSuccess method must be given a HttpServletRequest to check if a remember-me param is present and if it's equal to "true", if not it will just return. Second param is a HttpServletResponse, in which generated cookie is written, and finally a org.springframework.security.core.Authentication that will contain username of loggedin user. Now it should be clear why I overrided the getParameter method in the HttpServletRequestWrapper passed to loginSuccess. The added code by now, will activate the Remember-Me whenver called. This should not be the case, since not all users will wish their connections be maintained (they may connect from a public PC for example). This been said, we need to test if connecting user wants to be remembered:
//In user managed bean (where login method is defined)
private Boolean rememberMe = false;
and in the login page, we add a checkbox to let user decide:


Now we just test on the rememberMe value to decide whether to call RememberMeServices#loginSuccess or not. Et voilà!! We just finished with the Remember-Me feature in JSF 2 with Spring Security.
You can test your code by setting a session timeout value of 5 minutes for example, login to the application and wait for session to timeout and then re-call home page, you should access it without being obliged to re-login.

4) Instantiate JSF Managed beans

Now suppose, that with normal login, if user is successfully authenticated, you have a session managed bean that will handle some informations to be used by other managed beans in the application. You may say that we already have the logged in user's username in Spring Security' SecurityContextHolder. That's true, but also, the stored object has a few data, and you will soon be obliged to fetch more data for the User object from DB. That will result in extra DB access. 
So let's assume you have a managed bean like this: 
package com.raissi.managedbeans;

import java.io.Serializable;
import javax.inject.Named;
import org.springframework.context.annotation.Scope;
import com.raissi.model.User;

@Named("loggedInUser")
@Scope("session")
public class LoggedInUser implements Serializable{
 private static final long serialVersionUID = -1033377115353626379L;
 private User user;

 public User getUser() {
  return user;
 }

 public void setUser(User user) {
  this.user = user;
 }
}


We inject this bean in the UserManagedBean (view managed bean responsible for the login/logout process), and whenever a successful login takes place, we set user in LoggedInUser with the returned object from DB. And in all managed beans that need informations about user logged to the app, we just inject this managed bean.
Now if you try to access the application after session timeout, Spring Security will create a new session for you and authenticate you automatically. But in this newly created session, you won't have JSF session managed bean LoggedInUser. And if you try to access its User variable in a homeManagedBean, you will have a NullPointerException.
To initialize this bean, in early defined CustomUserDetailsService add the following code after setting authentication in SecurityContextHolder:

HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
LoggedInUser loggedInUser = (LoggedInUser)request.getSession().getAttribute("loggedInUser");
 if(loggedInUser == null){
         loggedInUser = new LoggedInUser();
  request.getSession().setAttribute("loggedInUser", loggedInUser);
 }
 if(loggedInUser.getUser() == null){
  loggedInUser.setUser(user);
 }
Thanks to Arjin comment below, this code will only work if the LoggedInUser objects are simple POJOs with no injected beans. But when we want to use other services inside it (as for example: injecting the RememberMeServices bean to centralize the updating process of SecurityContext, which I did indeed) then, things will go bad and complicated. And all of that if because of LoggedInUser object being manually instantiated.
The solution I opted for in this situation is to inject a scoped proxy of LoggedInUser inside CustomUserDetailsService. You would say, "but CustomUserDetailsService is a singleton service that will be created only once a time, however LoggedInUser is a session scoped bean, that will be created on every new session". And that's why I am using a scoped proxy of LoggedInUser. By that we will be "injecting a proxy object that exposes the same public interface as the scoped object but that can also retrieve the real, target object from the relevant scope" (more details here). So how to do it ?
First let's create an interface that LoggedInUser will implement:
package com.raissi.managedbeans;

import java.io.Serializable;

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

import com.raissi.domain.User;

public interface ILoggedInUser extends Serializable{

 public User getUser();
 public void setUser(User user);
 /**
  * Update security context
  * This method should be called whenever setUser is called.
  * It's defined here to be conform with the DRY principle.
  * @param request
  * @param response
  */
 public void updateSecurityContext(HttpServletRequest request, HttpServletResponse response);
}

After that let's change our LoggedInUser class:
package com.raissi.managedbeans;

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

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.stereotype.Component;

import com.raissi.domain.User;
import com.raissi.security.Authority;
import com.raissi.security.CustomUserDetails;
import com.raissi.util.UserRole;

@Component("loggedInUser")
@Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
public class LoggedInUser  implements ILoggedInUser{
 private static final long serialVersionUID = -1033377115353626379L;
 
 @Inject
 @Named("rememberMeServices")
 private RememberMeServices rememberMeServices;
 
 private User user;

 public User getUser() {
  return user;
 }
 @PostConstruct
 public void init(){
 }
 public void setUser(User user) {
  this.user = user;
 }
 
 
 public void updateSecurityContext(HttpServletRequest request, HttpServletResponse response){
  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(user.getRole())); //Role here, like "admin"
  }
  
  Authentication authentication =  new UsernamePasswordAuthenticationToken(new CustomUserDetails(user), null, auths);
  SecurityContextHolder.getContext().setAuthentication(authentication);
  
  HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
      @Override public String getParameter(String name) { return "true"; }            
  };
  rememberMeServices.loginSuccess(wrapper, response, authentication);
 }

}
Two things are to be noticed here:
a) we are using "proxyMode = ScopedProxyMode.INTERFACES" to tell Spring that we are exposing this bean as a proxy
b) we moved the logic to set authentication in Spring Security context from view managed beans (ex: in UserManagedBean) into the LoggedInUser.
This been done, you just update managed beans containing references to LoggedInUser to reference the new defined ILoggedInUser instead, example in UserManagedBean:
@Inject
private @Named("loggedInUser") ILoggedInUser loggedInUser;
And in login method:
//Get User object based on provided login and password:
User user = userService.loginUser(userLogin, password);
//Set user in LoggedInUser
loggedInUser.setUser(user);
//Update Security context:
HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
HttpServletResponse response = (HttpServletResponse) FacesContext.getCurrentInstance().getExternalContext().getResponse();
loggedInUser.updateSecurityContext(request, response);

By now everything should work fine, and you completely have Spring Security working with your JSF 2 application and also with the Remember-Me feature activated

13 comments:

  1. Nice article, but I would think twice about manually instantiating LoggedInUser and putting the result of that into the session directly. This will work with the given example, but will break as soon as LoggedInUser starts to use other (CDI) services, e.g. is injected itself with some beans and/or uses interceptors.

    With the code as given, sometimes LoggedInUser will be a proxy and sometimes it will not be one.

    I think you can better try to resolve the EL expression "loggedInUser" or inject the bean. It will always be non-null then. After that you can set the User if it's null.

    ReplyDelete
  2. Hi Arjan,
    I really appreciate your comment. I totally agree with you about this point. And I think using a scoped proxy of the LoggedInUser object is a best solution for it. Isn't it ?
    I will update the article with modifications right now.
    Again, thank you for your remark
    http://static.springsource.org/spring/docs/3.0.x/reference/beans.html#beans-factory-scopes-other-injection

    ReplyDelete
  3. I have the source code of all articles published here about JSF2 integration with Spring, Spring Security, Freemarker, etc..
    It's available on Github under:
    https://github.com/raissi/jsf2-spring-jpa

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Hi,
    source code given,doesn't have updated code(missed remember-me functionality check-box and managed Bean login method remember me functionality)

    ReplyDelete
    Replies
    1. Hi Narendar and welcome to my blog,
      In the class BaseManagedBean (from which almost all my managed beans inherit) you can find a method named updateSecurityContext that calls a method from the injected ILoggedInUser bean, updateSecurityContext.
      Now, in LoggedInUser managed bean (implementation for the ILoggedInUser interface), you can find the remember-me logic.
      As for the check box, it's very easy to add a checkbox to your jsf login view and then test its value in the managed bean to decide whether or not to remember the user

      Delete
    2. Please let me know if you have problems understanding special code parts,

      Delete
    3. This comment has been removed by the author.

      Delete
  6. Hi,
    Thanks for the information.But when I try to login its giving me below exception:
    /pages/user-profile.xhtml @22,75 rendered="#{loggedInUser.user.hasRole('ROLE_USER')}" Failed to parse the expression [#{loggedInUser.user.hasRole('ROLE_USER')}]

    ReplyDelete
    Replies
    1. Ok, I will see it later today and try to solve your problem, I am kind of busy now

      Delete
    2. Ok, sorry for being this late. I have been very busy at work. Anyway, I really can't tell about the real cause of this exception unless you give full stack.
      However, my first guess is about your EL version (may be you are using Tomcat6 ?)

      Delete
  7. This comment has been removed by the author.

    ReplyDelete