Why you still need SAML in 2026
Most fintechs would prefer OIDC. Most banks still ship SAML. If your customer is a bank, your customer’s IdP speaks SAML 2.0; their authentication team has been there for a decade and isn’t moving.
The pragmatic answer: a SAML verifier on your side. The user authenticates against the bank’s IdP, the bank’s IdP redirects to your service with a signed SAML assertion, you verify the assertion and mint your own JWT for the session.
The verify path
A SAML assertion is a signed XML document. Verification:
- Parse the XML. Use
encoding/xmlif you trust the input,etreeif you need XPath access. - Extract the certificate. The signing cert is embedded in the assertion’s
<ds:Signature>element (or registered out-of-band; both patterns exist). - Verify the certificate chain. Against your trusted bundle (typically the bank’s root CA).
- Canonicalise the signed elements. Exclusive XML Canonicalization (Exclusive C14N) per RFC 3741.
- Verify the signature. RSA-SHA256 typically; the signed digest must match the signed elements after canonicalization.
- Check the assertion validity. NotBefore, NotOnOrAfter, AudienceRestriction.
The library landscape
Pure stdlib doesn’t work — Exclusive C14N is enough code that nobody wants to hand-roll it. The standard pick in Go:
crewjam/samlfor the full SAML SP flowrussellhaering/goxmldsigfor the signature verify alone
For a bank federation, you typically want just the verify (not the full SP/IdP machinery). goxmldsig is the smaller dependency.
What the verify looks like
import (
"github.com/russellhaering/goxmldsig"
)
doc := etree.NewDocument()
_ = doc.ReadFromString(rawAssertion)
ctx := dsig.NewDefaultValidationContext(&dsig.MemoryX509CertificateStore{
Roots: []*x509.Certificate{bankRootCert},
})
validated, err := ctx.Validate(doc.Root())
if err != nil {
return ErrInvalidAssertion
}
// validated.Children() now has the signed assertion content.
// Extract NameID, AudienceRestriction, AttributeStatement.
After verify, the assertion’s subject becomes the user identity for your JWT mint.
Pitfalls
Verifying without checking AudienceRestriction. The assertion is signed for a specific audience (your service). Without the check, you’d accept assertions intended for other services that share the IdP.
Accepting assertions outside their validity window. Replay protection — NotOnOrAfter is the spec’s tool. Reject assertions older than a few minutes.
Not checking the chain. A self-signed certificate that happens to verify the signature isn’t enough. The cert must chain to a root you trust (the bank’s published root CA).
What ships
For the federations I’ve worked on (UAE bank IdP → Bancnet, ADFS → internal portals), the verify code was ~200 lines on top of goxmldsig. The remainder of the SP code (metadata generation, ACS endpoint wiring) is another ~200. Manageable; reviewable; in production for years without incident.
The pragmatic rule: SAML is older than you, runs deeper than you’d like, and is the right tool when the customer says it is. Stdlib + one focused library beats a full SAML framework you don’t need 80% of.