How to Decode a SAML Response by Hand

⏱️ 8 min read🔐 Pairs with the SAML Decoder

When single sign-on breaks, the evidence is a wall of base64 squiggles. This guide unwraps a SAML message layer by layer — URL, encoding, compression, XML — and then reads the handful of fields that decide whether the login was ever going to work.

Advertisement

What you're actually looking at

A SAML 2.0 response is an XML assertion — "this person authenticated, here's who they are, here's how long I vouch for them" — sent from an identity provider (IdP) to your service provider (SP). The XML never travels in the clear. It gets wrapped in layers chosen by the binding, and each layer has to come off in order:

  1. URL. In the redirect binding the whole message rides in the query string as SAMLRequest or SAMLResponse, URL-encoded on top of everything else.
  2. Base64 or base64url. POST binding uses standard base64; redirect uses the URL-safe alphabet (- and _ for + and /, usually without padding).
  3. DEFLATE. Redirect-binding messages are compressed with raw DEFLATE before encoding — no zlib header, no gzip header.
  4. XML. What's left is the message itself, with namespaced tags like <saml2p:Response>.

The bindings differ in one crucial way, and it's the difference between "base64-decode worked" and "I got binary garbage":

BindingWraps asCompressed?Where you find it
HTTP-POSTbase64(XML)NoForm field SAMLResponse in the POST to your ACS URL
HTTP-Redirectbase64url(DEFLATE(XML))Yes — raw DEFLATEQuery parameter on the IdP redirect URL

Redirect compresses because URLs have hard length limits in browsers — around 8 KB — and assertions don't fit uncompressed.

The decode, one layer at a time

Step 1: strip the URL. If what you captured is a full URL, pull the parameter out and URL-decode it. In JavaScript, new URL(theUrl).searchParams.get('SAMLResponse') does both.

Step 2: normalize the alphabet. Convert base64url back to base64 (-+, _/), strip whitespace, pad to a multiple of four with =. Now standard base64 decoders accept it.

Step 3: decode and sniff. Base64-decode to bytes. If the first byte is < (0x3C), you had a POST-binding message — the bytes are XML, and you're done. Otherwise you're holding a DEFLATE stream.

Step 4: inflate. Raw DEFLATE, no header — in Node that's zlib.inflateRawSync(buf); in the browser, pako's inflateRaw. Some stacks in the wild emit zlib-wrapped deflate instead (first byte 0x78), so a decoder worth using tries raw first, then wrapped. Out pops XML.

Or skip the steps: the SAML decoder on this site does all four and pretty-prints the result, entirely in your browser.

A worked example, with numbers

Take a realistic assertion: 2,527 bytes of XML naming [email protected] for the audience https://app.example.com/metadata. Two encodings of the same message:

That 72% size cut is why the redirect binding exists at all.

Reading the fields that matter

Once you can see the XML, six fields settle most debugging sessions:

FieldExample valueFails when…
Issuerhttps://idp.example.com/saml/metadataNot byte-identical to the IdP entity ID in your SP config
NameID[email protected]Format (emailAddress vs persistent) isn't what your user lookup expects
Audiencehttps://app.example.com/metadataIt equals some other SP's entity ID — wrong audience restriction
InResponseToid-9c8d7e6fDoesn't match an AuthnRequest ID you sent (or you expected IdP-initiated)
NotBefore2026-08-31T11:59:00ZYour clock says earlier — "not yet valid", the classic skew failure
NotOnOrAfter2026-08-31T12:05:00ZYour clock says later — expired assertion, often a replayed capture

Those last two deserve attention. In the example, the assertion was issued at 12:00:00Z, valid from 11:59:00Z until 12:05:00Z — a 6-minute window, 5 minutes of usable life after issue. SAML assertions are deliberately short-lived. If the IdP's clock is 3 minutes ahead of the SP's, the assertion arrives "not yet valid" and every login fails until someone adds skew tolerance. Five minutes is the conventional setting; needing more than ten usually means someone should fix NTP instead.

Decode your SAML response now

Paste the base64, the DEFLATE-compressed value, or the entire redirect URL — get pretty XML and the extracted fields, computed locally.

Open the SAML Decoder →

What decoding does not do

A decoded response is readable, not verified. The ds:Signature block you'll see inside is a claim until it's cryptographically checked against the IdP's X.509 certificate, with the reference digests and wrap-attack checks your SAML library performs. Never make an access decision by eyeballing decoded XML — decode to debug, verify in code. And since assertions carry names, emails, and group memberships, treat captures of production traffic the way you'd treat a password dump from your own HR system.

Related debugging tools

SAML is one of several "trust artifact" formats you'll end up squinting at. JWTs hide their claims in base64url too — the JWT decoder unpacks headers and payloads (also without verifying signatures). And since SAML verification bottoms out in X.509 certificates, the certificate decoder is the next stop when the question becomes "is this even the right cert?"

Advertisement

Frequently Asked Questions

Why won't base64-decoding alone show my SAML XML?

Because the message was probably compressed first. Redirect-binding messages are DEFLATE-compressed before base64url encoding, so decoding gives you a raw DEFLATE stream — binary bytes that don't start with '<'. You have to inflate those bytes (pako.inflateRaw in JavaScript, zlib inflateRawSync in Node) before the XML appears. POST-binding messages skip compression, so plain base64 decoding does show XML.

How do I capture a SAML response from my browser?

Open devtools before clicking login. For POST binding, watch the Network tab for the POST to your ACS URL (often /acs or /saml/acs), then look in the form data for the SAMLResponse field. For redirect binding, copy the full IdP URL with the SAMLRequest or SAMLResponse query parameter. Browser extensions like SAML-tracer automate exactly this capture.

What is clock skew in SAML?

Assertions carry NotBefore and NotOnOrAfter timestamps, and SPs accept them only if they bracket 'now' on the SP's clock. If the IdP server's clock runs a couple of minutes ahead of yours, fresh assertions look not-yet-valid. Skew tolerance — typically 5 minutes, configured on the SP — widens the accepted window on both ends to absorb normal drift.

Can I decode a SAML response that's signed?

Yes — signatures live inside the XML as a ds:Signature block, and they don't prevent decoding. But decoding is not verifying. Confirming the signature requires the IdP's X.509 certificate and cryptographic checks that belong in your SAML library, not in a human reading pretty-printed XML.

Related Tools