OpenID Connect Client with Mutual TLS Client Authentication
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 Name | Spring Boot Config | Endpoint Type | Path used in the Tutorial |
---|---|---|---|
Authorization Endpoint | authorizationUri | oauth-authorize | https://idsvr.example.com/oauth/v2/oauth-authorize |
Token Endpoint | tokenUri | oauth-token | https://idsvr.example.com/oauth/v2/oauth-token |
JSON Web Key Set | jwkSetUri | oauth-anonymous | https://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.
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.
keytool -export -alias demo-client -keystore demo-client.p12 -storepass Secr3t -file demo-client.cer
Remember the following details for later:
Parameter Name | Parameter Value |
---|---|
Keystore file | demo-client.p12 |
Keystore password | Secr3t |
Client certificate | demo-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 Name | Value in tutorial |
---|---|
Client ID | demo-client |
Client Authentication | mutual-tls |
Pinned Client Certificate | demo-client.cer |
Scopes | openid |
Capability | Code Flow |
Redirect Uri | https://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
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
:
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:
server:port: 9443ssl:key-store: classpath:server.p12key-store-password: Secr3tkey-store-type: pkcs12key-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
.
@Controllerpublic 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
:
spring:security:oauth2:client:registration:idsvr:client-name: Democlient-id: demo-clientclient-authentication-method: noneauthorization-grant-type: authorization_coderedirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"scope: openidprovider:idsvr:authorizationUri: https://idsvr.example.com/oauth/v2/oauth-authorizetokenUri: https://idsvr.example.com/oauth/v2/oauth-tokenjwkSetUri: https://idsvr.example.com/oauth/v2/oauth-anonymous/jwkscustom:client:ssl:key-store: demo-client.p12key-store-password: Secr3tkey-store-type: pkcs12trust-store: idsvr.p12trust-store-password: changeittrust-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 thecustom.client.ssl
properties fromapplication.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.
@Configurationpublic 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.
@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;}@BeanSecurityWebFilterChain 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 preconfiguredHttpClient
- Build a
WebClient
with theReactorClientHttpConnector
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.
@BeanReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> reactiveOAuth2AccessTokenResponseClientWithMtls() throws SSLException {SslContext sslContext = customTrustStoreConfig.createMutualTlsContext();WebClientReactiveAuthorizationCodeTokenResponseClient mtlsClient = newWebClientReactiveAuthorizationCodeTokenResponseClient();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.
After successful login you will be presented with details retrieved from the ID token.
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:
- Issue #4498 - Support client authentication using X.509 certificate
- Issue #8365 - Allow customization of restOperations for JwtDecoder built using an issuer uri
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