Build Status License Website

Spring security demo app

  • spring security with form login AND secured REST api with http basic auth
  • in memory authent for admin user AND jpa/hibernate persistence for users
  • thymeleaf templates for the ui
  • tests: security is tested for all html pages and the REST api
  • use of a domain model

Try

Openshift instance is here

Build then Run

Description

Security

Security stuff are gathered under the com.orange.spring.demo.security package. We rely on annotations to configure the application, there is no xml configuration. I am not saying that we should not use xml configuration, just that it is not the case here.

enable security, password hash and admin account

In the GlobalSecurityConfig class, we do the following things:

  • we annotate the class with @EnableWebSecurity to enable spring security
  • we configure a password encoder @Bean for user passwords hashing. Then spring security can use this bean to check passwords for us, and we can use it as well when we create a new user.
  • we setup the admin account "in memory". This way it is easier to start, but the drawback of this approach is that you need to update the application when you want to add admin users or update their passwords. For users we rely on a database.

So it looks like this:

@EnableWebSecurity
public class GlobalSecurityConfig {

  @Autowired
  @Qualifier("userDetailsService")
  private UserDetailsService userDetailsService;

  @Bean
  public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
  }

  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    AppSecurityAdmin.addAdminInMemory(auth);
  }
}

And for AppSecurityAdmin:

public class AppSecurityAdmin {

  public static void addAdminInMemory(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            .withUser("admin")
            .password("admin password")
            .authorities(AppSecurityRoles.authorities());
  }
}

There are three security levels in the application:

  • anonymous
  • authenticated user: 'ROLE_USER'
  • admin: 'ROLE_ADMIN'

These roles are configured at start up in AppSecurityRoles. First we check if the roles have been created in the DB, and if not we create it.

  ...
  public enum Role { ROLE_USER, ROLE_ADMIN }

  @Autowired
  private UserRoleRepository userRoleRepository;

  @PostConstruct
  void init() {
    addRoles();
  }

  private void addRoles() {
    if (userRoleRepository.count() == 0) {
      log.info("Add user roles");
      userRoleRepository.save(
              Arrays.asList(new UserRoleDB[] {
                      new UserRoleDB(ROLE_USER), new UserRoleDB(ROLE_ADMIN)
              })
      );
    }
  }
  ...

Distinct security authentication mechanisms for form login and REST api

To authenticate users, we can rely on several mechanisms:

  • session based: we create a new session after the user has successfully signed in through the login page. Then the session id present in http requests headers will let us do authentication.
  • token based: OAuth, json web token, etc are another mean, typically for stateless applications. See here and a spring demo here
  • http basic: the login and password of the user are encoded in the header of http requests. See here.

On this application we want to use:

  • regular session based authentication for form login / html pages
  • http basic authentication for the REST api

To do this we have defined two distinct WebSecurityConfigurerAdapter doc:

Both classes extends WebSecurityConfigurerAdapter and override the configuremethod to configure the HttpSecurity instance.

Here we need to define a priority: in the security chain pipeline, which configurer will be applied first? the problem is solved in the ApiSecurityConfig class with the @Order(1) annotation: the REST api configurer will be applied first.

Secure the REST api

It is done in the ApiSecurityConfig class:

@Configuration
@Order(1)
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    disableCsrfForNonBrowserApi(http);
    initApi(http);
  }

  private void initApi(HttpSecurity http) throws Exception {
    http
            // configure the HttpSecurity to only be invoked when matching the provided ant pattern
            .antMatcher("/ws/**")
            // configure restricting access
            .authorizeRequests()
            // open api is... opened
            .antMatchers("/ws/open/**").permitAll()
            // admin api restricted to... ADMIN
            .antMatchers("/ws/admin/**").hasRole("ADMIN")
            // and the rest is allowed by any authenticated user
            .antMatchers("/ws/sec/**").authenticated()
            .and()
            .httpBasic();
  }

  private void disableCsrfForNonBrowserApi(HttpSecurity http) throws Exception {
    http.csrf().disable();
  }
}

Since csrf protection is useless for REST api, we disable it. See spring doc as well. Note the @Order to apply REST security configurer first.

Secure the MVC part with form login & csrf protection while allowing webjars and h2-console

It is done in the BrowserSecurityConfig class:

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private Environment environment;

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

    http
            // configure the HttpSecurity to only be invoked when matching the provided ant pattern
            .antMatcher("/**")
            // configure restricting access
            .authorizeRequests()
                // open api is... opened
                .antMatchers("/").permitAll()
                // admin api restricted to... ADMIN
                .antMatchers("/admin/**").hasRole("ADMIN")
                // and the rest is allowed by any authenticated user
                .anyRequest().authenticated()
            .and()
                // setup login & logout
                .formLogin()
                .loginPage("/login")
                .permitAll()
            .and()
                .logout()
                .logoutSuccessUrl("/")
                .permitAll();
  }

  private void disableSecurityOnWebJars(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/webjars/**").permitAll();
  }

  private void disableSecForDBConsole(HttpSecurity http) throws Exception {
    if (isDevProfile()) {
      log.warn("Disable security to allow H2 console");
      String url = "/h2-console/**";
      http.csrf().ignoringAntMatchers(url);
      http.authorizeRequests().antMatchers(url).permitAll();
      http.headers().frameOptions().disable();
    }
  }

  private boolean isDevProfile() {
    return Arrays.asList(environment.getActiveProfiles()).contains("dev");
  }
}

So what are we doing here:

  • as we use webjars to retrieve client libraries like jQuery or Bootstrap, we need to disable security to allow the client to access these javascript files!
  • as we use hibernate to store user accounts, we need to disable security in development mode, to access the h2-console. Check this [link](here as well
  • then we configure authorized requests, for anonymous, authenticated users, admin...

Test a secured app...

The first time you enable security in your application, there is good chance that many integration or unit tests will fail.

There are two points to consider here:

  • you think you have secured the application. You need to write tests to be sure!
  • how to ease business tests when security is annoying you, ie. business tests fail not due to a bug but due to a restricted access problem.

A few links which can help:

A few points worthy to notice:

  • some annotations let you mock authentication in your tests: @WithMockUser @WithAnonymousUser @WithUserDetails. Check the [doc](Spring security doc on testing). So if a business test requires to be authenticated, just mock it with @WithMockUser and you are (almost) done.
  • MockMvc is just great to test your controllers, see the doc

Indeed, MockMvc lets you perform requests with mocked csrf protection, or with http-basic, etc.

For instance to test REST authentication with http basic:

  @Test
  public void retrieveUsersAsAnAuthenticatedUser() throws Exception {
    // given
    mockMvc
            // do
            .perform(MockMvcRequestBuilders.get("/ws/sec/users")
                            .with(httpBasic(username, password))
                            .accept(MediaType.APPLICATION_JSON))
            // then
            .andExpect(status().isOk());
  }

or to do a request with mocked csrf and authenticated as admin:

  @Test
  @WithMockUser(username = "admin", password = "admin password", roles = "ADMIN")
  public void adminIsAuthorizedToCreateUser() throws Exception {
    // given
    mockMvc
            // do
            .perform(
                    post("/admin/user/create")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                            .param("username", "titi")
                            .param("password", "toto")
            )
            // then
            .andExpect(status().isOk());
  }

Domain

We try to follow this architecture:

  • a persistence layer
  • a domain layer, which has no dependency to the persistence layer (so that changes to the persistence layer should not impact the domain).
  • a service layer which is the interface between the domain and the persistence layers
  • a controllers layer which rely on domain objects and services

In addition we try to have immutable objects in the domain:

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
/** let's try to be immutable **/
public class User {
  // as we are immutable, we do not need getters and we can allow direct access to fields
  public final long id;
  public final String username;
}

About @RequiredArgsConstructor, it is just a sugar, see lombok doc: it writes the constructor for us with mandatory (final) fields.

License, authors

GPLv2, Copyright (C) 2016 Orange

Christophe Maldivi & Denis Boisset

My side notes

TODO

  • use rolling logs to avoid filling the disk...
  • retry to use pre/post auth for repository, see spring data example here. Right now using "@EnableGlobalMethodSecurity(prePostEnabled = true)" involves a "object already built" exception. Maybe due to 1.4.0_M2 version?
  • apply the uniq constraint on userDB
  • improve UI to let admin remove users
  • improve UI to let admin create a user as an administrator

IntelliJ notes

As we use lombok, you need to add the lombok plugin to IntelliJ

Hibernate

console is enabled in dev mode (disabled by default)

More details here

Hot reloading

a - Code hot reload

  • run the server on the command line with maven (mvn spring-boot:run)
  • in IntelliJ, make sure that you have selected: Preferences->'Build, Execution, Deployment'->Compiler->'Make project automatically'
  • for thymeleaf templates, it is necessary to force a build to be sure to use the new version

b - Browser hot reload

Here we need to install the 'livereload' plugin in the browser, see here