Beyond Bearer Tokens: Implementing Mutual TLS for Strong Authentication in Java

In the landscape of API security, bearer tokens (JWT, OAuth2) have become the dominant authentication mechanism. However, they suffer from a fundamental weakness: they rely on a single secret that, if leaked, grants complete access. Mutual TLS (mTLS) provides a powerful complementary security layer by authenticating both the client and server using X.509 certificates. When combined with tokens, mTLS creates a robust, multi-factor authentication system that significantly raises the bar for attackers.

What is Mutual TLS?

Mutual TLS extends the standard TLS handshake by requiring the client to present a valid certificate to the server. While standard TLS only authenticates the server to the client, mTLS provides:

  • Server Authentication: Server presents its certificate to the client
  • Client Authentication: Client presents its certificate to the server
  • Certificate-Based Identity: Both parties are identified by cryptographically signed certificates
  • Channel Security: All traffic is encrypted with session keys derived from the handshake

Why Combine mTLS with Tokens?

AspectBearer Tokens AlonemTLS + Tokens
AuthenticationSingle factor (token)Two-factor (cert + token)
Transport SecurityTLS (optional)Required mTLS
Client IdentityToken claimsCertificate + claims
Replay ProtectionToken expirationSession-bound
Key MaterialShared secretPrivate key per client

Key Benefits:

  1. Defense in Depth: Compromising a token is insufficient without the corresponding client certificate
  2. Strong Client Identity: Certificates provide non-repudiable client authentication
  3. Channel Binding: Tokens are bound to the TLS session, preventing token export attacks
  4. Automated Rotation: Certificates can be rotated without changing tokens
  5. Regulatory Compliance: Meets strict authentication requirements (PCI-DSS, HIPAA)

Implementing mTLS in Java

1. Server-Side mTLS Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
@Configuration
@EnableWebSecurity
public class MTlsSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// Enable mTLS (X.509 client certificate authentication)
.x509()
.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(mTlsUserDetailsService())
.and()
// Require authentication for all endpoints
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
.and()
// Disable default form/login
.formLogin().disable()
.httpBasic().disable()
// Session management
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public UserDetailsService mTlsUserDetailsService() {
return username -> {
// Load user details based on certificate CN
UserDetails user = User.withUsername(username)
.password("") // No password needed
.roles("USER")
.build();
if (user == null) {
throw new UsernameNotFoundException("User not found: " + username);
}
return user;
};
}
@Bean
public X509AuthenticationFilter x509AuthenticationFilter() {
X509AuthenticationFilter filter = new X509AuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
return filter;
}
}

2. Tomcat mTLS Connector Configuration

import org.apache.catalina.connector.Connector;
import org.apache.coyote.http11.Http11NioProtocol;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MTlsTomcatConfig {
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
tomcat.addAdditionalTomcatConnectors(createMtlsConnector());
return tomcat;
}
private Connector createMtlsConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
connector.setPort(8443);
connector.setScheme("https");
connector.setSecure(true);
// Server certificate configuration
protocol.setSSLEnabled(true);
protocol.setKeystoreFile("/path/to/server-keystore.jks");
protocol.setKeystorePass("server-keystore-password");
protocol.setKeyAlias("server");
// Client certificate configuration (mTLS)
protocol.setTruststoreFile("/path/to/truststore.jks");
protocol.setTruststorePass("truststore-password");
protocol.setClientAuth("true"); // Require client certificate
protocol.setSslProtocol("TLSv1.3");
protocol.setCiphers("TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256");
// Certificate revocation checking
protocol.setCrlFile("/path/to/crl.pem");
protocol.setClientCertProvider("SunJSSE");
return connector;
}
}

3. Spring Boot mTLS Configuration (application.yml)

server:
port: 8443
ssl:
# Server certificate
key-store: classpath:server-keystore.p12
key-store-password: ${SERVER_KEYSTORE_PASSWORD}
key-store-type: PKCS12
key-alias: server
# Client certificate validation (mTLS)
client-auth: need  # 'need' for required, 'want' for optional
trust-store: classpath:truststore.p12
trust-store-password: ${TRUSTSTORE_PASSWORD}
trust-store-type: PKCS12
# Protocol and cipher configuration
enabled-protocols: TLSv1.3
ciphers: TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256
protocol: TLS
# Certificate revocation
crl-path: classpath:crl.pem

4. Extracting Certificate Information in Controllers

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.web.authentication.preauth.x509.X509Principal;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/secure")
public class MtlsController {
/**
* Access certificate information from request
*/
@GetMapping("/cert-info")
public Map<String, Object> getCertificateInfo(HttpServletRequest request) {
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(
"javax.servlet.request.X509Certificate");
Map<String, Object> certInfo = new HashMap<>();
if (certs != null && certs.length > 0) {
X509Certificate clientCert = certs[0];
certInfo.put("subjectDN", clientCert.getSubjectDN().toString());
certInfo.put("issuerDN", clientCert.getIssuerDN().toString());
certInfo.put("serialNumber", clientCert.getSerialNumber().toString());
certInfo.put("notBefore", clientCert.getNotBefore());
certInfo.put("notAfter", clientCert.getNotAfter());
certInfo.put("version", clientCert.getVersion());
certInfo.put("sigAlgName", clientCert.getSigAlgName());
// Extract SAN (Subject Alternative Names)
Collection<List<?>> san = clientCert.getSubjectAlternativeNames();
if (san != null) {
certInfo.put("subjectAltNames", san);
}
}
return certInfo;
}
/**
* Get authenticated principal from certificate
*/
@GetMapping("/principal")
public String getPrincipal(@AuthenticationPrincipal X509Principal principal) {
return "Authenticated client: " + principal.getName();
}
/**
* Example of combining mTLS with token validation
*/
@PostMapping("/mtls-token")
public ResponseEntity<?> secureEndpoint(
HttpServletRequest request,
@RequestHeader("Authorization") String authHeader) {
// Validate client certificate
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(
"javax.servlet.request.X509Certificate");
if (certs == null || certs.length == 0) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Client certificate required");
}
// Extract certificate details
X509Certificate clientCert = certs[0];
String clientId = extractClientIdFromCert(clientCert);
// Validate token
String token = authHeader.replace("Bearer ", "");
if (!validateToken(token, clientId)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Invalid token for this client");
}
// Process request
return ResponseEntity.ok(Map.of(
"message", "Successfully authenticated with mTLS + token",
"clientId", clientId
));
}
private String extractClientIdFromCert(X509Certificate cert) {
// Extract CN from subject DN
String subjectDN = cert.getSubjectDN().getName();
String[] parts = subjectDN.split(",");
for (String part : parts) {
if (part.trim().startsWith("CN=")) {
return part.trim().substring(3);
}
}
return null;
}
private boolean validateToken(String token, String clientId) {
// Implement token validation with client binding
// Token should be bound to this specific client
try {
Jwt jwt = Jwts.parserBuilder()
.setSigningKey(jwtSecret)
.build()
.parseClaimsJws(token);
String tokenClientId = jwt.getBody().get("clientId", String.class);
return clientId.equals(tokenClientId);
} catch (Exception e) {
return false;
}
}
}

Client-Side mTLS Implementation

5. Java HTTP Client with mTLS

import javax.net.ssl.*;
import java.io.FileInputStream;
import java.security.KeyStore;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
@Service
public class MtlsHttpClient {
private final HttpClient httpClient;
public MtlsHttpClient(
@Value("${client.keystore.path}") String keystorePath,
@Value("${client.keystore.password}") String keystorePassword,
@Value("${client.truststore.path}") String truststorePath,
@Value("${client.truststore.password}") String truststorePassword) 
throws Exception {
// Load client keystore (contains client certificate and private key)
KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream(keystorePath)) {
clientKeyStore.load(fis, keystorePassword.toCharArray());
}
// Load truststore (contains server CA certificates)
KeyStore trustStore = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream(truststorePath)) {
trustStore.load(fis, truststorePassword.toCharArray());
}
// Create key manager for client certificate
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(clientKeyStore, keystorePassword.toCharArray());
// Create trust manager for server certificate validation
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(trustStore);
// Create SSL context
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
// Build HTTP client with mTLS
this.httpClient = HttpClient.newBuilder()
.sslContext(sslContext)
.connectTimeout(Duration.ofSeconds(10))
.version(HttpClient.Version.HTTP_2)
.build();
}
public HttpResponse<String> sendSecureRequest(String url, String token) 
throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + token)
.timeout(Duration.ofSeconds(30))
.GET()
.build();
return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
}
public <T> HttpResponse<T> sendRequestWithBody(
String url, String token, Object body, 
HttpResponse.BodyHandler<T> responseHandler) 
throws Exception {
String jsonBody = new ObjectMapper().writeValueAsString(body);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(30))
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
return httpClient.send(request, responseHandler);
}
}

6. Apache HttpClient with mTLS

import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.springframework.stereotype.Component;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileInputStream;
import java.security.KeyStore;
@Component
public class ApacheMtlsClient {
public CloseableHttpClient createMtlsClient(
String keystorePath, 
String keystorePassword,
String truststorePath,
String truststorePassword) throws Exception {
// Load client keystore
KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream(new File(keystorePath))) {
clientKeyStore.load(fis, keystorePassword.toCharArray());
}
// Load truststore
KeyStore trustStore = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream(new File(truststorePath))) {
trustStore.load(fis, truststorePassword.toCharArray());
}
// Build SSL context with mTLS
SSLContext sslContext = SSLContextBuilder.create()
.loadKeyMaterial(clientKeyStore, keystorePassword.toCharArray())
.loadTrustMaterial(trustStore, null)
.build();
// Create HTTP client with mTLS
return HttpClients.custom()
.setSSLContext(sslContext)
.setConnectionTimeToLive(30, TimeUnit.SECONDS)
.build();
}
}

Certificate Binding for Tokens

7. Certificate-Bound JWT Tokens

@Service
public class CertificateBoundTokenService {
@Value("${app.jwt.secret}")
private String jwtSecret;
/**
* Generate token bound to client certificate
*/
public String generateCertificateBoundToken(
String clientId, 
X509Certificate clientCert) {
// Extract certificate fingerprint
String certFingerprint = calculateFingerprint(clientCert);
// Extract public key hash
String publicKeyHash = calculatePublicKeyHash(clientCert);
Date now = new Date();
Date expiryDate = new Date(now.getTime() + 3600000); // 1 hour
return Jwts.builder()
.setSubject(clientId)
.claim("cert_fingerprint", certFingerprint)
.claim("public_key_hash", publicKeyHash)
.claim("token_type", "certificate-bound")
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
/**
* Validate certificate-bound token
*/
public boolean validateCertificateBoundToken(
String token, 
X509Certificate clientCert) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(jwtSecret)
.build()
.parseClaimsJws(token)
.getBody();
// Verify certificate binding
String tokenFingerprint = claims.get("cert_fingerprint", String.class);
String actualFingerprint = calculateFingerprint(clientCert);
if (!tokenFingerprint.equals(actualFingerprint)) {
return false; // Token bound to different certificate
}
// Check token type
if (!"certificate-bound".equals(claims.get("token_type"))) {
return false;
}
return true;
} catch (Exception e) {
return false;
}
}
private String calculateFingerprint(X509Certificate cert) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] fingerprint = md.digest(cert.getEncoded());
return Base64.getEncoder().encodeToString(fingerprint);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate certificate fingerprint", e);
}
}
private String calculatePublicKeyHash(X509Certificate cert) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] keyHash = md.digest(cert.getPublicKey().getEncoded());
return Base64.getEncoder().encodeToString(keyHash);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate public key hash", e);
}
}
}

8. Certificate-Bound Token Filter

@Component
public class CertificateBoundTokenFilter extends OncePerRequestFilter {
@Autowired
private CertificateBoundTokenService tokenService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// Extract client certificate
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(
"javax.servlet.request.X509Certificate");
// Extract token
String authHeader = request.getHeader("Authorization");
String token = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
}
// For protected endpoints, require both
if (isProtectedEndpoint(request)) {
if (certs == null || certs.length == 0 || token == null) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Both certificate and token required");
return;
}
// Validate certificate-bound token
if (!tokenService.validateCertificateBoundToken(token, certs[0])) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid certificate-token binding");
return;
}
// Add certificate info to request attributes
request.setAttribute("clientCertificate", certs[0]);
request.setAttribute("clientId", 
extractClientIdFromCert(certs[0]));
}
chain.doFilter(request, response);
}
private boolean isProtectedEndpoint(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/api/secure/") || 
path.startsWith("/api/admin/");
}
}

Certificate Management Service

9. Client Certificate Registry

@Service
public class ClientCertificateRegistry {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* Register a client certificate
*/
public void registerCertificate(
String clientId, 
X509Certificate certificate,
LocalDateTime expiryDate) {
String fingerprint = calculateFingerprint(certificate);
String subjectDN = certificate.getSubjectDN().getName();
String issuerDN = certificate.getIssuerDN().getName();
String serialNumber = certificate.getSerialNumber().toString();
String sql = """
INSERT INTO client_certificates 
(client_id, fingerprint, subject_dn, issuer_dn, 
serial_number, not_before, not_after, status, registered_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 'ACTIVE', NOW())
""";
jdbcTemplate.update(sql,
clientId,
fingerprint,
subjectDN,
issuerDN,
serialNumber,
certificate.getNotBefore().toInstant(),
certificate.getNotAfter().toInstant()
);
}
/**
* Validate certificate against registry
*/
public boolean validateCertificate(X509Certificate certificate) {
String fingerprint = calculateFingerprint(certificate);
String sql = """
SELECT status, not_after, client_id 
FROM client_certificates 
WHERE fingerprint = ?
""";
return jdbcTemplate.query(sql, rs -> {
if (rs.next()) {
String status = rs.getString("status");
Instant notAfter = rs.getTimestamp("not_after").toInstant();
return "ACTIVE".equals(status) && 
notAfter.isAfter(Instant.now());
}
return false;
}, fingerprint);
}
/**
* Revoke certificate
*/
public void revokeCertificate(String fingerprint, String reason) {
String sql = """
UPDATE client_certificates 
SET status = 'REVOKED', 
revocation_reason = ?, 
revoked_at = NOW() 
WHERE fingerprint = ?
""";
jdbcTemplate.update(sql, reason, fingerprint);
}
private String calculateFingerprint(X509Certificate cert) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] fingerprint = md.digest(cert.getEncoded());
return Base64.getEncoder().encodeToString(fingerprint);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate fingerprint", e);
}
}
}

10. Certificate Revocation List (CRL) Checker

@Component
public class CertificateRevocationChecker {
private static final Logger logger = LoggerFactory.getLogger(CertificateRevocationChecker.class);
@Value("${certificate.crl.url}")
private String crlUrl;
private volatile X509CRL currentCrl;
private volatile Instant lastUpdate;
@Scheduled(fixedDelay = 3600000) // Update every hour
public void updateCrl() {
try {
URL url = new URL(crlUrl);
try (InputStream is = url.openStream()) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
currentCrl = (X509CRL) cf.generateCRL(is);
lastUpdate = Instant.now();
logger.info("CRL updated successfully, entries: {}", 
currentCrl.getRevokedCertificates().size());
}
} catch (Exception e) {
logger.error("Failed to update CRL", e);
}
}
public boolean isCertificateRevoked(X509Certificate cert) {
if (currentCrl == null) {
logger.warn("CRL not available, cannot check revocation");
return false; // Fail open or closed based on policy
}
X509CRLEntry entry = currentCrl.getRevokedCertificate(cert);
if (entry != null) {
logger.info("Certificate revoked: {}, reason: {}", 
cert.getSerialNumber(), entry.getRevocationReason());
return true;
}
return false;
}
public CertificateStatus checkCertificateStatus(X509Certificate cert) {
if (isCertificateRevoked(cert)) {
return CertificateStatus.REVOKED;
}
try {
cert.checkValidity();
return CertificateStatus.VALID;
} catch (CertificateExpiredException e) {
return CertificateStatus.EXPIRED;
} catch (CertificateNotYetValidException e) {
return CertificateStatus.NOT_YET_VALID;
}
}
public enum CertificateStatus {
VALID, REVOKED, EXPIRED, NOT_YET_VALID
}
}

OAuth2 with mTLS (RFC 8705)

11. OAuth2 mTLS Token Binding

@Service
public class OAuth2MtlsService {
@Autowired
private ClientCertificateRegistry certRegistry;
/**
* OAuth2 token endpoint with mTLS client authentication
*/
@PostMapping("/oauth/token")
public ResponseEntity<OAuth2Token> getToken(
HttpServletRequest request,
@RequestParam("grant_type") String grantType,
@RequestParam("scope") String scope) {
// Extract client certificate for authentication
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(
"javax.servlet.request.X509Certificate");
if (certs == null || certs.length == 0) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
X509Certificate clientCert = certs[0];
// Validate certificate
if (!certRegistry.validateCertificate(clientCert)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// Extract client ID from certificate
String clientId = extractClientIdFromCert(clientCert);
// Generate certificate-bound access token
String accessToken = generateCertificateBoundToken(clientId, clientCert);
String refreshToken = generateRefreshToken(clientId);
OAuth2Token response = new OAuth2Token(
accessToken,
"bearer",
3600,
refreshToken,
scope
);
return ResponseEntity.ok(response);
}
/**
* Token introspection with mTLS binding validation
*/
@PostMapping("/oauth/introspect")
public ResponseEntity<TokenIntrospection> introspectToken(
HttpServletRequest request,
@RequestParam("token") String token) {
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(
"javax.servlet.request.X509Certificate");
if (certs == null || certs.length == 0) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
X509Certificate clientCert = certs[0];
// Validate token and certificate binding
TokenIntrospection introspection = validateTokenWithCertificate(token, clientCert);
return ResponseEntity.ok(introspection);
}
private String generateCertificateBoundToken(String clientId, X509Certificate cert) {
String certFingerprint = calculateFingerprint(cert);
String publicKeyHash = calculatePublicKeyHash(cert);
return Jwts.builder()
.setSubject(clientId)
.claim("cnf", Map.of(
"x5t#S256", certFingerprint,
"jkt", publicKeyHash
))
.claim("token_type", "access_token")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000))
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
}

Performance Considerations

12. Caching Certificate Validation

@Component
public class CachedCertificateValidator {
private final Cache<String, Boolean> validationCache;
private final CertificateRevocationChecker revocationChecker;
private final ClientCertificateRegistry certificateRegistry;
public CachedCertificateValidator(
CertificateRevocationChecker revocationChecker,
ClientCertificateRegistry certificateRegistry) {
this.validationCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
this.revocationChecker = revocationChecker;
this.certificateRegistry = certificateRegistry;
}
public boolean validateCertificate(X509Certificate cert) {
String fingerprint = calculateFingerprint(cert);
return validationCache.get(fingerprint, key -> {
// Expensive validation logic
if (revocationChecker.isCertificateRevoked(cert)) {
return false;
}
if (!certificateRegistry.validateCertificate(cert)) {
return false;
}
try {
cert.checkValidity();
return true;
} catch (CertificateException e) {
return false;
}
});
}
public void invalidateCertificate(String fingerprint) {
validationCache.invalidate(fingerprint);
}
}

Configuration Properties

# application.yml
server:
port: 8443
ssl:
# Server certificate
key-store: classpath:server.p12
key-store-password: ${SERVER_KEYSTORE_PASSWORD}
key-store-type: PKCS12
key-alias: server
# mTLS configuration
client-auth: need
trust-store: classpath:truststore.p12
trust-store-password: ${TRUSTSTORE_PASSWORD}
trust-store-type: PKCS12
# Protocol and cipher suites
enabled-protocols: TLSv1.3
ciphers: TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256
# Certificate revocation
crl-path: classpath:crl.pem
check-revocation: true
mtls:
certificate:
validation:
cache-size: 10000
cache-ttl-minutes: 5
require-registry: true
revocation:
crl-url: https://ca.example.com/crl.pem
ocsp-enabled: true
ocsp-timeout-seconds: 5
token:
binding: required  # required, optional, none
fingerprint-algorithm: SHA-256
client:
registry-enabled: true
allow-self-signed: false
min-key-size: 2048

Best Practices

  1. Certificate Management: Use short-lived certificates with automated rotation
  2. Revocation Checking: Always check CRL or OCSP for certificate status
  3. Key Size: Require minimum 2048-bit RSA or 256-bit ECC
  4. Caching: Cache validation results for performance
  5. Fallback Strategy: Define behavior when revocation services are unavailable
  6. Audit Logging: Log all certificate-based authentications
  7. Certificate Pinning: Consider pinning for high-security applications
  8. Regular Rotation: Rotate both server and client certificates periodically

Testing mTLS

@Test
public class MtlsTest {
@Test
public void testMtlsAuthentication() throws Exception {
// Create test certificates
KeyPair keyPair = generateKeyPair();
X509Certificate clientCert = generateSelfSignedCertificate(
"CN=test-client", keyPair);
// Create SSL context with client certificate
SSLContext sslContext = createMtlsContext(clientCert, keyPair);
// Build HTTP client
HttpClient client = HttpClient.newBuilder()
.sslContext(sslContext)
.build();
// Send request
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8443/api/secure/test"))
.GET()
.build();
HttpResponse<String> response = client.send(request, 
HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
}
}

Conclusion

Mutual TLS provides a powerful cryptographic foundation for token-based authentication systems, adding a second factor that significantly enhances security. By binding tokens to client certificates, applications gain:

  • Stronger authentication through cryptographic client identity
  • Token binding preventing token export and replay attacks
  • Defense in depth requiring both certificate and token compromise
  • Regulatory compliance meeting stringent authentication requirements

Implementing mTLS with tokens in Java applications is achievable through Spring Security, embedded servlet containers, and proper certificate management. While it adds complexity, the security benefits are substantial—particularly for APIs handling sensitive data, financial transactions, or requiring regulatory compliance.

For organizations serious about API security, combining mTLS with tokens represents a best practice that aligns with zero-trust architectures and provides protection against common token-based attacks. As the threat landscape evolves, mTLS offers a proven, standards-based approach to raising the bar for authentication security.

Java Programming Intermediate Topics – Modifiers, Loops, Math, Methods & Projects (Related to Java Programming)


Access Modifiers in Java:
Access modifiers control how classes, variables, and methods are accessed from different parts of a program. Java provides four main access levels—public, private, protected, and default—which help protect data and control visibility in object-oriented programming.
Read more: https://macronepal.com/blog/access-modifiers-in-java-a-complete-guide/


Static Variables in Java:
Static variables belong to the class rather than individual objects. They are shared among all instances of the class and are useful for storing values that remain common across multiple objects.
Read more: https://macronepal.com/blog/static-variables-in-java-a-complete-guide/


Method Parameters in Java:
Method parameters allow values to be passed into methods so that operations can be performed using supplied data. They help make methods flexible and reusable in different parts of a program.
Read more: https://macronepal.com/blog/method-parameters-in-java-a-complete-guide/


Random Numbers in Java:
This topic explains how to generate random numbers in Java for tasks such as simulations, games, and random selections. Random numbers help create unpredictable results in programs.
Read more: https://macronepal.com/blog/random-numbers-in-java-a-complete-guide/


Math Class in Java:
The Math class provides built-in methods for performing mathematical calculations such as powers, square roots, rounding, and other advanced calculations used in Java programs.
Read more: https://macronepal.com/blog/math-class-in-java-a-complete-guide/


Boolean Operations in Java:
Boolean operations use true and false values to perform logical comparisons. They are commonly used in conditions and decision-making statements to control program flow.
Read more: https://macronepal.com/blog/boolean-operations-in-java-a-complete-guide/


Nested Loops in Java:
Nested loops are loops placed inside other loops to perform repeated operations within repeated tasks. They are useful for pattern printing, tables, and working with multi-level data.
Read more: https://macronepal.com/blog/nested-loops-in-java-a-complete-guide/


Do-While Loop in Java:
The do-while loop allows a block of code to run at least once before checking the condition. It is useful when the program must execute a task before verifying whether it should continue.
Read more: https://macronepal.com/blog/do-while-loop-in-java-a-complete-guide/


Simple Calculator Project in Java:
This project demonstrates how to create a basic calculator program using Java. It combines input handling, arithmetic operations, and conditional logic to perform simple mathematical calculations.
Read more: https://macronepal.com/blog/simple-calculator-project-in-java/

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper