Sunday, December 9, 2012

Spring MVC (Security): custom authentication manager and login page

In the previous post, we've implemented basic authentication and authorization features, mainly relying on the login page that Spring security generates. However, most of the time, we'll want to have our own login page as well as a custom authentication manager (having all the usernames, passwords, and roles hardcoded in the  web.xml file is definitely not a good solution!). Let's see how we could achieve that. As we use the project from this previous post as starting point, feel free to check out it source code here.


Tools and libraries
  • Eclipse Indigo
  • Spring-core 3.1.1
  • Spring-security 3.1.0
  • Tomcat 7
  • Maven 3
STEP 1 - Creating the custom login page

In our example, we'll create a login page, named "myLoginPage.jsp"  that is actually quite similar to the one that Spring Security generates   except that we'll add a logout button:

<%@ taglib prefix="s" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="security"
 uri="http://www.springframework.org/security/tags"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form"%>
<div id="loginPannel">
 <security:authorize access="!isAuthenticated()">
  <h1>This is my custom login page</h1>
  
  <c:if test="${loginFailed}">
   <div style="color: red">Could not sign in, please check your login/password...</div>
  </c:if>  

  <form method="post" class="signin" action="j_spring_security_check">
   <table>
    <tr>
     <th><label for="username_or_email">Username/email</label></th>
     <td><input id="username_or_email" name="j_username" type="text" /></td>
    </tr>
    <tr>
     <th><label for="password">Password</label></th>
     <td><input id="password" name="j_password" type="password" /></td>
    </tr>
    <tr>
     <th></th>
     <td><input name="commit" type="submit" value="Sign In" /></td>
    </tr>
   </table>
  </form>
 </security:authorize>
</div>

Note the following line in our JSP page: <security:authorize access="!isAuthenticated()">
It specifies that this page is restricted to user that aren't authenticated. Access rules can be defined using expressions which correspond to the methods (such as, hasRole( ), hasAnyRole( ), ...) from the "SecurityExpressionRoot" provided by Spring.

STEP 2 - Customizing login page

In this step, we edit the Spring configuration file "MyDispatcherServlet-servlet.xml", to override some behavior of Spring default security configuration:

 <security:http auto-config="true" use-expressions="true"> 
  <security:form-login login-page="/login.go" default-target-url="/home.go" authentication-failure-url="/login.go?errorLogin"/> 
  <security:intercept-url pattern="/home.go" access="hasRole('ADMIN')" /> 
  <security:logout logout-success-url="/home.go" />
 </security:http>


  • use-expression="true": allows us to use expression such as hasRole(...), hasAnyRole(...), isAuthenticated( ), and so on, to define access rules
  • login-page="/login.go": defines the URL at which the login page is located
  • default-target-url="/home.go": defines the URL toward which the user will be redirected once he successfully logged in
  • authentication-failure-url="/login.go?errorLogin": defines the URL toward which the user will be redirected when the login fails. In our example, we redirect the user to the login page with an additional "errorLogin" parameter in the URL. This existence of this parameter will be checked in a Spring Controller class. If it exists, an attribute will be added to the model before returning the view to be displayed. Within the JSP page, we check this model attribute to eventually display an error message. 
  • <security:intercept-url pattern="/home.go" access="hasRole('ADMIN')" />: indicates that the "/home.go" URL is subject to access rules.
  • <security:logout logout-success-url="/home.go" />: defines the URL at which the user is redirected after he logged out.
STEP 3 - Defining a custom authentication provider

In most cases, we'll store user credentials and roles in a DB, LDAP, you name it, and consequently, we'll want authentication and authorization to be performed against that user repository. In this example, we'll configure an authentication provider as if we were storing the user credentials in a DB.

To do so, we edit the Spring configuration file "MyDispatcherServlet-servlet.xml" and define a custom Spring service (here, it's named "myUserDetailService") as authentication provider:

 <security:authentication-manager>
  <security:authentication-provider
   user-service-ref="myUserDetailService">
  </security:authentication-provider>
 </security:authentication-manager>

STEP4 - Implementing the custom authentication provider


  • First, we have to create a class that implements the GrantedAuthority interface from Spring. This interface will have us implement a getAuthority( ) method that returns a role name.
Ex:

package service.security;

import org.springframework.security.core.GrantedAuthority;

public class GrantedAuthorityImpl implements GrantedAuthority{
 private static final long serialVersionUID = 1029928088340565343L;

 private String rolename;
 
 public GrantedAuthorityImpl(String rolename){
  this.rolename = rolename;
 }
 
 public String getAuthority() {
  return this.rolename;
 }

}


  • Then, we create a class that implements the UserDetails interface from Spring. This class will hold all the account-related info such as the user credentials and the associated roles.
package service.security;

import java.util.Collection;
import java.util.HashSet;

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

public class UserDetailsImpl implements UserDetails{
 private static final long serialVersionUID = -6509897037222767090L;
 
 private Collection authorities = new HashSet();
 private String password;
 private String username;
 
 public UserDetailsImpl(String username, String password, Collection authorities){
  this.username = username; 
  this.password = password;
  this.authorities = authorities;
 }

 public Collection getAuthorities() {
  return this.authorities;
 }

 public String getPassword() {
  return this.password;
 }

 public String getUsername() {
  return this.username;
 }

 public boolean isAccountNonExpired() {
  return true;
 }

 public boolean isAccountNonLocked() {
  return true;
 }

 public boolean isCredentialsNonExpired() {
  return true;
 }

 public boolean isEnabled() {
  return true;
 }

}

  • Finally, we create a class that implements the UserDetailsService interface from Spring. This interface contains only one method: UserDetails loadUserByUsername(String username). In our example, we emulate a DB user repository by defining a Map whose keys are usernames, and values are UserDetails objects. Pay attention to the static initialization block in which we populate this map with 3 users.
Remark:

If we actually had user credentials stored in DB, we would just have to implement a repository class, and inject it into this class.
package service.security;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.springframework.security.core.GrantedAuthority;
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;

@Service("myUserDetailService")
public class UserDetailsServiceImpl implements UserDetailsService{
 
 // just to emulate user data and credentials retrieval from a DB, or whatsoever authentication service
 private static Map<String, UserDetails> userRepository = new HashMap<String, UserDetails>();
 
 static{
  GrantedAuthority authorityAdmin = new GrantedAuthorityImpl("ADMIN");
  GrantedAuthority authorityGuest = new GrantedAuthorityImpl("GUEST");
  
  /* user1/password1 --> ADMIN */
  Set<GrantedAuthority> authorities1 = new HashSet<GrantedAuthority>();
  authorities1.add(authorityAdmin);
  UserDetails user1 = new UserDetailsImpl("user1", "password1", authorities1);
  userRepository.put("user1", user1);
  
  /* user2/password2 --> GUEST */
  Set<GrantedAuthority> authorities2 = new HashSet<GrantedAuthority>();
  authorities2.add(authorityGuest);
  UserDetails user2 = new UserDetailsImpl("user2", "password2", authorities2);
  userRepository.put("user2", user2);
  
  /* user3/password3 --> ADMIN + GUEST */
  Set<GrantedAuthority> authorities3 = new HashSet<GrantedAuthority>();
  authorities3.add(authorityAdmin);
  authorities3.add(authorityGuest);
  UserDetails user3 = new UserDetailsImpl("user3", "password3", authorities3);
  userRepository.put("user3", user3);
 }
 
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  UserDetails matchingUser = userRepository.get(username);
  
  if(matchingUser == null){
   throw new UsernameNotFoundException("Wrong username or password");
  }
  
  return matchingUser;
 }

}


  • In the Controller class, we add the two following URL mapping. As as result, GET requests for /login URL will display the custom login page, while requests for the same URL with the "errorLogin" parameter will display the same custom login page with an additional "loginFailed" model attribute. This attribute is checked in the JSP page, to display an error message, if neccessary.
 @RequestMapping(method=RequestMethod.GET, value="/login")
 public String displayLoginPage(){
  return "myLoginPage";
 }
 
 @RequestMapping(value="/login", params="errorLogin")
 public String directToLoginPageWithError(Model model){
  // Adding an attribute to flag that an error happened at login
  model.addAttribute("loginFailed", true);

  return "myLoginPage";
 }

Testing the application

  • Custom login page:
  • Login with bad credentials


  • Login with user1/password1
  • Login with user2/password2

In this last case, user2 is authenticated but not authorized to access the home.go page. In the Spring configuration "MyDispatcherServlet-servlet.xml" file, we specified that the home.go page is restricted to user with role ADMIN with the following configuration element:

<security:intercept-url pattern="/home.go" access="hasRole('ADMIN')" /> 

Source code

The source code of this example application can be checked out here.

18 comments:

  1. @KH,

    This is very good article. Works like a charm. Awesome!

    Thanks,
    Manju

    ReplyDelete
  2. Hello,

    Your article is great and one of the most complete I've seen. I have one question though: I'd like to store additional data in the user session. Where in your example should I do that?

    Let's say the user authenticating is an employee and I want to store it's employer's ID in his session since I will be using it a lot in the future requests.

    Thank you! Keep up the great work :)

    ReplyDelete
  3. Simplest, but not the cleanest, would be adding a parameter of type HttpSession to the controller methods in which you want to set/get session attribute. This is possible because the HttpSession object is shared across Spring controllers.

    Another solution seems to be using the @SessionAttribute Spring annotation. But to be honest, I haven't used it yet so I won't be able to provide you concrete usage examples.

    I'll definitely try the latter out to widen my own Spring knowledge (guess I'll blog about it too) ;-)

    ReplyDelete
    Replies
    1. Thanks for your reply.

      I finally chose to use an external class that acts as a Session Accessor. Any controller that needs some data in the session will use this class to access it. On the first try, the data will be retrieved from DB and stored into session and all later calls will retrieve the data from the Session, avoiding many DB calls.

      What's your opinion on this idea?

      Delete
    2. Right off the top of my head, I guess it's one solution, as long as you only set values into the session, that don't change between requests. I mean, suppose you wanted to set an attribute that is specific to a certain request within a workflow, you'll then have to consider scenarios such as a user refreshing the page, navigating back and forth, etc.

      Delete
    3. Yeah it's all about some data that I need in lots of requests. Since some Employee from Employer A can't access data from Employer B etc. So many requests need these data to be performed correctly and retrieve them from the DB each time is too costly imo.

      Delete
  4. great tutorial...works perfectly..
    but why Authorities was given as a collection?? Cant it be given as simply a String??

    ReplyDelete
    Replies
    1. Authorities objects are returned as Collection because my Authority implementation implements the GrantedAuthority interface to comply with Spring-Security.

      Delete
  5. NIILZON-LE-SAUCISSONMarch 12, 2014 at 9:05 PM

    A nice article bro :)

    ReplyDelete
  6. Thanks for sharing this.. If you have time kindly check out my blog http://javapointers.com

    ReplyDelete
  7. Thanks for sharing this.. If you have time kindly check out my blog http://javapointers.com

    ReplyDelete
  8. Thanks , I was looking for this , works well

    ReplyDelete
  9. nice article, but how to change user role (user management) ?
    thanks

    ReplyDelete
    Replies
    1. In such situation you could either destroy a user's session so that he's force to log in over again (obviously, you'd first implement a notification system to let the user know what's happening), or more user friendly, you could simply reload the user's Principal object with the up-to-date granted authorities.

      For more implementation details, I suggest you look up the matter on StackOverflow ;-)

      Delete
  10. Dear Sir,
    I m new to Spring MVC and still learning.Your post is great and very well explained!I tried your example and it works great..thought it might sound dumb could you give an example of the case where we have a DB and we have made a repository class, what do you mean (how) do we inject it int that class?I mean what should the DAOServiceImpl should do with "ResultSet" after it get the corresponding fields?Does it have to return something or does it update a Model class? (Sorry for such an obvious thing like that but I am confused with all that stuff I see on web)
    Thanks for your time and post,happy coding everyone!

    ReplyDelete
    Replies
    1. Hi,

      Based on the example I described in this post, you'd just need to inject an instance of your DAOServiceImpl class (I presume it is a Spring-managed bean) into the UserDetailsServiceImpl instance, using the @Autowired annotation. Then in the loadUserByUsername( ) method, you would call the appropriate method from your DAOService class to retrieve the user from your DB. The last step is to build the UserDetails object using the user that has been retrieved from the DB.

      Hope that helps ;-)

      Delete
    2. You Sir made my day,
      thank you very much for your reply. I was trying to do that but (thought faulty it didn't work) due to a single letter mismatch in GrantedAuthorityImpl;So I started looking the word inject you mentioned and started reading about @inject and got paniced..So after reading what you answered I gave it 1 more chance and yeap I did it..
      Again thank you very much for you time and this post, which is the only so well explained over the web..

      Delete