Spring boot Oauth2 with MongoDb e custom authentication

In this article I’m going to illustrate the implementation of Spring boot security Oauth2 from both the server and the client side. The example uses NoSQL Db as MongoDB, a choice that I think it’s optimal for this solution.

Starting from Jeebb GitHub (available here)  I rework the example including some parts that are commons in a real world case; don’t misunderstand me, Jeebb’s solution works well and using a database for storing the data is very common in real use cases.

Nonetheless, I adding some part as a custom confirmation page and an external authentication system (based on a cookie).

That’s because I’ve looked a lot of solutions where the credentials data are commonly stored in an external resource from the Oauth database.

The actors are always the same:

  • Client: the client application which tries to connect at the protected resources;
  • Oauth2 Authorization: the application that authorizes, releases and validates the token;
  • Resource: the resource protected by the Oauth2 Authorization server.

I’m not going to show again the sequence diagram of Oauth2; if you’re looking for that just have a look my previous post here.

Now, what have I updated from the Jeebb’s solution? The main issue is in the user’s authentication before starting the authorization process in Oauth2.

According to Spring Security framework, you can add a filter to achieve this:


@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    .addFilterBefore(new CustomFilter(authenticationManagerBean()),BasicAuthenticationFilter.class)
        .authorizeRequests()
            .antMatchers("/login").permitAll()
            .antMatchers("/**").authenticated()                                    
        .and()
        .exceptionHandling()
		.authenticationEntryPoint(
				new CustomLoginUrlAuthenticationEntryPoint("/login"))
          .and()
            .userDetailsService(userDetailsService());
        
}

All right, at the row 4 is declared CustomFilter instance and this is the class definition


public class CustomFilter extends GenericFilterBean {

   private AuthenticationManager authenticationManager;
   
   private static final String COOKIE_NAME = "authentication";
   
   public CustomFilter(AuthenticationManager authenticationManager) {
      this.authenticationManager = authenticationManager;
   }

   @Override
   public void doFilter(ServletRequest request, ServletResponse response,
         FilterChain chain) throws IOException, ServletException {

      HttpServletResponse httpReponse = (HttpServletResponse) response;
      HttpServletRequest httpRequest = (HttpServletRequest) request;
      
      User user = getCookieCredential(httpRequest);
      
      if (user != null) {
         
         UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
               user.getPrincipal(), user.getCredential(),
               AuthorityUtils.createAuthorityList("ROLE_USER"));

         Authentication authResult = this.authenticationManager
               .authenticate(authRequest);
         /*
          * User Authenticate
          */
         SecurityContextHolder.getContext().setAuthentication(authResult);
      }
      else
      {
         Cookie cookie = new Cookie(COOKIE_NAME, null); 
         cookie.setPath("/");
         cookie.setHttpOnly(true);
         cookie.setMaxAge(0); // Don't set to -1 or it will become a session cookie!
         httpReponse.addCookie(cookie);
      }

      chain.doFilter(httpRequest, httpReponse);

   }

   private User getCookieCredential(HttpServletRequest httpRequest) {
	
   Cookie[] cookies = httpRequest.getCookies();

   if (cookies != null)
      for (Cookie cookie : cookies) {
         if (cookie.getName().equals(COOKIE_NAME)) {
            byte[] decodedBytes = Base64.getDecoder().decode(
                  cookie.getValue());
            String decodedString = new String(decodedBytes);
            String[] credentialSplitted = decodedString.split(":");
            return new User(credentialSplitted[0], credentialSplitted[1]);
         }
      }
   
   return null;
   }
}

The overridden method doFilter checks the cookie value and extracts the user credential (stored as plain base64 format) into a User object and then set into the security context.

Differently from Jeebb’s solution, I removed the database access to check the user credentials at the CustomUserDetailsService. I demand this at the cookie value.


public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /*
    	Query query = new Query();
        query.addCriteria(Criteria.where("username").is(username));
        MongoUser user =
                mongoTemplate.findOne(query, MongoUser.class);
        if (user == null) {
            throw new UsernameNotFoundException(String.format("Username %s not found", username));
        }

        String[] roles = new String[user.getRoles().size()];

        return new User(user.getUsername(), user.getPassword(),
                AuthorityUtils.createAuthorityList(user.getRoles().toArray(roles)));
         */
    	return new User(username, username, AuthorityUtils.createAuthorityList("ROLE_USER"));
    	
    }
}

I don’t care to store the password in this step, the authentication process is relegate at the cookie value.

The cookie is generated in the “fake” login process. The code is the following.


@RequestMapping("/login")
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request, 
   HttpServletResponse response, 
@CookieValue(value="authentication", required=false) String cookieAuthentication) throws Exception {
	
  	/*
  	 * Check authentication
  	 */
  	model.putAll(request.getParameterMap());
  	
  	if (cookieAuthentication!=null)
     return new ModelAndView(authorizationPage, model);
  	else
     return new ModelAndView(login, model);
}

and the presentation layer is:

<html>
<head>
<style type="text/css">
body
{	
	font-family: Verdana, Geneva, sans-serif; 
	font-size: 12px;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<script type="text/javascript">
$(document).ready(function() {
	
	$("#auth").on("submit", function(){

		// Create Base64 Object
		var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(e){var t="";var n,r,i,s,o,u,a;var f=0;e=Base64._utf8_encode(e);while(f<e.length){n=e.charCodeAt(f++);r=e.charCodeAt(f++);i=e.charCodeAt(f++);s=n>>2;o=(n&3)<<4|r>>4;u=(r&15)<<2|i>>6;a=i&63;if(isNaN(r)){u=a=64}else if(isNaN(i)){a=64}t=t+this._keyStr.charAt(s)+this._keyStr.charAt(o)+this._keyStr.charAt(u)+this._keyStr.charAt(a)}return t},decode:function(e){var t="";var n,r,i;var s,o,u,a;var f=0;e=e.replace(/[^A-Za-z0-9+/=]/g,"");while(f<e.length){s=this._keyStr.indexOf(e.charAt(f++));o=this._keyStr.indexOf(e.charAt(f++));u=this._keyStr.indexOf(e.charAt(f++));a=this._keyStr.indexOf(e.charAt(f++));n=s<<2|o>>4;r=(o&15)<<4|u>>2;i=(u&3)<<6|a;t=t+String.fromCharCode(n);if(u!=64){t=t+String.fromCharCode(r)}if(a!=64){t=t+String.fromCharCode(i)}}t=Base64._utf8_decode(t);return t},_utf8_encode:function(e){e=e.replace(/rn/g,"n");var t="";for(var n=0;n<e.length;n++){var r=e.charCodeAt(n);if(r<128){t+=String.fromCharCode(r)}else if(r>127&&r<2048){t+=String.fromCharCode(r>>6|192);t+=String.fromCharCode(r&63|128)}else{t+=String.fromCharCode(r>>12|224);t+=String.fromCharCode(r>>6&63|128);t+=String.fromCharCode(r&63|128)}}return t},_utf8_decode:function(e){var t="";var n=0;var r=c1=c2=0;while(n<e.length){r=e.charCodeAt(n);if(r<128){t+=String.fromCharCode(r);n++}else if(r>191&&r<224){c2=e.charCodeAt(n+1);t+=String.fromCharCode((r&31)<<6|c2&63);n+=2}else{c2=e.charCodeAt(n+1);c3=e.charCodeAt(n+2);t+=String.fromCharCode((r&15)<<12|(c2&63)<<6|c3&63);n+=3}}return t}}

		// Define the string
		var string = $("#uid").val() + ":" + $("#pwd").val();

		// Encode the String
		var encodedString = Base64.encode(string);
		console.log(encodedString); // Outputs: "SGVsbG8gV29ybGQh"
		
		$.cookie("authentication", encodedString, {
			   expires : 1,           //expires in 10 days

			   path    : '/',          //The value of the path attribute of the cookie 
			                           //(default: path of page that created the cookie).

			   domain  : 'localhost',  //The value of the domain attribute of the cookie
			                           //(default: domain of page that created the cookie).

			   secure  : false          //If set to true the secure attribute of the cookie
			                           //will be set and the cookie transmission will
			                           //require a secure protocol (defaults to false).
			});
		
	});
});
</script>
</head>
<body>
<form method="post" action="/login" id="auth">
    <table cellpadding="5" cellspacing="5" style="margin-left: auto; margin-right: auto;">       
        <tr>
            <td><b>Login ID</b></td>
            <td>
                <input type="text" id="uid" />
            </td>
        </tr>
        <tr>
            <td><b>Password</b></td>
            <td>
                <input type="password" id="pwd" />
            </td>
        </tr>
        <tr>
            <td colspan="2" align="right">
                <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
                <input type="hidden" name="client_id" value="${client_id[0]}" />
                <input type="hidden" name="response_type" value="${response_type[0]}" />
                <input type="hidden" name="redirect_uri" value="${redirect_uri[0]}" />                
                <input type="reset" value="Clear" />
                <input type="submit" value="Login" />
            </td>
        </tr>
    </table>
</form>
</body>
</html>

It’s not the scope of this post illustrates the authentication process using Spring Security framework; I know the code above is unusable in production environment, I use it only for example purpose.

Another common pattern is when you’re asked to customize the confirmation page; Spring boot makes it easy to complete as task. It’s enough to rewrite the code from org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint and activate the Spring Mvc controller in the application file:

#Spring view resolver
spring.mvc.view.prefix=/WEB-INF/
spring.mvc.view.suffix=.jsp

Also, I made the choice to put the redirect_uri into the authorization and token Url in the client side.
Have a look at that; the configuration file changed as:

...
oauth.auth.url=http://localhost:8081/oauth/authorize?client_id=web-client&response_type=code&redirect_uri=http://localhost:8080
oauth.token.url=http://localhost:8081/oauth/token?grant_type=authorization_code&code=%s&redirect_uri=http://localhost:8080
oauth.token.refresh.url=http://localhost:8081/oauth/token?grant_type=refresh_token&refresh_token=%s
...

It’s time to run! The complete solution is available here.

Summarizing, I got a lot of problem to debug the solution for understanding the process flow; unfortunately, this module is not well documented as a lot of other modules but it’s only provided as appendix from Spring Security solution with a short tutorial online.
I hope that Spring’s developer groups can sort out a better support in the future version, this would be very helpful.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.