spring-projects/spring-security · error · BadCredentialsException
Failed to parse client certificate
Error message
Failed to parse client certificate
What it means
SubjectX500PrincipalExtractor parses the certificate's subject DN as an LDAP name (javax.naming.ldap.LdapName) to walk its RDNs. If the DN string is not a valid LDAP name, InvalidNameException is converted to a BadCredentialsException with the message 'Failed to parse client certificate'.
Solutions
- Inspect the certificate's DN and confirm it is RFC 2253 compliant; reissue the certificate with a properly escaped DN if it is not.
- Feed the extractor the RFC 2253 form of the DN (cert.getSubjectX500Principal().getName(X500Principal.RFC2253)) rather than the legacy getSubjectDN().getName().
- Fall back to SubjectDnX509PrincipalExtractor with a regex if the DNs in your PKI are not LDAP-parseable.
- Catch BadCredentialsException around authentication so a malformed certificate results in a 401 rather than a server error.
Example fix
// before String dn = cert.getSubjectDN().getName(); // legacy, may be unparseable // after String dn = cert.getSubjectX500Principal().getName(X500Principal.RFC2253); // properly escaped for LdapName
Defensive patterns
Strategy: try-catch
Validate before calling
try {
new javax.naming.ldap.LdapName(dn);
} catch (javax.naming.InvalidNameException e) {
// DN not RFC 2253 parseable — refuse before authentication
} Type guard
boolean isParseableDn(String dn) {
try { new LdapName(dn); return true; } catch (InvalidNameException e) { return false; }
} Try / catch
try {
return extractor.extractPrincipal(cert);
} catch (BadCredentialsException e) {
log.error("Unparseable subject DN on client cert", e);
throw new BadCredentialsException("Client certificate rejected");
} Prevention
- Use getSubjectX500Principal().getName(X500Principal.RFC2253) as the DN source
- Issue certificates with properly escaped CN values (no raw commas/quotes)
- Test with real-world DNs containing special characters
- Have a fallback extractor for legacy DN formats
When it happens
Trigger: Calling getDns (indirectly via getSubject/principalName) with a subject DN that LdapName cannot parse — e.g. a DN containing characters that must be escaped (like a CN containing a comma, '+', or quotes) but are not RFC 2253 escaped.
Common situations: Certificates with free-form CN values containing special characters (commas in company names, apostrophes in names); unusual attribute type OIDs or legacy DN encodings produced by some CAs; code passing a manually constructed DN string instead of one from getSubjectX500Principal().getName(X500Principal.RFC2253).
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Bad credentials
- No matching pattern was found in subject DN
- No matching pattern was found in subject DN
- AccountStatusUserDetailsChecker.disabled
- AccountStatusUserDetailsChecker.expired
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/cf3c403eceacd0ee.
Report an issue: GitHub.
Appendix: source
Thrown at web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java:83
public Object extractPrincipal(X509Certificate clientCert) {
Assert.notNull(clientCert, "clientCert cannot be null");
X500Principal principal = clientCert.getSubjectX500Principal();
String subjectDN = principal.getName(this.x500PrincipalFormat);
this.logger.debug(LogMessage.format("Subject DN is '%s'", subjectDN));
String principalName = getSubject(subjectDN);
this.logger.debug(LogMessage.format("Extracted Principal name is '%s'", principalName));
return principalName;
}
private List<Rdn> getDns(String subjectDn) {
try {
// read most-specific first, see gh-19254
List<Rdn> rdns = new ArrayList<>(new LdapName(subjectDn).getRdns());
Collections.reverse(rdns);
return rdns;
}
catch (InvalidNameException ex) {
throw new BadCredentialsException("Failed to parse client certificate", ex);
}
}
private String getSubject(String subjectDn) {
for (Rdn rdn : getDns(subjectDn)) {
String type = rdn.getType();
if (this.subjectDnType.equals(type)) {
return String.valueOf(rdn.getValue());
}
}
throw new BadCredentialsException(this.messages.getMessage("SubjectX500PrincipalExtractor.noMatching",
new Object[] { subjectDn }, "No matching pattern was found in subject DN: {0}"));
}
@Override
public void setMessageSource(MessageSource messageSource) {
Assert.notNull(messageSource, "messageSource cannot be null");
this.messages = new MessageSourceAccessor(messageSource);View on GitHub (pinned to 96852e8860)