Reactive RESTful service with Spring 5, Spring Boot 2 and MongoDB (part 2)

In this second post of the series “Reactive RESTful service with Spring 5, Spring Boot 2 and MongoDB” we will see how to useSpring Security OAuth2 and SSL to secure the service created in part1

The project is available on github.

Overview

ssauth2-archi

  1. The client requests a new token from the Authorization Server by sending a POST to /oauth/token
  2. The server authenticates the client and returns a payload containing the token.
  3. The Resource Server intercepts calls to the REST api and checks if the client has provided a token in the HTTP header
  4. The token is validated with the Authorization server.
  5. If the token is valid then the resource is returned.

Tokens have an expiry time so it’s the responsibility of the client to request a new one whenever the current token expires.

Project dependencies

We start by adding spring-boot-starter-security starter module to enable Spring Security, then we add the dependency spring-security-oauth2 to enable OAuth2 and finally we add spring-security-test dependency for tests.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security.oauth</groupId>
	<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-test</artifactId>
	<scope>test</scope>
</dependency>

Authorization Server

Spring provides @EnableAuthorizationServer annotation along with AuthorizationServerConfigurerAdapter class that can be extended to create an Authorization Server

@EnableAuthorizationServer
@Configuration
public class OAuth2AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
....
}

The class can be customized by overriding AuthorizationServerConfigurerAdapter’s methods which is what we will do to add an in-memory client (for ease of use)

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
	clients.inMemory()
		.withClient("clientId")
		.secret("clientSecret")
		.scopes("read","write","read-write")
		.and().build();
}

The code can be made cleaner by externalizing client’s credentials to application.properties file located under src/main/resources

# OAuth2 credentials
oauth2.clientId=clientId
oauth2.secret=clientSecret
oauth2.scopes=read,write,read-write

Then injecting them with @Value annotation

package customerservice.oauth2;

public class OAuth2AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {

	@Value("${oauth2.clientId}")
	private String clientId;

	@Value("${oauth2.secret}")
	private String secret;

	@Value("${oauth2.scopes}")
	private String[] scopes;

	/* OAuth2 in memory credentials */
	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.inMemory()
			.withClient(clientId)
			.secret(secret)
			.scopes(scopes)
			.and().build();
	}
}

The client we just created has three scopes: read, write and read-write, which means it can access resources restricted to read-only operations, to write-only operation and to read-write operations.
Scopes in OAuth2 can be seen as equivalent to Spring Security roles.

Resource Server

The Resource server is created by extending ResourceServerConfigurerAdapter and annotating the class with @EnableResourceServer

package customerservice.oauth2;

@EnableResourceServer
@Configuration
public class OAuth2ResourceServerConfigurer extends ResourceServerConfigurerAdapter {

}

We don’t need to customize OAuth2ResourceServerConfigurer, but if needed this can be done by overriding ResourceServerConfigurerAdapter‘s methods.

Enable oauth2 SpEL variable

Regular Spring Security annotations support OAuth2 but this support is not enabled by default, to enable it an extra step is needed which consists in extending GlobalMethodSecurityConfiguration and annotating the new class with @EnableGlobalMethodSecurity(prePostEnabled = true)

We also need to customize the behavior of the new class by overriding the method createExpressionHandler() to return an OAuth2MethodSecurityExpressionHandler instead of the default MethodSecurityExpressionHandler, this will enable the SpEL variable oauth2 that will use with the Spring Security annotation @PreAuthorize

package customerservice.oauth2;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2GlobalMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {

	@Override
	protected MethodSecurityExpressionHandler createExpressionHandler() {
		return new OAuth2MethodSecurityExpressionHandler();
	}
}

Securing resources

Spring offers different strategies to define access controls, one of them is the Method Security Expression which offers a set of @Pre and @Post annotations that can be applied at method level.

More details can be found in the reference documentation.

For this example we will use @PreAuthorize annotation.

package customerservice.restapi;

public class CustomerController {

	@PreAuthorize("#oauth2.hasAnyScope('read','write','read-write')")
	@RequestMapping(method = GET)
	public Mono<ResponseEntity<List<Customer>>> allCustomers() {
	}

	@PreAuthorize("#oauth2.hasAnyScope('read','write','read-write')")
	@RequestMapping(method = GET, value = "/{id}")
	public Mono<ResponseEntity<Customer>> oneCustomer(@PathVariable @NotNull ObjectId id) {
	}

	@PreAuthorize("#oauth2.hasAnyScope('write','read-write')")
	@RequestMapping(method = POST, consumes = { APPLICATION_JSON_UTF8_VALUE })
	public Mono<ResponseEntity<?>> addCustomer(@RequestBody @Valid Customer newCustomer) {
	}
}

As mentioned previously, the SpEL variable oauth2 is used to restrict access to clients with certain scopes listed in hasAnyScope()

For example allCustomers() method is restricted to clients with at least one of the three scopes: ‘read’, ‘write’, ‘read-write’

Testing

All tests except CustomerServiceTest continue to run successfully, that’s because they are either unit tests or integration tests in mocked web environments where Spring Security is not active.

CustomerServiceTest on the other hand runs in a real web environment where Spring Security is active.

To make the test class run successfully we need to send a valid token every time we contact the Resource server, let’s add a method that will request a token and return it.

private String requestToken(WebClient webClient) {

	// 1
	WebClient webClientAuth = webClient.filter(basicAuthentication("clientId", "clientSecret"));

	// 2
	JsonNode tokenResp = webClientAuth.post().uri("/oauth/token")
		.contentType(APPLICATION_FORM_URLENCODED)
		.accept(APPLICATION_JSON_UTF8)
		.body(fromObject("grant_type=client_credentials"))
		.exchange()
		.flatMap(resp -> resp.bodyToMono(JsonNode.class))
		.block();

	// 3
	return tokenResp.get("access_token").asText();
}

// 1 We useclient’s credentials to do Basic Authentication

// 2 The webClient sends a POST to the Authorization Server’s URL /oauth/token and blocks until the server replies, the body of the POST specifies the grant type that is used to obtain the access token, available types are: “Authorization Code”, “Implicit”, “Password credentials” and “Client credentials”. More details are available in Section 1.3 of the OAuth2 specification.

// 3 The token is extracted from the JSON payload and returned

@Test
public void testCRUDOperationsAllTogether() throws IOException {

	final WebClient webClient = createSSLWebClient();

	final HttpHeaders headers = new HttpHeaders();
	headers.add(ACCEPT, APPLICATION_JSON_UTF8_VALUE);
	headers.add(CONTENT_TYPE, APPLICATION_JSON_UTF8_VALUE);
	// 1
	headers.add(AUTHORIZATION, String.format("Bearer %s", requestToken(webClient)));
	...
}

//1 Now that we have a token we can send it in the HTTP header every time a resource is requested.

Testing with curl

The procedure described below has been tested on Ubuntu linux only, it may need adjustments on Windows.

First we get a token from the Authorization Server

curl -v -u clientId:clientSecret http://localhost:8080/oauth/token -d grant_type=client_credentials

The Authorization server replies with a JSON payload similar to this one
{
"access_token" : "7c28125d-49e0-46b2-8bf6-4981eaf2943a",
"token_type" : "bearer",
"expires_in" : 42921,
"scope" : "read write read-write"
}

We put the token in a shell variable

Linux:
export TOKEN=7c28125d-49e0-46b2-8bf6-4981eaf2943a

Windows:
set TOKEN=7c28125d-49e0-46b2-8bf6-4981eaf2943a

Every time a resource is requested the token must be sent in the HTTP header “Authorization”

curl -i -X GET -H "Content-Type:application/json" -H "Authorization: Bearer $TOKEN" http://localhost:8080/customers

SSL

Now that authentication and authorization are managed with OAuth2 let’s secure the communication channel with SSL

For ease of use a self-signed certificate will be used which is acceptable for this example, but in real life scenarios a trusted Signed Certificate provided from a Certification Authority should be used.

All files will be generated under src/main/resources

Generate the self-signed certificate

  • cd to src/main/resources
  • keytool -genkey -alias customerservice -keyalg RSA -keysize 2048 -keystore servicestore.jks -validity 3650

The creation wizard will ask a few questions, most of them can be left to Unknown except the password field that has the value qwerty in this example and "First and last name" field that must be localhost,  or the name of the machine where the server will be deployed.

This command will create a keystore named servicestore.jks containing the self-signed certificate

SSL activation

Add the section below to application.properties located under src/main/resources and the service will be available on https://localhost:8443 instead of http://localhost:8080

# SSL configuration
server.port=8443
server.ssl.enabled=true
server.ssl.key-store=classpath:servicestore.jks
server.ssl.key-store-password=qwerty
server.ssl.key-password=qwerty

Tests

All class tests will succeed except CustomerServiceTest, that’s because they are either unit tests or integration tests that run in mocked environments where Spring Security is not active.

CustomerServiceTest fails because it’s executed in a real environment, so let’s fix it.

First we need to export the certificate to pem (Privacy-enhanced Electronic Mail) format

  • cd to src/main/resources
  • keytool -exportcert -rfc -keystore servicestore.jks -storepass qwerty -alias customerservice > servicestore.pem

Then we update CustomerServiceTest class to make it use the certificate.

package customerservice;

public class CustomerServiceTest {

	@LocalServerPort
	private int port;

	//1
	@Value("classpath:servicestore.pem")
	private Resource pemResource;

	private WebClient createSSLWebClient() throws IOException {

	//2
	final File pemFile = pemResource.getFile();
	final ClientHttpConnector clientConnector = new ReactorClientHttpConnector(options -> options.sslSupport(builder -> builder.trustManager(pemFile)));

	//3
	return WebClient.builder()
		.baseUrl(String.format("https://127.0.0.1:%d", port))
		.clientConnector(clientConnector)
		.build();
}

// 1
The pem file is injected as a resource

// 2
A reactive ClientHttpConnector is created, this connector uses the self-signed certificate as a trusted certificate.

// 3
The connector is passed to the builder of WebClient and a new instance of the client is returned.

That’s all we need, for the rest of the test SSL management is transparent.

curl

To test with curl we need to export the certificate to p12 format first, to do so we will use the command line tool openssl

  •  cd to src/main/resources
  • keytool -importkeystore -srckeystore servicestore.jks -destkeystore servicestore.pfx -deststoretype PKCS12 -srcalias customerservice -deststorepass qwerty -destkeypass qwerty
  • openssl pkcs12 -in servicestore.pfx -out servicestore.p12 -nodes

To run correctly, curl needs both pem and p12 files:

curl --cacert src/main/resoures/servicestore.pem --cert src/main/resources/servicestore.p12 -v -u clientId:clientSecret https://localhost:8443/oauth/token -d grant_type=client_credentials

The rest is similar to what has been explained previously; a token is first requested, then put into a shell variable and passed in the requests to the Resource Server.

For example, to list all customers:

curl -i -X GET -H "Accept:application/json" -H "Authorization: Bearer $TOKEN" https://localhost:8443/customers --cacert src/main/resources/servicestore.pem --cert src/main/resources/servicestore.p12

That concludes this series on creating a Reactive RESTful service with Spring 5, the source code will be updated with the beta releases of Spring 5 and Boot 2 until the GA is released.

Finally, I would like to thank Station Kaffe for their support and the “darn good” coffee 🙂

Advertisements

One thought on “Reactive RESTful service with Spring 5, Spring Boot 2 and MongoDB (part 2)

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s