·2 min read·← All posts
Go SAML Identity Federation Banking

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:

  1. Parse the XML. Use encoding/xml if you trust the input, etree if you need XPath access.
  2. Extract the certificate. The signing cert is embedded in the assertion’s <ds:Signature> element (or registered out-of-band; both patterns exist).
  3. Verify the certificate chain. Against your trusted bundle (typically the bank’s root CA).
  4. Canonicalise the signed elements. Exclusive XML Canonicalization (Exclusive C14N) per RFC 3741.
  5. Verify the signature. RSA-SHA256 typically; the signed digest must match the signed elements after canonicalization.
  6. 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:

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.

← Back to all posts