Decode Keycloak SAMLResponse and SAMLRequest on Linux

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
quay.io/keycloak/keycloak:26.8.0
libxml2-2.12.5-10.el10_2.2.x86_64
python3-3.12.13-2.el10_2.1.x86_64
Applies to Any Linux host with Podman or Docker
Privilege Normal user (rootless Podman). Realm setup uses the Keycloak bootstrap admin
Scope How to decode a Keycloak SAMLResponse or SAMLRequest into XML on Linux, and which fields to read first. HTTP-POST is Base64. HTTP-Redirect wraps the XML as DEFLATE, then Base64, then URL encoding. Does not cover audience failures, ACS allow-list errors, signature validation, or turning assertion encryption on.
Related guides Keycloak SAML metadata
Keycloak SAML NameID
Keycloak SAML attribute mapper
Fix Keycloak SAML audience errors
Fix Keycloak SAML ACS URL errors
Short on time?Beta Get the gist of this 13-minute read in seconds, then ask follow-up questions

In this HTTP-POST login, the browser carries SAMLResponse as Base64-encoded XML. Base64 is encoding, not encryption. With HTTP-Redirect, the XML is compressed and encoded before Keycloak puts it in the URL. To read it, undo those layers in reverse: URL-decode, then Base64-decode, then inflate the raw-DEFLATE data.

The captures come from the two-realm login in Keycloak SAML SSO:

  • Realm company on 127.0.0.1:18115 is the identity provider
  • Realm apps on 127.0.0.1:18116 is the broker
  • The POST field and the redirect query value are saved as files on that host

A captured assertion can contain sensitive identity and authorization data. Treat it as authentication-sensitive data, and do not paste it into a website.

A SAMLResponse wraps the assertion. The Response is the outer SAML protocol message. The Assertion inside it contains the authenticated subject, audience, conditions, and user attributes. An encrypted response may contain EncryptedAssertion instead.

text
SAMLResponse
└── Assertion
    ├── Subject / NameID
    ├── Conditions / Audience
    └── AttributeStatement

Quick Answer

A SAML binding defines how the XML message travels through HTTP. The two bindings carry the same kind of SAML XML but package it differently for HTTP. POST puts Base64 text in a form field. Redirect compresses the XML first so it fits more efficiently in a URL. The two browser bindings you will most often inspect are:

  • HTTP-POST: SAMLResponse is Base64-encoded XML. Decode Base64, then format the XML.
  • HTTP-Redirect: SAMLRequest or SAMLResponse is URL-encoded, Base64-encoded, and raw-DEFLATE compressed. Decode in reverse: URL-decode, then Base64-decode, then inflate.

Write the decoded XML before you format it. The command prints nothing:

bash
base64 -d response.b64 > response-decoded.xml

Format a second file for reading. xmllint adds line breaks. Keep response-decoded.xml unchanged after decoding so you still have the original XML bytes for later signature work:

bash
xmllint --format response-decoded.xml > response-pretty.xml

Both commands print nothing when the files are written. Use the two files for different jobs:

  • response-pretty.xml: read this one
  • response-decoded.xml: the XML after the Base64 step, left unchanged for any signature validation

For a Redirect-binding value saved as request.urlencoded, undo the three layers in one step:

bash
python3 - <<'PY'
import base64, urllib.parse, zlib

value = open("request.urlencoded").read().strip()
raw = base64.b64decode(urllib.parse.unquote(value))
xml = zlib.decompress(raw, -15)
open("request.xml", "wb").write(xml)
PY

The script writes request.xml and prints nothing. The sections below take the same pipelines apart, including why the zlib window is -15.


Capture SAMLRequest and SAMLResponse Safely

Open browser DevTools, choose Network, enable Preserve log, then start the SAML login. Several redirects happen during sign-in, and Preserve log keeps the earlier requests on screen. Where you find the message depends on the binding:

  • A POST-binding message is a form field
  • A Redirect-binding message is a query parameter

They are not decoded the same way. A Redirect-binding URL may also contain RelayState, SigAlg, and Signature. Decode only the value of the SAMLRequest or SAMLResponse parameter. RelayState is separate application or navigation state, not SAML XML.

These arrows show the decoding order, not the order used when the sender encoded the message.

               HTTP-POST
                   ↓
Copy the parsed SAMLResponse form value
                   ↓
             Base64-decode
                   ↓
                  XML

         Redirect binding
                ↓
  URL-decode the query parameter
                ↓
    base64-decode SAMLRequest
                ↓
       raw DEFLATE inflate
                ↓
XML starts with samlp:AuthnRequest

If you copy the raw application/x-www-form-urlencoded request body instead of the parsed form field, URL-decode that field first.

On this login I saved three files:

  • response.b64: the POST SAMLResponse form value the browser sent to http://127.0.0.1:18116/realms/apps/broker/company-idp/endpoint
  • request.urlencoded: the Redirect SAMLRequest query parameter from the company SSO URL
  • request.b64: that same parameter after URL-decoding

Copy the field only. A HAR export also contains cookies and the password POST. Keep the files on the host that captured them.


Decode POST-Binding SAMLResponse

If you copied the parsed SAMLResponse form value, Base64-decode it. The bytes are already XML.

Decode the first bytes of the saved field:

bash
base64 -d response.b64 | head -c 72
output
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns

The text starts with <samlp:Response. That is the SAML response element. Do not inflate a POST-bound response. After Base64 decoding, it is already XML. The next section is the binding that does need inflate.


Decode Redirect-Binding Messages

Keycloak sent the AuthnRequest with HTTP-Redirect. HTTP-Redirect wraps the XML as DEFLATE, then Base64, then URL encoding. To inspect it, undo those layers in reverse: URL-decode, then Base64-decode, then raw-DEFLATE inflate. Base64 alone still leaves compressed bytes. Two details matter before you inflate:

  • The query parameter is URL-encoded. A Base64 + can appear as %2B, so feeding the encoded value directly to base64 -d can fail. URL-decode first.
  • -15 tells Python's zlib module to expect a raw DEFLATE stream, without a zlib or gzip header. That is what the SAML HTTP-Redirect binding uses.

The first 96 bytes of the inflated request are XML:

bash
base64 -d request.b64 | python3 -c \
  'import sys,zlib; sys.stdout.buffer.write(zlib.decompress(sys.stdin.buffer.read(), -15)[:96])'
output
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:nam

That is the AuthnRequest. A POST SAMLResponse never takes this inflate step. A Redirect SAMLResponse, if an identity provider sends one, uses the same three steps as this request.


Pretty-Print SAML XML on Linux

Pretty-printing is useful for reading. Do not use a reformatted or edited copy as proof of the XML you decoded.

The Quick Answer created three files:

  • response.b64 is the HTTP form value
  • response-decoded.xml is the XML after Base64 decoding, left byte-for-byte unchanged
  • response-pretty.xml is the formatted copy for reading

Read the top of the formatted file. The certificate and signature value continue below these lines:

bash
head -n 6 response-pretty.xml
output
<?xml version="1.0"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Destination="http://127.0.0.1:18116/realms/apps/broker/company-idp/endpoint" ID="ID_df43df89-bd92-4b80-9322-2df98d4d3016" InResponseTo="ID_e40625d6-95a9-43d4-b855-f3c8a6d9597a" IssueInstant="2026-10-04T02:23:41.566Z" Version="2.0">
  <saml:Issuer>http://127.0.0.1:18115/realms/company</saml:Issuer>
  <dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
    <dsig:SignedInfo>
      <dsig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>

Compare sizes. Base64 expands the same document. It does not hide it:

bash
wc -c response.b64 response-pretty.xml
output
10860 response.b64
 8144 response-pretty.xml
19004 total

10860 and 8144 are the encoded POST field and the formatted XML. Anyone who can read the POST can read the NameID after this step, unless the assertion is encrypted.


Fields to Inspect First

Read these before you change a client setting. They answer who sent the response, where it was posted, and which request it answers.

Field Beginner meaning
Issuer Who created the SAML message
Destination Where the response was intended to be delivered
InResponseTo Which AuthnRequest this response answers
StatusCode Whether the identity provider reports the SAML operation as successful or failed
NameID Identifier representing the user in this SAML login
Audience The service provider for which the assertion is intended
NotBefore / NotOnOrAfter When the assertion is valid
Attribute Extra user information such as email, groups, or roles

In this POST-binding response, Destination is the Assertion Consumer Service (ACS) URL, the service-provider endpoint where the browser delivers the SAML response:

bash
xmllint --xpath 'string(//*[local-name()="Response"]/@Destination)' response-pretty.xml
output
http://127.0.0.1:18116/realms/apps/broker/company-idp/endpoint

InResponseTo is the ID of the AuthnRequest. It lets the service provider correlate this response with the login request it started. On this login it matched the inflated request:

bash
xmllint --xpath 'string(//*[local-name()="Response"]/@InResponseTo)' response-pretty.xml
output
ID_e40625d6-95a9-43d4-b855-f3c8a6d9597a

Both Issuer values are the company realm. The outer Response can have its own Issuer, and the nested Assertion can also have an Issuer. In this Keycloak login they are both the same identity provider realm:

bash
xmllint --xpath '//*[local-name()="Issuer"]/text()' response-pretty.xml
output
http://127.0.0.1:18115/realms/company
http://127.0.0.1:18115/realms/company

NotBefore means do not accept the assertion before this time. NotOnOrAfter means do not accept it at or after this time. Read NotOnOrAfter:

bash
xmllint --xpath 'string(//*[local-name()="Conditions"]/@NotOnOrAfter)' response-pretty.xml
output
2026-10-04T02:24:39.565Z

NotBefore on the same element was 2026-10-04T02:23:39.565Z. In this captured 26.8.0 response, those timestamps were one minute apart. A later decode of this file can show a time that has already passed. That does not mean the destination or the audience was wrong at login. The service provider also considers clock skew, so do not diagnose a time failure from a stale captured XML file alone.

The status on this response was urn:oasis:names:tc:SAML:2.0:status:Success. A failed login can still carry a response. Read StatusCode before you spend time on NameID.


Inspect NameID, Audience and Attributes

These three parts of the assertion describe the user and the intended receiver:

  • NameID: the subject identifier the identity provider sent for this user. Depending on the SAML configuration, it might be a username, an email address, a persistent identifier, or another mapped value.
  • Audience: the service provider for which the assertion is intended
  • Attributes: the extra claims. SAML user attributes such as email, roles, and groups normally appear inside an AttributeStatement as one or more Attribute elements.

The NameID is the username, in the unspecified format:

bash
xmllint --xpath 'string(//*[local-name()="NameID"])' response-pretty.xml
output
alice

The audience is the apps realm URL, which is the SAML client ID. It is not the broker endpoint from Destination:

bash
xmllint --xpath 'string(//*[local-name()="Audience"])' response-pretty.xml
output
http://127.0.0.1:18116/realms/apps

An email mapper wrote one attribute. Read that value:

bash
xmllint --xpath 'string(//*[@Name="email"]/*[local-name()="AttributeValue"])' response-pretty.xml
output
alice@example.com

The same assertion also contains several Role values emitted by Keycloak's role_list mapper:

  • They can include realm roles as well as client roles inherited through the user's effective role mappings
  • They are separate from the email attribute
  • Role and group mapping is where those values are configured

If the expected attribute is absent from the decoded XML, the problem is in the SAML configuration or source data, not the decoding step.


Identify XML Signatures and Encrypted Assertions

A signature and an encrypted assertion change what you can inspect. Keep these limits in mind:

  • Decoding only reveals the XML. It does not prove that the signature is valid.
  • Signature validation requires the identity provider's trusted signing certificate and proper XML signature verification.
  • Signing and encryption is where those checks are configured.

This clear response has two Signature elements:

bash
xmllint --xpath 'count(//*[local-name()="Signature"])' response-pretty.xml
output
2

string() returns only the first SignatureMethod. On this file that algorithm is RSA-SHA256. This line does not prove the second signature uses the same algorithm:

bash
xmllint --xpath 'string(//*[local-name()="SignatureMethod"]/@Algorithm)' response-pretty.xml
output
http://www.w3.org/2001/04/xmldsig-more#rsa-sha256

To find where each signature is attached, count signatures directly inside the outer Response:

bash
xmllint --xpath \
  'count(/*[local-name()="Response"]/*[local-name()="Signature"])' \
  response-pretty.xml
output
1

Then count signatures directly inside the Assertion:

bash
xmllint --xpath \
  'count(//*[local-name()="Assertion"]/*[local-name()="Signature"])' \
  response-pretty.xml
output
1

The captured XML contains two signatures: one on the Response and one on the Assertion. From these checks:

  • The inspected SignatureMethod used RSA-SHA256
  • NameID is still readable, because signing does not hide content
  • To list every signature algorithm, print the Algorithm attributes without string()
  • Check a signature against response-decoded.xml, not against a copy you formatted or edited

If the assertion is encrypted, Base64 decoding only reveals the outer SAML XML. Count EncryptedAssertion:

bash
xmllint --xpath 'count(//*[local-name()="EncryptedAssertion"])' response-encrypted.xml
output
1

NameID is not in that file:

bash
xmllint --xpath 'count(//*[local-name()="NameID"])' response-encrypted.xml
output
0

When the EncryptedAssertion count is 1:

  • Those values are inside the encrypted assertion, so they cannot be read from this XML until the assertion is decrypted with the matching private key
  • The visible methods on this response were AES-256-GCM for the assertion and RSA-OAEP for the wrapped key
  • Configuring that encryption is covered in the signing and encryption guide, not here

Redact SAML Data Before Sharing

A captured assertion can contain sensitive identity and authorization data and, depending on its profile and validity conditions, may still be security-sensitive while valid. Treat it as authentication-sensitive data.

For destination, audience, or attribute troubleshooting, redact:

  • SignatureValue
  • Certificate data. The X.509 certificate is public-key material rather than a private key, but it can still expose realm and key metadata you may not need in a ticket.
  • KeyName, when it is a realm key id from your server
  • NameID and user attributes such as alice@example.com

Leave the element names and the URLs. Destination, Audience, Issuer, and InResponseTo are the usual question. Replace the person and the certificate with a short note that you removed them.

For a signature-validation problem, the rules change:

  • Do not alter the signed XML. A redacted or reformatted file is not something another person can validate.
  • Share response-decoded.xml through an approved secure channel with the minimum people necessary.
  • Avoid pasting a real assertion into a third-party decoder unless you have verified that processing stays on your machine and your organization's policy allows it.

Use the Trace to Diagnose the Next Error

Decoding tells you which SAML value is wrong. The next step is to correct the corresponding Keycloak or service-provider setting.

  • Destination is not the ACS the service provider expected: compare the AuthnRequest ACS, the Keycloak SAML client ACS settings, and the service provider's public or broker URL
  • Audience is not the service provider entity ID: that mismatch is covered in audience errors
  • NameID is not the identifier the service provider expected: formats and mappers are covered in Keycloak SAML NameID
  • The attribute you expected is missing: that is a mapper problem, covered in Keycloak SAML attribute mappers
  • AssertionConsumerServiceURL is rejected or not registered: see Keycloak SAML ACS URL errors
  • StatusCode is Success but you cannot find NameID: first check whether the response contains EncryptedAssertion. If it does not, inspect the assertion and the subject structure
  • A signature is present and the login still failed: decoding did not validate it. Continue with Keycloak SAML signing and encryption using the untouched decoded XML

Once you identify the mismatched field, follow the corresponding troubleshooting path rather than changing unrelated SAML settings. Decoding the same response a second time will show the same values.


Summary

HTTP-POST normally gives you Base64-encoded XML. HTTP-Redirect adds raw DEFLATE and URL encoding, so decode those layers in reverse.

Keep the original decoded XML untouched and use a formatted copy for reading. Start with StatusCode, Destination, Issuer, InResponseTo, NameID, Audience, the timestamps, and the expected attributes.

Decoding reveals signatures but does not validate them. An EncryptedAssertion keeps the subject and attributes unreadable until the service provider decrypts it.


References


Frequently Asked Questions

Is SAMLResponse encrypted or just base64 encoded?

In this HTTP-POST login the form field is Base64. Decoding it produces XML. That XML can contain an EncryptedAssertion, which is actual encryption, or a readable NameID, which means the assertion itself was not encrypted. Base64 size and the formatted XML size on this login were 10860 and 8144 bytes. That gap is encoding overhead. An HTTP-Redirect message is also compressed with raw DEFLATE before the Base64 step.

How do I decode SAMLResponse on Linux?

Save the POST form field as response.b64. Run base64 -d into response-decoded.xml, then format a copy with xmllint as response-pretty.xml. Do not inflate a POST body. Read Destination, Issuer, InResponseTo, NameID, and Audience from the formatted file. For a Redirect-binding SAMLRequest, URL-decode, Base64-decode, then inflate raw DEFLATE.

Why does base64 decoding a SAMLRequest look compressed?

HTTP-Redirect compresses the XML with raw DEFLATE, then Base64-encodes it, then URL-encodes it. The first bytes after base64 -d are not an XML tag. URL-decode the query parameter, Base64-decode, then inflate with zlib window -15. A POST SAMLResponse does not use that compress step.

Which SAML fields should I inspect?

Start with Destination, Issuer, InResponseTo, StatusCode, NameID, Audience, and the attribute you expected to see. Then count Signature elements by parent, and count EncryptedAssertion. Those tell you whether the XML is still readable and whether a later failure is audience, destination, or encryption. Decoding does not validate a signature.

Is it safe to paste a SAML assertion into an online decoder?

Avoid pasting real SAML assertions into third-party online decoders unless you have verified that processing is entirely local and permitted by your organization's security policy. The safest troubleshooting workflow is to decode the assertion on the host that captured it. A captured assertion can contain sensitive identity and authorization data and may still be security-sensitive while it is valid. The X.509 certificate is public-key material, not a private key, but you can still leave it out of a ticket. Remove user values before you send a file for a destination or attribute question, and do not edit the original XML if someone still needs to validate the signature.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)