Securing Identity Federation: A Guide to SAML Assertion Parsing in Java

Security Assertion Markup Language (SAML) has become the cornerstone of enterprise single sign-on (SSO) and identity federation. At the heart of every SAML transaction lies the SAML Assertion—an XML-based security token that carries authentication and authorization statements about a user. This article provides a comprehensive guide to parsing and processing SAML Assertions in Java, covering everything from basic parsing to advanced security validation.


Understanding SAML Assertions

A SAML Assertion is an XML document that contains security information about a subject (typically a user). The key components of a SAML Assertion are:

  1. Issuer: Who created the assertion (Identity Provider)
  2. Subject: Who the assertion is about (the user)
  3. Conditions: Validity period and other restrictions
  4. Authentication Statement: How and when the user was authenticated
  5. Attribute Statement: User attributes (roles, email, etc.)
  6. Signature: XML signature for integrity and authenticity

Sample SAML Assertion Structure:

<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_123456" IssueInstant="2023-10-01T12:00:00Z">
<saml2:Issuer>https://idp.example.com</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<!-- XML Signature -->
</ds:Signature>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">[email protected]</saml2:NameID>
</saml2:Subject>
<saml2:Conditions NotBefore="2023-10-01T12:00:00Z" NotOnOrAfter="2023-10-01T12:05:00Z"/>
<saml2:AuthnStatement AuthnInstant="2023-10-01T11:55:00Z">
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
<saml2:AttributeStatement>
<saml2:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml2:AttributeValue>[email protected]</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="role" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml2:AttributeValue>admin</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
</saml2:Assertion>

Key Java Libraries for SAML Processing

Several libraries are commonly used for SAML processing in Java:

  1. OpenSAML (Most popular)
  2. Spring Security SAML (Deprecated, but still used)
  3. SimpleSAMLphp Java port (Limited use)
  4. Auth0 java-jwt (For JWT, not SAML)
  5. SAML2 from OIOSAML (Danish government)

OpenSAML is the industry standard and will be our focus.


Setting Up OpenSAML

Maven Dependencies:

<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-core</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-api</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-impl</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-security-api</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-security-impl</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-xmlsec-api</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-xmlsec-impl</artifactId>
<version>4.3.0</version>
</dependency>

Initialization:

import org.opensaml.core.config.InitializationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
public class SamlInitializer {
public static void initialize() {
try {
InitializationService.initialize();
} catch (Exception e) {
throw new RuntimeException("Failed to initialize OpenSAML", e);
}
}
}

Basic SAML Assertion Parsing

Here's how to parse a SAML Assertion from XML:

import org.opensaml.core.xml.io.UnmarshallerFactory;
import org.opensaml.core.xml.io.UnmarshallingException;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.Map;
public class BasicSamlParser {
public Assertion parseAssertion(String samlXml) throws Exception {
// Convert XML string to DOM Document
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse(new ByteArrayInputStream(samlXml.getBytes()));
Element element = document.getDocumentElement();
// Get unmarshaller factory
UnmarshallerFactory unmarshallerFactory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory();
// Unmarshall the assertion
return (Assertion) unmarshallerFactory.getUnmarshaller(element).unmarshall(element);
}
public void printAssertionInfo(Assertion assertion) {
System.out.println("=== SAML Assertion Details ===");
System.out.println("Assertion ID: " + assertion.getID());
System.out.println("Issuer: " + assertion.getIssuer().getValue());
System.out.println("Issue Instant: " + assertion.getIssueInstant());
// Subject information
if (assertion.getSubject() != null && assertion.getSubject().getNameID() != null) {
System.out.println("Subject: " + assertion.getSubject().getNameID().getValue());
System.out.println("NameID Format: " + assertion.getSubject().getNameID().getFormat());
}
// Conditions
if (assertion.getConditions() != null) {
System.out.println("Not Before: " + assertion.getConditions().getNotBefore());
System.out.println("Not On Or After: " + assertion.getConditions().getNotOnOrAfter());
}
// Authentication context
if (!assertion.getAuthnStatements().isEmpty()) {
System.out.println("Authn Instant: " + assertion.getAuthnStatements().get(0).getAuthnInstant());
System.out.println("Authn Context: " + 
assertion.getAuthnStatements().get(0).getAuthnContext().getAuthnContextClassRef().getURI());
}
}
public Map<String, String> extractAttributes(Assertion assertion) {
Map<String, String> attributes = new HashMap<>();
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
for (Attribute attribute : attributeStatement.getAttributes()) {
String attributeName = attribute.getName();
String attributeValue = attribute.getAttributeValues().get(0).getDOM().getTextContent();
attributes.put(attributeName, attributeValue);
}
}
return attributes;
}
}

Advanced Parsing with Security Validation

Complete SAML Processor with Validation:

import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.saml.common.SAMLObject;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureException;
import org.opensaml.xmlsec.signature.support.SignatureValidator;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
public class SecureSamlProcessor {
private List<X509Certificate> trustedCertificates = new ArrayList<>();
public void addTrustedCertificate(X509Certificate certificate) {
trustedCertificates.add(certificate);
}
public SamlValidationResult validateAssertion(Assertion assertion) {
SamlValidationResult result = new SamlValidationResult();
try {
// 1. Validate signature
validateSignature(assertion, result);
// 2. Validate conditions
validateConditions(assertion, result);
// 3. Validate timestamps
validateTimestamps(assertion, result);
// 4. Validate issuer
validateIssuer(assertion, result);
} catch (Exception e) {
result.addError("Validation error: " + e.getMessage());
}
return result;
}
private void validateSignature(Assertion assertion, SamlValidationResult result) {
Signature signature = assertion.getSignature();
if (signature == null) {
result.addError("Assertion is not signed");
return;
}
boolean signatureValid = false;
List<String> signatureErrors = new ArrayList<>();
// Try each trusted certificate
for (X509Certificate certificate : trustedCertificates) {
try {
BasicX509Credential credential = new BasicX509Credential(certificate);
SignatureValidator.validate(signature, credential);
signatureValid = true;
break; // Stop at first valid signature
} catch (SignatureException e) {
signatureErrors.add("Signature validation failed with certificate: " + 
certificate.getSubjectX500Principal().getName());
}
}
if (!signatureValid) {
result.addError("Signature validation failed with all trusted certificates: " + signatureErrors);
}
}
private void validateConditions(Assertion assertion, SamlValidationResult result) {
if (assertion.getConditions() == null) {
result.addWarning("No conditions found in assertion");
return;
}
// Check audience restrictions if any
if (assertion.getConditions().getAudienceRestrictions() != null && 
!assertion.getConditions().getAudienceRestrictions().isEmpty()) {
// Implement audience validation logic
result.addInfo("Audience restrictions present");
}
// Check one-time use
if (assertion.getConditions().getOneTimeUse() != null) {
result.addInfo("One-time use condition present");
}
}
private void validateTimestamps(Assertion assertion, SamlValidationResult result) {
Instant now = Instant.now();
if (assertion.getConditions() != null) {
// Check NotBefore
if (assertion.getConditions().getNotBefore() != null && 
now.isBefore(assertion.getConditions().getNotBefore())) {
result.addError("Assertion is not yet valid");
}
// Check NotOnOrAfter
if (assertion.getConditions().getNotOnOrAfter() != null && 
now.isAfter(assertion.getConditions().getNotOnOrAfter())) {
result.addError("Assertion has expired");
}
}
// Check IssueInstant is reasonable (not too far in past/future)
if (assertion.getIssueInstant() != null) {
long timeDiff = Math.abs(now.toEpochMilli() - assertion.getIssueInstant().toEpochMilli());
if (timeDiff > 5 * 60 * 1000) { // 5 minutes tolerance
result.addWarning("Assertion issue time is significantly different from current time");
}
}
}
private void validateIssuer(Assertion assertion, SamlValidationResult result) {
if (assertion.getIssuer() == null || assertion.getIssuer().getValue() == null) {
result.addError("Issuer is missing");
return;
}
String issuer = assertion.getIssuer().getValue();
// Validate against trusted issuers
if (!isTrustedIssuer(issuer)) {
result.addError("Untrusted issuer: " + issuer);
}
}
private boolean isTrustedIssuer(String issuer) {
// Implement your trusted issuer validation logic
return issuer != null && issuer.startsWith("https://trusted-idp.");
}
}
class SamlValidationResult {
private List<String> errors = new ArrayList<>();
private List<String> warnings = new ArrayList<>();
private List<String> info = new ArrayList<>();
public void addError(String error) { errors.add(error); }
public void addWarning(String warning) { warnings.add(warning); }
public void addInfo(String infoMsg) { info.add(infoMsg); }
public boolean isValid() { return errors.isEmpty(); }
public List<String> getErrors() { return errors; }
public List<String> getWarnings() { return warnings; }
public List<String> getInfo() { return info; }
}

Parsing SAML Responses (Contains Assertions)

SAML Assertions are typically embedded within SAML Responses:

import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.Status;
import org.opensaml.saml.saml2.core.StatusCode;
public class SamlResponseParser {
public List<Assertion> parseResponse(Response response) {
List<Assertion> assertions = new ArrayList<>();
// Check response status
Status status = response.getStatus();
if (status != null && status.getStatusCode() != null) {
String statusCode = status.getStatusCode().getValue();
if (!StatusCode.SUCCESS.equals(statusCode)) {
throw new RuntimeException("SAML Response failed with status: " + statusCode);
}
}
// Extract assertions
assertions.addAll(response.getAssertions());
return assertions;
}
public void processEncryptedAssertion(Response response) throws Exception {
// Handle encrypted assertions
if (!response.getEncryptedAssertions().isEmpty()) {
// Decrypt using your private key
// Implementation depends on your encryption setup
System.out.println("Encrypted assertions present - decryption required");
}
}
}

Best Practices for SAML Parsing

  1. Always Validate Signatures: Never trust unsigned assertions
  2. Check Timestamps: Prevent replay attacks by validating time windows
  3. Validate Issuer: Ensure assertions come from trusted identity providers
  4. Handle Namespaces Properly: SAML relies heavily on XML namespaces
  5. Use Secure XML Parsing: Protect against XXE attacks
  6. Implement Proper Error Handling: Don't expose sensitive information in errors

Secure DocumentBuilder Configuration:

private DocumentBuilder createSecureDocumentBuilder() throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// Security configurations to prevent XXE attacks
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
factory.setNamespaceAware(true);
return factory.newDocumentBuilder();
}

Common Pitfalls and Solutions

Pitfall 1: Ignoring Clock Skew

// Solution: Allow small clock skew
private static final long CLOCK_SKEW_ALLOWANCE = 2 * 60 * 1000; // 2 minutes
private boolean isTimeValid(Instant notBefore, Instant notOnOrAfter) {
Instant now = Instant.now();
Instant adjustedNow = now.plusMillis(CLOCK_SKEW_ALLOWANCE);
return (notBefore == null || !adjustedNow.isBefore(notBefore)) &&
(notOnOrAfter == null || now.isBefore(notOnOrAfter.plusMillis(CLOCK_SKEW_ALLOWANCE)));
}

Pitfall 2: Not Handling Multiple Assertions

// Always handle multiple assertions
for (Assertion assertion : response.getAssertions()) {
SamlValidationResult result = validator.validateAssertion(assertion);
if (result.isValid()) {
return assertion; // Use first valid assertion
}
}
throw new RuntimeException("No valid assertions found");

Testing SAML Parsing

Unit Test Example:

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SamlParserTest {
@BeforeAll
static void setup() {
SamlInitializer.initialize();
}
@Test
void testValidAssertionParsing() throws Exception {
String samlXml = loadTestSaml("valid-assertion.xml");
BasicSamlParser parser = new BasicSamlParser();
Assertion assertion = parser.parseAssertion(samlXml);
assertNotNull(assertion);
assertEquals("https://idp.example.com", assertion.getIssuer().getValue());
assertEquals("[email protected]", assertion.getSubject().getNameID().getValue());
}
@Test
void testInvalidSignature() throws Exception {
String samlXml = loadTestSaml("tampered-assertion.xml");
SecureSamlProcessor processor = new SecureSamlProcessor();
BasicSamlParser parser = new BasicSamlParser();
Assertion assertion = parser.parseAssertion(samlXml);
SamlValidationResult result = processor.validateAssertion(assertion);
assertFalse(result.isValid());
assertTrue(result.getErrors().stream()
.anyMatch(error -> error.contains("Signature validation")));
}
private String loadTestSaml(String filename) {
// Load test SAML from resources
return "...";
}
}

Conclusion

SAML Assertion parsing in Java requires careful attention to security and XML processing details. By using OpenSAML and following security best practices, you can:

  • Securely parse and validate SAML assertions
  • Extract user information and attributes reliably
  • Prevent common attacks like replay, tampering, and XXE
  • Integrate seamlessly with identity providers and service providers

Remember that SAML security is only as strong as your validation logic. Always validate signatures, check timestamps, verify issuers, and handle errors securely to build robust SAML-based authentication systems.

Leave a Reply

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


Macro Nepal Helper