Reference
- https://docs.spring.io/spring-security/reference/servlet/authentication/x509.html
- https://magnus-k-karlsson.blogspot.com/2023/08/using-java-17-keytool-as-root-ca-to.html
- https://magnus-k-karlsson.blogspot.com/2023/08/spring-boot-3-with-basic-authentication.html
Maven
Using same pom, but different artifactId as previous blog: Basic Auth and JDBC Password Storage
MySQL
Using same docker mysql container wiht same data as in previous blog: Basic Auth and JDBC Password Storage
Server and User Certificate and Truststore
Using same root ca, certs and truststore as in previous blog: Using Java 17 keytool as Root CA to create Server and User Certificate
Applicaiton
src/main/resources/application.properties
server.port = 8443
# https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-logging
logging.level.org.springframework.security = TRACE
# https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#appendix.application-properties.server
#server.ssl.bundle
#server.ssl.certificate
#server.ssl.certificate-private-key
#server.ssl.ciphers
server.ssl.client-auth = want
server.ssl.enabled = true
#server.ssl.enabled-protocols
server.ssl.key-alias = localhost
server.ssl.key-password = changeit
server.ssl.key-store = localhost.p12
server.ssl.key-store-password = changeit
#server.ssl.key-store-provider
server.ssl.key-store-type = PKCS12
#server.ssl.protocol
#server.ssl.trust-certificate
#server.ssl.trust-certificate-private-key =
server.ssl.trust-store = truststore.jks
server.ssl.trust-store-password = changeit
#server.ssl.trust-store-provider
server.ssl.trust-store-type = JKS
#server.servlet.session.cookie.domain
server.servlet.session.cookie.http-only = true
server.servlet.session.cookie.max-age = 3m
#server.servlet.session.cookie.name
#server.servlet.session.cookie.path
server.servlet.session.cookie.same-site = strict
server.servlet.session.cookie.secure = true
server.servlet.session.persistent = false
#server.servlet.session.store-dir
server.servlet.session.timeout = 3m
server.servlet.session.tracking-modes = cookie
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=user
spring.datasource.password=changeit
Spring Java configuration
package se.mkk.springboot3x509jdbc;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class X509JdbcSecurityConfig {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http //
.sessionManagement(
// https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
// https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#appendix.application-properties.server
Customizer.withDefaults())
.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests //
.requestMatchers("/login", "/logout").permitAll() //
.anyRequest().authenticated()) //
.x509(x509 -> x509 //
// https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/basic.html
.subjectPrincipalRegex("CN=(.*?),") //
.userDetailsService(this.userDetailsService())) //
.csrf(csrf -> csrf //
// https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-token-repository-cookie
.ignoringRequestMatchers("/logout", "/api"))
.logout(logout -> logout //
// https://docs.spring.io/spring-security/reference/servlet/authentication/logout.html#clear-all-site-data
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.ALL))));
return http.build();
}
// private UserDetailsService userDetailsService() {
// return username -> {
// log.info("X509 {}", username);
// return User.withUsername(username).password("DUMMY").roles("USER", "ADMIN").build();
// };
// }
// https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/jdbc.html
@Autowired
private DataSource dataSource;
private UserDetailsService userDetailsService() {
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
return users;
}
}
Test REST endpoint
package se.mkk.springboot3x509jdbc;
import java.security.Principal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:4200")
public class UserController {
@GetMapping
public Map<String, String> getUser(HttpServletRequest request, HttpSession session, Principal principal) {
Map<String, String> rtn = new LinkedHashMap<>();
rtn.put("session_getMaxInactiveInterval_sec", session.getMaxInactiveInterval() + "s");
rtn.put("session_getLastAccessedTime",
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").format(new Date(session.getLastAccessedTime())));
rtn.put("request_getRemoteUser", request.getRemoteUser());
rtn.put("request_isUserInRole_USER", Boolean.toString(request.isUserInRole("USER")));
rtn.put("request_getUserPrincipal_getClass", request.getUserPrincipal().getClass().getName());
rtn.put("principal_getClass_getName", principal.getClass().getName());
rtn.put("principal_getName", principal.getName());
if (principal instanceof Authentication authentication) {
List<String> authorities = authentication.getAuthorities().stream()
.map(grantedAuthority -> grantedAuthority.getAuthority()).toList();
rtn.put("JwtAuthenticationToken.getAuthorities()", authorities.toString());
}
return rtn;
}
}
Test
We need to convert p12 to pem files for curl.
$ openssl pkcs12 -in john.p12 -out john.pem -nodes
$ curl -v -X GET --cacert RootCA.cert.pem --cert john.cert.pem --key john.key.pem https://localhost:8443/api/users
Note: Unnecessary use of -X or --request, GET is already inferred.
* Trying 127.0.0.1:8443...
* Connected to localhost (127.0.0.1) port 8443 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
* CAfile: RootCA.cert.pem
* CApath: none
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
* subject: C=SE; O=Server; OU=Localhost_Server; CN=localhost
* start date: Aug 30 09:35:00 2023 GMT
* expire date: Nov 28 09:35:00 2023 GMT
* subjectAltName: host "localhost" matched cert's "localhost"
* issuer: C=SE; O=CertificateAuthority; OU=Root_CertificateAuthority; CN=RootCA
* SSL certificate verify ok.
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET /api/users HTTP/1.1
> Host: localhost:8443
> User-Agent: curl/7.85.0
> Accept: */*
>
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Set-Cookie: JSESSIONID=5358CF4B62B84A8E99C4DB183CB2BDD2; Max-Age=180; Expires=Wed, 30 Aug 2023 10:07:14 GMT; Path=/; Secure; HttpOnly; SameSite=Strict
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 0
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< Strict-Transport-Security: max-age=31536000 ; includeSubDomains
< X-Frame-Options: DENY
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Wed, 30 Aug 2023 10:04:14 GMT
<
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection #0 to host localhost left intact
{"session_getMaxInactiveInterval_sec":"180s","session_getLastAccessedTime":"2023-08-30 12:04:14 +0200","request_getRemoteUser":"john","request_isUserInRole_USER":"true","request_getUserPrincipal_getClass":"org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken","principal_getClass_getName":"org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken","principal_getName":"john","JwtAuthenticationToken.getAuthorities()":"[ROLE_ADMIN, ROLE_USER]"}
$ curl -s -X GET --cacert RootCA.cert.pem --cert john.cert.pem --key john.key.pem https://localhost:8443/api/users | jq -r
{
"session_getMaxInactiveInterval_sec": "180s",
"session_getLastAccessedTime": "2023-08-30 12:04:44 +0200",
"request_getRemoteUser": "john",
"request_isUserInRole_USER": "true",
"request_getUserPrincipal_getClass": "org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken",
"principal_getClass_getName": "org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken",
"principal_getName": "john",
"JwtAuthenticationToken.getAuthorities()": "[ROLE_ADMIN, ROLE_USER]"
}