Curity OIDC client with mutual TLS client authentication

OpenID Connect Client with Mutual TLS Client Authentication

Advanced Security
QualityAvailability
Download on GitHub
On this page

In the preceding tutorial you could learn how to configure an OIDC client with Spring Security. If you missed it, check it out here: OIDC Client with Spring Security

Instead of using a shared secret for authentication, we will show how to setup a client with Spring Security that uses mutual TLS for authentication and as a result retrieves constrained access tokens.

For more information on Mutual TLS Client Authentication read up on the topic here: Mutual TLS Client Authentication

Prerequisites

Before you get started with the development we need to prepare our infrastructure. We need the following to be in place before starting development:

  • Private Key and Client Certificate
  • Have the Curity Identity Server support Mutual TLS Authentication
  • OAuth Client with Mutual TLS Authentication configured in the Curity Identity Server

Configuration of the Curity Identity Server is out of scope of this tutorial. The easiest way is to download and install the sample configuration from Curity Developer Portal after running the initial setup wizard. This sample configuration already has an authentication profile and an OAuth profile that can be easily updated to work with this tutorial. Take a look at the Token Service Admin Guide for help with enabling support for Mutual TLS Client Authentication.

Make sure you are familiar with the following endpoints of the Curity Identity Server and that Client Authentication is allowed for the oauth-token endpoint:

Endpoint NameSpring Boot ConfigEndpoint TypePath used in the Tutorial
Authorization EndpointauthorizationUrioauth-authorizehttps://idsvr.example.com/oauth/v2/oauth-authorize
Token EndpointtokenUrioauth-tokenhttps://idsvr.example.com/oauth/v2/oauth-token
JSON Web Key SetjwkSetUrioauth-anonymoushttps://idsvr.example.com/oauth/v2/oauth-anonymous/jwks

Create a Client Certificate

First of all we need a private key and client certificate that we can use in our OIDC client for the mutual TLS authentication. For the purpose of this tutorial we use a self-signed certificate. We will store the certificate and its private key in a Java keystore.

shell
1
keytool -genkeypair -alias demo-client -keyalg RSA -keysize 4096 -keystore demo-client.p12 -storepass Secr3t -storetype pkcs12 -validity 10 -dname "CN=demo-client, OU=Example, O=Curity AB, C=SE"

Let's take a brief look at the command. We created a Java keystore file called demo-client.p12 that is protected with the password Secr3t. The keystore contains an RSA key that was used to create a self-signed certificate with a validity of 10 days and the subject CN=demo-client, OU=Example, O=Curity AB, C=SE. The certificate and the key are grouped by the alias demo-client.

Place the keystore file in the resources folder of your application. This is required when configuring the application.

Now export the certificate. You need that file for the client configuration in the Curity Identity Server.

shell
1
keytool -export -alias demo-client -keystore demo-client.p12 -storepass Secr3t -file demo-client.cer

Remember the following details for later:

Parameter NameParameter Value
Keystore filedemo-client.p12
Keystore passwordSecr3t
Client certificatedemo-client.cer

Configure the Demo Client

Make sure you configure a client in the Curity Identity Server before getting started. You must be familiar with the following details:

  • client id
  • client certificate
  • scopes
  • authorization grant type (capability)
  • redirect uri

We assume that the application will be deployed locally which is reflected in the redirect uri. We will use the following values:

Parameter NameValue in tutorial
Client IDdemo-client
Client Authenticationmutual-tls
Pinned Client Certificatedemo-client.cer
Scopesopenid
CapabilityCode Flow
Redirect Urihttps://localhost:9443/login/oauth2/code/idsvr

If you are using the sample configuration from Curity Developer Portal, you can simply duplicate the client for web-based applications called www that comes with the sample configuration instead of creating a new client from scratch. If you cannot use the sample configuration for some reason follow the code flow tutorial to create a client that supports code flow.

For a complete reference on configuring Mutual TLS Client Authentication check out the corresponding chapter in the Token Service Admin Guide. Choose Trust by pinned Certificate and the demo-client.cer from above.

Prepare your Project

Use the Spring Initializr to create a simple Spring Boot application from scratch. It is a website that assists you to create a new Spring Boot application with the necessary dependencies.

Open start.spring.io in your browser to access Spring Initializr.

In the configuration window that opens, enter io.curity.example for the name of the group and call the artifact demo-client.

Search for and add the following dependencies:

  • Spring Security
  • OAuth2 Client
  • Spring Reactive Web
  • Thymeleaf
Spring Initializr

Generate the application. Spring Initializr creates an archive with a bootstrap application that includes the selected dependencies. Download and extract the archive, and import the project in an IDE of your choice.

Setup HTTPS

It is good practice to secure web applications with HTTPS. Therefore we explain quickly how you can configure SSL for this Spring Boot application.

Run the following command to create a self-signed certificate for localhost:

shell
1
keytool -genkeypair -alias https -keyalg RSA -keysize 4096 -keystore server.p12 -storepass Secr3t -storetype pkcs12 -validity 10 -dname "CN=localhost, OU=Example, O=Curity AB, C=SE"

Copy the file server.p12 into src/main/resources. Rename application.properties in the same folder to application.yml and configure HTTPS for the application by adding the following fragment to the file:

yaml
1234567
server:
port: 9443
ssl:
key-store: classpath:server.p12
key-store-password: Secr3t
key-store-type: pkcs12
key-store-alias: https

The application will now run on https://localhost:9443.

Insecure Certificate

The browser will not trust this self-signed server certificate. You may notice an SSLHandshakeException in the console when running this example. Make sure your browser trusts the certificate if you want to get rid of the error.

Add a Controller

We will add a controller to demonstrate how to access tokens by returning some data of the access and ID token. Use the @RegisteredOAuth2AuthorizedClient annotation to get the client and the access token it obtained. Use @AuthenticationPrincipal for accessing information about the user. When using OpenID Connect, choose OidcUser for the class representing the authenticated principal. This class holds the ID token and additional user information if available.

The controller updates the model and returns to the template called index.

java
1234567891011121314151617
@Controller
public class OidcLoginController {
@GetMapping("/")
public String index(Model model,
@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,
@AuthenticationPrincipal OidcUser oidcUser) {
model.addAttribute("userName",
authorizedClient.getPrincipalName());
model.addAttribute("userClaims",
oidcUser.getClaims());
model.addAttribute("clientName",
authorizedClient.getClientRegistration().getClientName());
model.addAttribute("scopes",
authorizedClient.getAccessToken().getScopes());
return "index";
}
}

Access Token is for Resource Server

The access token is supposed to be opaque to the client and is intended for the Resource Server. In this example we just list the scopes to visualize that the client received an access token. Read up on scopes and claims in our series of articles starting with Introduction to Claims

Configure the OAuth Client

Register the following client in the application configuration file src/main/resources/application.yml:

yaml
123456789101112131415161718192021222324252627
spring:
security:
oauth2:
client:
registration:
idsvr:
client-name: Demo
client-id: demo-client
client-authentication-method: none
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope: openid
provider:
idsvr:
authorizationUri: https://idsvr.example.com/oauth/v2/oauth-authorize
tokenUri: https://idsvr.example.com/oauth/v2/oauth-token
jwkSetUri: https://idsvr.example.com/oauth/v2/oauth-anonymous/jwks
custom:
client:
ssl:
key-store: demo-client.p12
key-store-password: Secr3t
key-store-type: pkcs12
trust-store: idsvr.p12
trust-store-password: changeit
trust-store-type: pkcs12

This triggers String Boot to register a client. The client registration gets the id idsvr that is also part of the (default) redirect-uri.

The remaining properties, client-id, authorization-grant-type and scope, have been defined when configuring the client in the Curity Identity Server (see Prerequisites). You can choose any client-name. This is the string that will be used in the default login page setup at /login.

Spring Boot will use the authorization and token endpoints for the OAuth 2.0 flow. The JSON Web Key Set is used to validate the ID Token.

Public Client

Since we do not want Spring to send any client secret we specify none as the client authentication method although it technically means that we configure the client as a public client which is not true. Spring Security does not support mutual TLS for client authentication out of the box (see Issue #4498)

When obtaining a token - either as part of the code flow or when using refresh tokens - the client must authenticate with a client certificate. For the TLS connection to work it also must trust the certificate presented by the server. We will use the custom.client.ssl settings to configure the (mutual) TLS settings for the client. The keystore with the client certificate and the truststore with the server certificate will be fetched from the resources folder in this example.

Configure Security

Let's configure mutual TLS for the OAuth 2.0 client authentication. We want Spring Security to use our client certificate in a mutual TLS connection with the Curity Identity Server when requesting the access token. For that we will have to load the client keystore that we prepared, use that keystore in an SSL context and apply that context to an HTTP client that we tell the framework to use when requesting the access token.

Load Client Certificate

  • Place the client keystore in the resources folder of your application.
  • Create a configuration-class and call it TrustStoreConfig. This class will be responsible for reading the custom.client.ssl properties from application.yml.
  • Load the key material.
  • Prepare SSL/TLS context that can be used by the web clients when sending requests to the Curity Identity Server.
java
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
@Configuration
public class TrustStoreConfig {
private SslContextBuilder mutualTLSContextBuilder;
[...]
public TrustStoreConfig(
@Value("${custom.client.ssl.trust-store-type:jks}")
String trustStoreType,
@Value("${custom.client.ssl.trust-store:}")
String trustStorePath,
@Value("${custom.client.ssl.trust-store-password:}")
String trustStorePassword,
@Value("${custom.client.ssl.key-store}")
String keyStorePath,
@Value("${custom.client.ssl.key-store-password}")
String keyStorePassword,
@Value("${custom.client.ssl.key-store-type:jks}")
String keyStoreType)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException {
TrustManagerFactory trustManagerFactory = trustManagerFactory(
trustStoreType, trustStorePath, trustStorePassword);
mutualTLSContextBuilder = SslContextBuilder
.forClient()
.keyManager(
keyManagerFactory(keyStoreType, keyStorePath, keyStorePassword)
);
if (trustManagerFactory != null) {
mutualTLSContextBuilder.trustManager(trustManagerFactory);
}
}
private KeyManagerFactory keyManagerFactory(
String keyStoreType, String keyStorePath, String keyStorePassword)
throws KeyStoreException, NoSuchAlgorithmException, IOException, CertificateException, UnrecoverableKeyException {
KeyStore clientKeyStore = KeyStore.getInstance(keyStoreType);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
try (InputStream ksFileInputStream = new ClassPathResource(keyStorePath).getInputStream()) {
clientKeyStore.load(ksFileInputStream, keyStorePassword.toCharArray());
keyManagerFactory.init(clientKeyStore, keyStorePassword.toCharArray());
}
return keyManagerFactory;
}
}

Trust Store

The trust store can be loaded accordingly but was not outlined here for readability.

Configure OAuth 2.0 with MTLS

  • Create a configuration-class and call it SecurityConfig.java.

This class makes use of the TrustStoreConfig to get the SslContext for the web clients used in the different parts of the OAuth 2.0 flow. It will also enable OAuth 2.0 login.

java
12345678910111213141516171819202122232425262728
@Configuration
@Import(TrustStoreConfig.class)
public class SecurityConfig {
/**
* Configuration of a custom trust store.
*/
private final TrustStoreConfig customTrustStoreConfig;
/**
* Load the configuration of the custom key and trust store.
* @param trustStoreConfig
*/
public SecurityConfig(final TrustStoreConfig trustStoreConfig) {
this.customTrustStoreConfig = trustStoreConfig;
}
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges ->
exchanges
.anyExchange().authenticated()
)
.oauth2Login(Customizer.withDefaults());
return http.build();
}
}

Spring reactive WebClient abstracts the reactive http client it uses for the requests. By default, it uses Reactor Netty HttpClient internally (see Spring Docs). Therefore we also build our solution on the same.

  • Create a secure HttpClient
  • Use the Reactor Netty implementation of ClientHttpConnector with the preconfigured HttpClient
  • Build a WebClient with the ReactorClientHttpConnector
java
123456789101112131415
private WebClient createWebClient(SslContext sslContext) {
HttpClient nettyClient = HttpClient
.create(ConnectionProvider.create("small-test-pool", 3))
.wiretap(true)
.secure(sslContextSpec -> sslContextSpec.sslContext(sslContext)
.handshakeTimeout(Duration.of(2, ChronoUnit.SECONDS)));
ClientHttpConnector clientConnector = new ReactorClientHttpConnector(nettyClient);
return WebClient
.builder()
.clientConnector(clientConnector)
.build();
}

We are now ready to create a token response client that can handle the code flow of our application with mutual TLS client authentication.

java
123456789101112
@Bean
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> reactiveOAuth2AccessTokenResponseClientWithMtls() throws SSLException {
SslContext sslContext = customTrustStoreConfig.createMutualTlsContext();
WebClientReactiveAuthorizationCodeTokenResponseClient mtlsClient = new
WebClientReactiveAuthorizationCodeTokenResponseClient();
WebClient mtlsWebClient = createWebClient(sslContext);
mtlsClient.setWebClient(mtlsWebClient);
return mtlsClient;
}

Take into account that there is another implementation for when refresh tokens are used to obtain a new access token. Update ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accordingly to the example above. Also, Spring Security will fetch the keys from Curity Identity Server to verify the tokens from the jwkSetUri. The web client retrieving those keys must trust the server certificate.

Run the Demo Application

Update application.yml to fit your infrastructure.

Start the demo application with mvn spring-boot:run if you use maven or ./gradlew bootRun for a gradle project.

Navigate to https://localhost:9443 to access the index site and trigger a login. Login

After successful login you will be presented with details retrieved from the ID token. User

You can also navigate to https://localhost:9443/login to access the default login page created by Spring Security.

Conclusion

Since Spring Security does not support mutual TLS for client authentication you have to be aware of the details of the OAuth 2.0 flow that your application is going to use such as the code flow and refresh tokens. Each web client that handles requests to the Curity Identity Server that require authentication must be updated with the mutual TLS context.

Due to the lack of customizable web clients, the configuration of a custom trust store gets cumbersome because every possible request to the server must be considered and updated which requires knowledge of Spring Security's implementation details. A simple workaround are JVM arguments.

The framework is under constant development and some of the drawbacks mentioned above are registered and may be solved in future versions:

Further Information and Source Code

You can find the source code of the example on GitHub.

For further examples and help regarding OAuth2 and Spring Security visit Spring Security Reference.

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial