Ingini

Authentication & authorization with Spring Boot & MongoDB

Introduction

Ivan Hristov

Ivan Hristov


Java Spring Jongo Spring Boot Spring Security Authentication Authorization Denormalization

Authentication & authorization with Spring Boot & MongoDB

Posted by Ivan Hristov on .
Featured

Java Spring Jongo Spring Boot Spring Security Authentication Authorization Denormalization

Authentication & authorization with Spring Boot & MongoDB

Posted by Ivan Hristov on .

Abstract: What should we denormalize and what should we not? How many times have you heard this question in the context of MongoDB? As a follow up of this question there is often a counter example of how we used to do it with a relational database. In this article we are going to look at some schema design considerations from a relational and document-oriented perspective. Then we will implement an authentication & authorization solution using Spring Boot, Spring Security, and Jongo.

Goal: Comparison between relational and document-oriented schema designs for an authentication & authorization solution. Learn how to setup MongoDB as a data source for Spring Security. See how to wrap up everything in Spring Boot.

Acknowledgement: My gratitude goes to the open source community.

Code: A repository dedicated to this article is located on GitHub: Spring Boot & MongoDB

Maybe this is the first time that you have to create an authentication & authorization (A&A) solution. Maybe you have already worked on such a problem once or twice. Regardless of what is your experience on this subject, here is a short setup of a common way to tackle this task with a relational database.
Note: For the sake of brevity and the KISS principle, I've omitted some parts of the complete Security Database Schema of Spring Security.

First, you need a USER table to contain at least the username and the password:

Table: USER

| ID | USERNAME | PASSWORD |
----------------------------
| 10 | user     | pass     |

Second, you need a ROLE table to stock all roles a user can have:

Table: ROLE

| ID | ROLE |
-------------
| 20 | ADMIN |

And now of course a third table to link the previous two:

Table: USER_ROLES

| USER_ID | ROLE_ID |
---------------------
| 10      | 20      |

This is all we need in terms of a nicely normalized relational database design. Simple right? Three tables each having it's purpose.

Now, let us take a step back and ponder over this schema. Our tables are designed in a fashion that avoids redundancy. That is of course, if we assume that passwords are properly salted and usernames and roles are unique. Then the only duplication of data that we might have under this assumption is in the third, intermediary table which is also called a join table or associative table.

Some will argue that this schema brings also higher degree of data integrity and consistency. Some will argue that indeed data integrity and consistency are higher but we are trading off referential integrity.

So how does this bring us closer to solving our client's problem of having a system for authentication and authorization purposes? Well, it does not. The chances that you will find a client who will state: "I want an A&A solution but mind the redundancy / integrity / consistency!" are pretty low (if such chances exist at all).

Provided that we believe that most human beings are responsible when it comes to security and adding some marge for unforseen situations. It is safe to assume that the person who will choose what roles will exist in a system will rename them no more than 5 times per year. Thanks to this statement we can simplify the tables to only have two that look like:

Table: USERS

| ID | USERNAME | PASSWORD |
----------------------------
| 10 | user     | pass     |

Table: USER_ROLES

| USERNAME | ROLE |
-------------------
| user     | ADMIN|

We can go even further as to have one single USER table with a ROLES column of an array type. Some database such as Oracle and PostgreSQL have support for array data type.

Let's go one step further and ask: Why? Why do we work with tables and represent users as rows?

Truth to be told from a client perspective it does not matter. How we represent data is our concern as engineers and not something that our client want or need to be aware of. If it's easier for you to work by representing users as rows than go that way. If on the other hand you think it's more reasonable and intuitive to keep a document describing a user then let's see how we can resolve the problem with MongoDB.

MongoDB is a document oriented database that uses a binary JSON format (called for short BSON) to represent entities, such as our user. In order to represent the information concerning a user we need this:

Document: USER

{
  _id: <id_generated_or_given_by_us>
  username: 'user',
  password: 'pass',
  roles: ['ADMIN']
}

The above few rows form a document in MongoDB terminology. This document is stored in a collection, for example named as users. That's all we need to have the data stored in our MongoDB to answer the problem who is authorized to do what and how is he authenticated.

I leave to the dearest reader to decide for himself which approach he/she likes - a table based or a document based. For those of you who would like to go with the document based approached the next few lines will teach you how to use Spring Boot, Spring Security and Jongo to authenticate and authorise a user. To avoid pasting all the code here, I've setup a GitHub repo which needs running MongoDB on 127.0.0.1:27017 with a database called test inside which there is a collection users. For the purpose of testing you will need to create a document inside the collection by opening the mongo shell and executing:

db.users.insert({username: 'user', password: 'pass', roles: ['ADMIN']});  

That being done, let's see the Java-based Spring Configuration:

File: InginiMain.java

package org.ingini.spring.boot.mongodb;

import com.mongodb.DB;  
import com.mongodb.MongoClient;  
import com.mongodb.MongoException;  
import org.jongo.Jongo;  
import org.jongo.MongoCollection;  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.context.annotation.Bean;

import java.net.UnknownHostException;

@SpringBootApplication
public class InginiMain {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(InginiMain.class, args);
    }

    @Bean
    public Jongo jongo() {
        DB db;
        try {
            db = new MongoClient("127.0.0.1", 27017).getDB("test");
        } catch (UnknownHostException e) {
            throw new MongoException("Connection error : ", e);
        }
        return new Jongo(db);
    }

    @Bean
    public MongoCollection users() {
        return jongo().getCollection("users");
    }
}

What is important to note here is the MongoDBAuthenticationProvider which is a custom made class extending Spring AbstractUserDetailsAuthenticationProvider class. Here what it looks like:

File: MongoDBAuthenticationProvider.java

package org.ingini.spring.boot.mongodb.security;

import org.ingini.spring.boot.mongodb.domain.Client;  
import org.jongo.MongoCollection;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.security.authentication.InternalAuthenticationServiceException;  
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;  
import org.springframework.security.core.AuthenticationException;  
import org.springframework.security.core.userdetails.User;  
import org.springframework.security.core.userdetails.UserDetails;  
import org.springframework.stereotype.Service;

@Service
public class MongoDBAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Autowired
    private MongoCollection users;

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    }

    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        UserDetails loadedUser;

        try {
            Client client = users.findOne("{#: #}", Client.USERNAME, username).as(Client.class);
            loadedUser = new User(client.getUsername(), client.getPassword(), client.getRoles());
        } catch (Exception repositoryProblem) {
            throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
        }

        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }
}

To save some space, I won't paste the details about the Client.java class which you can check on-line.

Rather, let's concentrate on the Spring Security configuration:

File: SecurityConfig.java

package org.ingini.spring.boot.mongodb.config;

import org.ingini.spring.boot.mongodb.security.MongoDBAuthenticationProvider;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.boot.autoconfigure.security.SecurityProperties;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.core.annotation.Order;  
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;  
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.annotation.web.builders.WebSecurity;  
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;  
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MongoDBAuthenticationProvider authenticationProvider;

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.formLogin().defaultSuccessUrl("/resource")
                .and().logout().and().authorizeRequests()
                .antMatchers("/index.html", "/home.html", "/login.html", "/", "/access", "/logout").permitAll().anyRequest()
                .authenticated()
                .and().csrf().disable();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }
}

Note that I've set

@EnableGlobalMethodSecurity(securedEnabled = true)

This will trigger verifycation of Spring Security's @Secured annotation. Like for example in the ResourceController.java:

package org.ingini.spring.boot.mongodb.controller;

import org.ingini.spring.boot.mongodb.domain.Client;  
import org.jongo.MongoCollection;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.security.access.annotation.Secured;  
import org.springframework.security.core.userdetails.UserDetails;  
import org.springframework.security.web.bind.annotation.AuthenticationPrincipal;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;  
import java.util.Map;

@RestController
public class ResourceController {

    @Autowired
    private MongoCollection users;

    @Secured({"ROLE_ADMIN"})
    @RequestMapping("/resource")
    public Map<String, Object> home(@AuthenticationPrincipal UserDetails userDetails) {
        Client client = users.findOne("{#: #}", Client.USERNAME, userDetails.getUsername()).as(Client.class);
        Map<String, Object> model = new HashMap<>();
        model.put("roles", client.getRoles());
        return model;
    }
}

... and that's almost everything you need to have an A&A solution. Don't forget to check-out the GitHub repo for the full project.

P.S.
To test the solution you can use Chrome Postman REST client or CURL.

Update: 27th of April 2015

Question (re-printed for clarity from a comment):

Would it not be better to define a UserDetailsService and configure your AuthenticationManagerBuilder with that?

You should consider a custom AbstractUserDetailsAuthenticationProvider if you need more flexibility than a custom UserDetailsService can provide.
In a production environment, I would suggest to stick to the default implementation - the DaoAuthenticationProvider unless you need to add or modify default security checks. In case you are interested, I've created a branch feature/user_details_service which works with a custom implementation of UserDetailsService.

Ivan Hristov

Ivan Hristov

http://ingini.org

View Comments...