Build a Spring-Boot REST service with Basic Authentication for several users

This post describes how to build a REST service with Spring-Boot that uses Basic-Authentication for several users and that uses the username of the authenticated user to do it’s work. Warning: A service using basic authentication should always use HTTPS as transport protocol, either by running behind a web server proxy or by setting up HTTPS by itself. I’ll cover the latter in a later post.

This might be a setup for a service, where for each user, data is stored in a database, so it not only is necessary to authenticate the user to use the service, but it is also necessary in the service to know which user is accessing the service.

The source code for this project can be found at GitHub. The coce relevant for this post is tagged post-20150728.

Setting up the project

I use IntelliJ IDEA (EAP Version 15 with Spring support) for the project and a maven project setup.

To create the initial project, I use Spring Initializr from within IDEA, but you can use https://start.spring.io as well to create the project. I named the project SecuRest, the initial dependencies are Core/Security and Web/Web:

Bildschirmfoto 2015-07-28 um 13.13.57

The created pom.xml looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.sothawo</groupId>
  <artifactId>securest</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>SecuRest</name>
  <description>Demo project for Spring Boot with Basic Authentication and HTTPS</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.2.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
  

</project>

The Service

The first version of the implemented service just has one method which echoes it’s call arguments back:

/**
 * Copyright (c) 2015 sothawo
 *
 * http://www.sothawo.com
 */
package com.sothawo.securest;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * Sample service.
 *
 * @author P.J. Meisch (pj.meisch@sothawo.com).
 */
@RestController
@RequestMapping("/service")
public class UserService {

    @RequestMapping(value = "/echo/{in}", method = RequestMethod.GET)
    public String echo(@PathVariable(value = "in") final String in) {
        return "You said: " + in;
    }
}

There is nothing yet to retrieve the name of the current user, we’ll do that later.

Running the application with the basic setup

This is all that is needed for the first basic application sceleton. When the application ist started, Spring-Boot sets up the security so that the whole application is secured and can only be accessed by a user user; the password is displayed on application startup in the log in a line like this (the password changes each time the application starts). This is the basic behaviour of Spring-Boot when spring-boot-starter-security is found on application startup.

Using default security password: e482e82e-1115-4fc4-86e4-bdcc432da039

When trying to access the application without passing in a username and password, the following result is returned:

curl localhost:8080/service/echo/hello
{"timestamp":1438088762118,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/service/echo/hello"}

With curl I can pass in the username and password like this:

curl user:e482e82e-1115-4fc4-86e4-bdcc432da039@localhost:8080/service/echo/hello
You said: hello

As an alternative I use IntelliJ’s REST Tool to create an authorization header for the user user and password e482e82e-1115-4fc4-86e4-bdcc432da039 and pass it along with the request:

curl -H "Authorization: Basic dXNlcjplNDgyZTgyZS0xMTE1LTRmYzQtODZlNC1iZGNjNDMyZGEwMzk=" localhost:8080/service/echo/hello
You said: hello

So now access to the service is granted. But, as I said, there is only one user named user allowed, and the password changes every time on application startup. Next, I’ll set up some users with passwords.

 

Setting up the Users

I am using the most basic form to setup different users, and that is by using an in-memory user store. To have that I add the following configuration class:

/**
 * Copyright (c) 2015 sothawo
 *
 * http://www.sothawo.com
 */
package com.sothawo.securest;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * Security configuration.
 *
 * @author P.J. Meisch (pj.meisch@sothawo.com).
 */
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user1").password("secret1").roles("USER")
                .and()
                .withUser("user2").password("secret2").roles("USER");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().fullyAuthenticated();
        http.httpBasic();
        http.csrf().disable();
    }
}

This configuration class configures two users with their passwords and roles. And although when starting the application, a password for the default user user is shown in the log, that user no longer can access the application, only the new configured two users can.

By using @EnableWebMvcSecurity and not @EnableWebSecurity the application has access to the current user in the following way:

/**
 * Sample service.
 *
 * @author P.J. Meisch (pj.meisch@sothawo.com).
 */
@RestController
@RequestMapping("/service")
public class UserService {

    @RequestMapping(value = "/echo/{in}", method = RequestMethod.GET)
    public String echo(@PathVariable(value = "in") final String in, @AuthenticationPrincipal final UserDetails user) {
        return "Hello " + user.getUsername() + ", you said: " + in;
    }
}

After restarting the application the service gives the following result:

curl user1:secret1@localhost:8080/service/echo/hello
Hello user1, you said: hello

So our service is protected by username and passwords and has access to the name of the current user.

Notice: As written at the beginning of this post, it is necessary to use HTTPS transport. This is covered in a later post.

Java Webservice using HTTPS part 2

This article shows how to implement a HTTPS web service connection where the server only allows trusted clients to connect. It extends and uses the information given in this article.

First, it is necessary to create a certificate for the client and to store it in the client’s keystone:

keytool -genkey -keystore client_keystore.ks -alias client

This certificate must be exported from the client’s keystore…

keytool -export -alias client -keystore client_keystore.ks -file client.cer

and imported into the server’s truststore:

keytool -import -alias client -file client.cer -keystore server_truststore.ks

The server must now be given the information about the truststore, and must request the client authentication:

…
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManagerFactory;
import javax.xml.ws.Endpoint;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsParameters;
import com.sun.net.httpserver.HttpsServer;
 …
 
httpsServer = HttpsServer.create(new InetSocketAddress(interfaceName, port), 0);
SSLContext sslContext = SSLContext.getInstance("TLS");
 
// keystore
char[] keystorePassword = "keystore_password".toCharArray();
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream("server_keystore.ks"), keystorePassword);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, keystorePassword);
// truststore 
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
char[] truststorePassword = "truststore_password".toCharArray();
ks.load(new FileInputStream("server_truststore.ks"), truststorePassword);
tmf.init(ks);

sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
 
HttpsConfigurator configurator = new HttpsConfigurator(sslContext) {
    /* (non-Javadoc)
     * @see com.sun.net.httpserver.HttpsConfigurator#configure(com.sun.net.httpserver.HttpsParameters)
     */
     @Override
     public void configure(HttpsParameters params) {
         SSLParameters sslParams = getSSLContext().getDefaultSSLParameters();
         sslParams.setNeedClientAuth(true);
         params.setSSLParameters(sslParams);
      }
 };
httpsServer.setHttpsConfigurator(configurator);
 
HttpContext httpContext = httpsServer.createContext("/path");
Endpoint endpoint = Endpoint.create(serviceImpl);
endpoint.publish(httpContext);
…
httpsServer.start()

For the client, in addition to the truststore settings, now it is necessary to set the System properties for the keystore (either as shown from within the client or by passing the corresponding -D arguments to the Java VM):

System.getProperties().put("javax.net.ssl.trustStore", "client_truststore.ks");
System.getProperties().put("javax.net.ssl.trustStorePassword", "truststore_password");
System.getProperties().put("javax.net.ssl.keyStore", "client_keystore.ks");
System.getProperties().put("javax.net.ssl.keyStorePassword", "keystore_password");