Friday, January 28, 2011

JSF 2 with Spring 3 - protection with Spring Security (part 2 of 2)

Note: below description uses Eclipse Indigo, Tomcat 7.0.28, MyFaces 2.1.7, Spring Framework 3.1, Spring Security 3.1.0.


Requirements:
  • a working example of JSF 2.0 application with Spring Framework integrated (can be found here)
You will learn:
  • how to use Spring Security Framework in order to protect web application
From the previous post we know what Spring Framework is, and what advantages it gives us when used in web application. Business logic managed by Spring is not the only one advantage coming from Spring - we can use Spring's embedded mechanisms to secure our web application. This post will show the basic usage of Spring Security for securing our sample JSF 2.0 webapp. 
What parts of our application will be protected? Consider those scenarios:
1. Only registered user (or page administrator) can see details of a selected bike.
2. Only page administrator can add a new bike to the shop offer.

We need two user roles which will determine the privilleges which user has: registered users role and admin users role. Moreover, for the scenario 2, we have to add a new function: adding new bike. For this function we will create a page addBike.xhtml and a JSF managed bean for that page, named addBike.java.

addBike.xhtml source code is shown below:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
  <ui:composition template="../shopTemplate.xhtml">
       
        <ui:define name="content">
        <h:form>
         <h:outputText value="#{msg['bikes.list.name']}: "/><h:inputText value="#{addBike.name}" /><br/>
         <h:outputText value="#{msg['bikes.list.price']}: "/><h:inputText value="#{addBike.price}" /><br/>
         <h:outputText value="#{msg['bikes.list.discountprice']}: "/><h:inputText value="#{addBike.discountPrice}" /><br/>         
         <h:outputText value="#{msg['bikes.list.description']}: "/><h:inputText value="#{addBike.description}" /><br/>
         <h:commandButton action="#{addBike.addNewBike}" value="#{msg['bikes.add.button']}" />
        </h:form>
        </ui:define>

  </ui:composition>
</html>
Nothing special - standard form for entering the data.
addBike.java source code is also simple:
package com.jsfsample.managedbeans;
...
@ManagedBean(name="addBike")
@SessionScoped
public class AddBike implements Serializable {

 private static final long serialVersionUID = -2155913853431899821L;
 
 
 @ManagedProperty("#{bikeDataProvider}")
 private BikeDataProvider bikeDataProvider; // injected Spring defined service for bikes
 
 private String name;
 private String description;
 private String price;
 private String discountPrice;
 private Integer categoryId;
 
 public String addNewBike(){

  Bike newBike = new Bike();
  newBike.setName(getName());
  newBike.setDescription(getDescription());
  newBike.setPrice(Integer.parseInt(getPrice()));
  newBike.setDiscountPrice(Integer.parseInt(getDiscountPrice()));
  newBike.setCategory(categoryId);
  
  // save new bike and return to the shop
  bikeDataProvider.add(newBike);  
  return "/bikesShop.xhtml";
 }; 
 ...
}
Please note that we use here BikeDataProvider.java class, which is Spring managed service, the same we used for loading bikes list and loading a certain bike details in previous post.

Now it is time for protected parts of application. I will show two ways of protecting webapp: protecting resources (like access to certain page) and protecting business logic methods execution. Scenario 1 will be an example of protecting business logic and scenario 2 will be an example of protecting resources.
When user tries to access the protected area (resource or invoke protected method), application will check user roles and based on them will decide if let the user go further or force him to log in. Log in - that's right - a login page will be displayed where user will enter his credentials. Based on them Spring Security will decide what roles user has and depends on assigned roles further action will be continued or not. Let's modify our application to use Spring Security:

Step 1. Modify configuration files:
applicationContext.xml source:
...
 <!-- 
 resource security  
 -->
 <sec:http auto-config="true" access-denied-page="/faces/accessDenied.xhtml">
  <sec:form-login login-page="/faces/login.xhtml" />
  <sec:intercept-url pattern="/faces/admin/**" access="ROLE_ADMIN" />     
 </sec:http>
 <!-- 
 business logic (method) security 
 -->
 <sec:global-method-security
  secured-annotations="enabled" jsr250-annotations="enabled" >  
 </sec:global-method-security>
 <!-- 
 manager responsible for loading user account with assigned roles 
 -->
 <sec:authentication-manager alias="authenticationManager">
  <sec:authentication-provider
   user-service-ref="userDetailsService" />
 </sec:authentication-manager>
 ...
Access-denied-page is invoked when user is authenticated but is not authorized to access protected resources. When user is not authenticated, he is moved into form-login instead of access-denied-page.
web.xml source:
...
 <filter>
  <filter-name>springSecurityFilterChain</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
 </filter>
 <filter-mapping>
  <filter-name>springSecurityFilterChain</filter-name>
  <url-pattern>/*</url-pattern>
 </filter-mapping>
 ...
Step 2. Additional pages: login.xhtml and accessDenied.xhtml.
accessDenied.xhtml is simple page displaying only a message saying that the user is authenticated but still is not authorized to go further.
login.xhtml is a simple page with login form where user enters his credentials (login and password). The more interesting part is corresponding managed bean LoginBean.java which uses a Spring service for authenticating users:
package com.jsfsample.managedbeans;
...
@ManagedBean(name = "loginBean")
@SessionScoped
public class LoginBean implements Serializable {
 private static final long serialVersionUID = 1L;

 private String login;
 private String password;

 @ManagedProperty(value = "#{authenticationService}")
 private AuthenticationService authenticationService; // injected Spring defined service for bikes


 public String login() {

  boolean success = authenticationService.login(login, password);
  
  if (success){
   return "bikesShop.xhtml"; // return to application but being logged now 
  }
  else{
   FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Login or password incorrect."));   
   return "login.xhtml";
  }
 }
 ...
}
When login was successful and user is authenticated, he is moved to the shop. If not a proper message is displayed and user can re-enter his credentials or go back to the shop without login. Let's look inside AuthenticationService.java class which is a service deciding if user is authenticated or not.

Step 3. AuthenticationService implementation:
package com.jsfsample.application.impl;
...
@Service("authenticationService")
public class AuthenticationServiceImpl implements com.jsfsample.application.AuthenticationService {


 @Resource(name = "authenticationManager")
 private AuthenticationManager authenticationManager; // specific for Spring Security

 @Override
 public boolean login(String username, String password) {
  try {
   Authentication authenticate = authenticationManager
     .authenticate(new UsernamePasswordAuthenticationToken(
       username, password));
   if (authenticate.isAuthenticated()) {
    SecurityContextHolder.getContext().setAuthentication(
      authenticate);    
    return true;
   }
  } catch (AuthenticationException e) {   
  }
  return false;
 }
 ...
}
This Spring managed service uses internally a class AuthenticationManager, which comes from Spring Security and was defined as a manager in applicationContext.xml file:
...
 <!-- 
 manager responsible for loading user account with assigned roles 
 -->
 <sec:authentication-manager alias="authenticationManager">
  <sec:authentication-provider
   user-service-ref="userDetailsService" />
 </sec:authentication-manager>
 ...
Note that we do not explicit define AuthenticationManager! It is a ready to use object. But AuthenticationManager has helper service named userDetailService defined in applicationContext.xml file - this service must be written by our own.

Step 4. Implementation of userDetailService
userDetailsService source code is shown below:
package com.jsfsample.application.impl;
...
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

 private HashMap users = new HashMap();
 
 @Override
 public UserDetails loadUserByUsername(String username)
   throws UsernameNotFoundException{
  
  org.springframework.security.core.userdetails.User user = users.get(username);
  
  if (user == null) {
   throw new UsernameNotFoundException("UserAccount for name \""
     + username + "\" not found.");
  }
  
  return user;
 }

 @PostConstruct
 public void init() {
  
  // sample roles  
  Collection adminAuthorities = new ArrayList();
  adminAuthorities.add(new GrantedAuthorityImpl("ROLE_ADMIN"));
  
  Collection userAuthorities = new ArrayList();
  userAuthorities.add(new GrantedAuthorityImpl("ROLE_REGISTERED"));
  
  boolean enabled = true;
  boolean accountNonExpired = true;
  boolean credentialsNonExpired = true;
  boolean accountNonLocked = true;
  
  // sample users with roles set
  users.put("admin", new org.springframework.security.core.userdetails.User("admin", "admin", enabled, accountNonExpired,
    credentialsNonExpired, accountNonLocked, adminAuthorities));
  
  users.put("user", new org.springframework.security.core.userdetails.User("user", "user", enabled, accountNonExpired,
    credentialsNonExpired, accountNonLocked, userAuthorities));
 }
}
We have here a Spring Security specific objects representing users and roles. In the init() method I created some mocked data two roles representing page administrators and registered users - ROLE_ADMIN and ROLE_REGISTERED. For each role I created a single account: admin (password: admin) for the ROLE_ADMIN and user (password: user) for the ROLE_REGISTERED. That's all - it is time to protect applicartion.

Step 5. Protecting application.
5 a) Scenario 1: protecting business logic. We have to protect invoking a method which allows to see bike details. This method is placed inside BikeDataProvider.java service class. In order to protect the method we have to add an annotation defining roles allowed to execute this method:
package com.jsfsample.services;
...
public interface BikeDataProvider {
 ...
 
 @RolesAllowed({"ROLE_ADMIN","ROLE_REGISTERED"}) 
 public abstract Bike getBikeById(Integer id);
 
 public abstract void add(Bike newBike);
}
This simply means that only registered users or admin users can see bike details. 
Why we do not protect the add(...) method? Because we protect the whole page access where this method is executed - of course in addition we can also protect this method by annotating it with @RolesAllowed({"ROLE_ADMIN"}).
5 b) Scenario 2: protecting resource. According to rules of protecting resources defined in applicationContext.xml, we protect all resources which are located inside /admin directory. So we have to create a directory /admin under /WebContent directory and move addBike.xhtml page there. It should look like this:




Note: there is a little trick in the protecting resources like pages in JSF. Spring Security tries to match exact URL address to apply the rule. But in JSF there is a "old URL" issue - after navigtation from page A to page B, URL address in browser still points to page A. In order to make the rule working we have to force the browser to show the current URL instead of old one. It is done by adding a special command into the navigation string returning a page for adding a bike:
public String showForm(){  
 ...  
 return "/admin/addBike.xhtml?faces-redirect=true";
}


That's all about Spring Security in our sample application. 
How to test it? After deploying application on the server and starting the server, we have to open a browser and type in URL:

http://localhost:8080/JSF2FeaturesSpring

Then try to display some bike details. When promped for login, enter credentials: user, user and try again. Then try to add a new bike - You should see access denied page. The close the application and clean the browser cache and try the same with the user admin, admin.

-------------------------------------------
Download source files:

Note: make sure that Java, Eclipse and Tomcat are properly installed and configured for running the project (additional configuration may be required if different directories are used).

Eclipse complete sample project is here (with all required libraries). The sample project is a ready to run application which contains all described Spring Security issues in this post. You can also download a war file located here (just copy it inside webapps folder in Your Tomcat and start Tomcat with the script startup.bat)