X.509 Certificate Authentication

What Is X.509 Authentication?

X.509 authentication (also called mutual TLS or mTLS) uses digital certificates instead of passwords. The client presents a certificate during the TLS handshake. The server verifies the certificate against a trusted Certificate Authority (CA) and extracts the user identity from the certificate’s Common Name (CN) or Subject Alternative Name (SAN).

sequenceDiagram
    participant Client as Client (with certificate)
    participant Server as Spring Boot Server

    Client->>Server: TLS ClientHello
    Server->>Client: TLS ServerHello + Server Certificate
    Server->>Client: CertificateRequest (ask for client cert)
    Client->>Server: Client Certificate + CertificateVerify
    Server->>Server: Verify against trusted CA
    Server->>Server: Extract CN from certificate: "alice"
    Server->>Server: loadUserByUsername("alice")
    Server-->>Client: TLS established + authenticated
    Client->>Server: GET /api/data (no password needed)
    Server-->>Client: 200 OK — alice is authenticated

Use Cases

Use caseWhy X.509
Service-to-service (microservices)No passwords to rotate, mutual verification
IoT device authenticationDevices can’t type passwords; certs are pre-provisioned
Enterprise client authenticationSmart cards, corporate device certificates
High-security API clientsStronger than passwords or API keys
Internal admin toolsCertificate = proof of corporate device ownership

Spring Security Configuration

@Configuration
@EnableWebSecurity
public class X509SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .x509(x509 -> x509
                .subjectPrincipalRegex("CN=(.*?)(?:,|$)")  // extract CN from Subject
                .userDetailsService(x509UserDetailsService())
            );
        return http.build();
    }

    @Bean
    public UserDetailsService x509UserDetailsService() {
        // Load user from database using the CN extracted from the certificate
        return username -> userRepository.findByCertificateCn(username)
            .map(AppUserDetails::new)
            .orElseThrow(() -> new UsernameNotFoundException(
                "No user found for certificate CN: " + username
            ));
    }
}

The subjectPrincipalRegex extracts the identity from the certificate’s Subject field. A certificate Subject looks like:

CN=alice, OU=Engineering, O=Example Corp, C=US

The regex CN=(.*?)(?:,|$) extracts alice.


Server-Side TLS Configuration

Configure Spring Boot to require client certificates:

# application.yml
server:
  ssl:
    enabled: true
    key-store: classpath:server-keystore.p12
    key-store-password: serverpass
    key-store-type: PKCS12
    # Trust store — CA certificates that signed client certs
    trust-store: classpath:ca-truststore.p12
    trust-store-password: trustpass
    trust-store-type: PKCS12
    # WANT: request cert but don't require it (check in Spring Security)
    # NEED: require cert at TLS level (rejects before Spring Security)
    client-auth: want  # or "need" for strict mutual TLS

client-auth: want lets Spring Security handle the logic — useful when you want to fall back to another auth method if no certificate is provided.

client-auth: need rejects the TLS handshake if no valid client certificate is presented — the request never reaches Spring Security.


Generating Certificates for Development

# 1. Create a CA key and certificate
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
    -subj "/C=US/O=Example Corp/CN=Example CA"

# 2. Create server key and CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
    -subj "/C=US/O=Example Corp/CN=localhost"

# 3. Sign server certificate with CA
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key \
    -CAcreateserial -out server.crt

# 4. Create client key and CSR
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr \
    -subj "/C=US/O=Example Corp/CN=alice"  # CN=alice is the username

# 5. Sign client certificate with CA
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key \
    -CAcreateserial -out client.crt

# 6. Create server keystore (PKCS12)
openssl pkcs12 -export -out server-keystore.p12 \
    -inkey server.key -in server.crt -certfile ca.crt \
    -passout pass:serverpass

# 7. Create CA truststore (for verifying client certs)
keytool -importcert -file ca.crt -alias ca \
    -keystore ca-truststore.p12 -storepass trustpass -storetype PKCS12 -noprompt

# 8. Create client keystore (for the client to present)
openssl pkcs12 -export -out client-keystore.p12 \
    -inkey client.key -in client.crt -certfile ca.crt \
    -passout pass:clientpass

Custom Principal Extractor

For more complex certificate parsing (extracting email, employee ID, or SAN):

@Component
public class CertificatePrincipalExtractor implements X509PrincipalExtractor {

    @Override
    public Object extractPrincipal(X509Certificate cert) {
        // Option 1: extract from Subject CN
        String subject = cert.getSubjectX500Principal().getName();
        return extractCn(subject);

        // Option 2: extract email from Subject Alternative Names
        // try {
        //     Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
        //     return altNames.stream()
        //         .filter(san -> (Integer) san.get(0) == 1) // type 1 = email
        //         .map(san -> (String) san.get(1))
        //         .findFirst()
        //         .orElse(extractCn(subject));
        // } catch (CertificateParsingException e) {
        //     return extractCn(subject);
        // }
    }

    private String extractCn(String subject) {
        // Parse "CN=alice, OU=Engineering, ..." → "alice"
        LdapName ln = new LdapName(subject);
        return ln.getRdns().stream()
            .filter(rdn -> rdn.getType().equalsIgnoreCase("CN"))
            .map(rdn -> rdn.getValue().toString())
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("No CN in certificate: " + subject));
    }
}
http.x509(x509 -> x509
    .x509PrincipalExtractor(certificatePrincipalExtractor)
    .userDetailsService(x509UserDetailsService())
);

Combining X.509 with JWT (Service-to-Service)

A common microservice pattern: service clients use certificates for mutual authentication, and the gateway/auth service issues short-lived JWTs:

flowchart LR
    Client[Service A\nwith client cert] -->|mTLS| GW[API Gateway\nverify cert → issue JWT]
    GW -->|JWT| SB[Service B\nverify JWT]
    GW -->|JWT| SC[Service C\nverify JWT]
@Configuration
public class ServiceSecurityConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain serviceChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/internal/**")
            .x509(x509 -> x509
                .subjectPrincipalRegex("CN=(.*?)(?:,|$)")
                .userDetailsService(serviceAccountDetailsService())
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/internal/**").hasRole("SERVICE")
            )
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}

Testing X.509 Authentication

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class X509AuthTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void withValidCertificate_shouldAuthenticate() throws Exception {
        // Load client keystore
        KeyStore clientKs = KeyStore.getInstance("PKCS12");
        try (InputStream ks = new ClassPathResource("client-keystore.p12").getInputStream()) {
            clientKs.load(ks, "clientpass".toCharArray());
        }

        // Create SSL context with client cert
        SSLContext sslContext = SSLContextBuilder.create()
            .loadKeyMaterial(clientKs, "clientpass".toCharArray())
            .loadTrustMaterial(trustStore, null)
            .build();

        CloseableHttpClient httpClient = HttpClients.custom()
            .setSSLContext(sslContext)
            .build();

        ResponseEntity<String> response = new RestTemplate(
            new HttpComponentsClientHttpRequestFactory(httpClient)
        ).getForEntity("https://localhost:" + port + "/api/data", String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }

    @Test
    void withNoCertificate_shouldReturn401() {
        ResponseEntity<String> response =
            restTemplate.getForEntity("/api/data", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
    }
}

Summary

  • X.509 authentication uses client TLS certificates — no passwords, strong identity.
  • Configure server.ssl.client-auth: want/need in application.yml to enable client certificate requests.
  • Use http.x509() in Spring Security to extract the principal from the certificate CN and load UserDetails.
  • Use want for flexible auth (fall back to other mechanisms); use need for strict mTLS.
  • Generate development certificates with OpenSSL; use a proper CA (e.g., HashiCorp Vault PKI) in production.
  • Best use cases: microservice mTLS, IoT devices, enterprise client authentication.

Next: Article 12 covers OAuth2 fundamentals — grant types, flows, and the terminology you need before implementing OAuth2 login or resource server.