What is SAML? How SAML single sign-on works
SAML, the Security Assertion Markup Language, is an open standard that lets a company’s identity provider tell an application who a person is, in a signed XML message, so people sign in once with their company account instead of keeping a password for every application. If you sell software to larger companies, someone will ask whether you support it. This guide explains how a SAML sign-in works, why SAML is easy to get subtly wrong, and how we specified, built and tested the Proteles implementation.
Why SAML still matters
Most companies past a certain size run their own identity provider: Okta, Microsoft Entra ID, Active Directory Federation Services, Google Workspace or another. It is where their IT team decides who works there, which applications each person may use, and how people prove who they are. When someone leaves, one account is switched off and they can no longer sign in anywhere.
That is why “SAML SSO” turns up on security questionnaires and procurement checklists. It means your product joins the applications the customer controls from one place, and there is no separate password for you to store, reset or forget to revoke.
SAML 2.0 is not new: OASIS approved it in 2005, nine years before OpenID Connect. It is still what much of enterprise IT speaks, so the question is not whether to support it, but how to do so without inheriting its sharp edges.
The three parties, and the trust between them
Every SAML sign-in involves three parties:
- The person, and their browser, wanting to use your application.
- The identity provider (IdP), the customer’s system that knows who that person is. It makes statements about people, called assertions, and signs them with a private key only it holds.
- The service provider (SP): your application, or the service that signs people in to it. It receives assertions and decides whether to believe them.
With the usual browser bindings, HTTP-Redirect and HTTP-POST, the identity provider and the service provider do not message each other during a sign-in. Everything travels through the browser: a redirect one way, a form that submits itself the other. The browser is a courier, and whoever controls it, including an attacker using their own, can read every message and change any byte. (A third binding, HTTP-Artifact, adds a direct back channel between the two; Proteles does not use it.)
So trust comes from an exchange made ahead of time, usually as XML documents called metadata. The service provider publishes its entity ID, a unique name that is usually a URL, and its Assertion Consumer Service (ACS) URL, where assertions must be delivered. The identity provider publishes its own entity ID, the URL where sign-ins start, and the certificate for the key that signs its assertions. After that, an assertion is believable only if that key signed it, it names that identity provider as its issuer, and it is addressed to that service provider.
How a sign-in works
The common case starts at your application, and is called SP-initiated sign-in:
- The person opens your application and chooses to sign in with their company account.
- The service provider creates an
AuthnRequest(“please authenticate someone and send the answer to my ACS URL”) with a fresh ID it remembers. - The browser carries the request to the identity provider, in a redirect’s URL (the HTTP-Redirect binding) or in a form that submits itself (HTTP-POST).
- The identity provider authenticates the person however it normally does: a password, a second factor, a passkey or an existing session. Your application sees none of it.
- The identity provider returns a
Responsecontaining a signedAssertion, in a page whose form posts itself to the ACS URL. - The browser delivers it.
- The service provider checks the signature, the issuer, the audience, the recipient, the time window, which request it answers and whether it has been seen before. Only then does it start a session.
- The person is signed in.
A RelayState value can travel with the request and come back
with the response, typically to remember where to send the person afterwards.
The assertion’s signature does not cover it, so a careful service provider
treats it as untrusted, or does not rely on it at all.
IdP-initiated sign-in, and why it is riskier
A sign-in can also start at the identity provider, when the person clicks your application’s tile in their company’s app portal and the identity provider sends a response nobody asked for. It is convenient, and it removes a defence: with no request, there is no request ID to answer and nothing tying the response to the browser that delivers it. An attacker holding a valid assertion for their own account can have a victim’s browser deliver it, and the victim ends up working in the attacker’s account without noticing. This is login cross-site request forgery, and it is why IdP-initiated sign-in should be a deliberate choice, made per connection.
What an assertion says
A SAML Response is an XML document with an Assertion
inside. The Issuer says who is speaking, and the
Signature proves it. The Subject says who the assertion
is about: a NameID, and a SubjectConfirmation whose
SubjectConfirmationData says where the assertion may be delivered
(Recipient) and until when (NotOnOrAfter). Conditions give a validity window and,
in an AudienceRestriction, the service provider it was issued for.
The AuthnStatement says when and how the person signed in, and may
carry SessionNotOnOrAfter, the latest the resulting session should
last. The AttributeStatement carries facts such as email, name or
groups, only as trustworthy as the directory they came from.
Every part is something the service provider has to check, not merely read,
and the notes in the figure say how. One check depends on how the sign-in
started: a response to the service provider’s request must answer that request
(InResponseTo), and a response nobody asked for must not claim to
answer one.
SAML and OpenID Connect
OpenID Connect solves the same problem with newer building blocks.
| SAML 2.0 | OpenID Connect | |
|---|---|---|
| Standardised | 2005, by OASIS | 2014, by the OpenID Foundation |
| Built on | XML and XML Signature | OAuth 2.0, JSON, and signed JSON Web Tokens |
| How messages travel | Mostly through the browser: redirects and self-submitting forms | A browser redirect, then a direct request from the application to the provider for tokens |
| What gets verified | A signature over part of an XML document | A signature over a whole, compact token |
| Best suited to | Browser sign-in to web applications | Web, single-page and mobile applications, and APIs |
Why SAML is hard to get right
Describing SAML is easy; implementing a service provider safely is not. XML Signature signs part of a document, and a service provider is safe only if the part it reads is exactly the part that was signed. The most damaging SAML vulnerabilities are ways for those two to come apart. They are logic errors around correctly signed documents, rarely broken cryptography.
Signature wrapping
An attacker takes a genuinely signed assertion, perhaps issued to their own account, and moves it somewhere the application does not read. Where the application does look, they insert a forged, unsigned assertion naming someone else. A verifier that finds the signed element by its ID, anywhere in the document, recomputes its hash (the digest the signature covers), reports a valid signature, and the application reads the forgery. A 2012 study, On Breaking SAML (USENIX Security), found this class in 11 of the 14 major SAML frameworks it examined, and it keeps returning: a widely used Ruby library fixed one in 2024 (CVE-2024-45409), and two more in 2025 that came from two XML parsers disagreeing about the same document (CVE-2025-25291 and CVE-2025-25292).
Comments that shorten a name
Canonicalisation, which normalises XML before it is hashed, usually drops
comments. An attacker who can register
alice@example.com.attacker.test at the identity provider gets a
genuine assertion for that account, then inserts a comment:
alice@example.com<!---->.attacker.test. The signature still
verifies, and a library that returns only the text before the comment reads
alice@example.com. In 2018, researchers at Duo disclosed this flaw
in several open-source SAML libraries at once
(CVE-2017-11427
and related).
Replay, misdirection and the parser
A valid assertion is a bearer credential for a few minutes, and one copied from a log, a proxy or a shared computer can be presented again. An assertion issued to one service provider must not work at another, including one an attacker runs or, in a multi-tenant product, another customer’s. XML also lets a document declare entities, and an unguarded parser can be made to read local files, make network requests or exhaust its memory before any signature is checked. The rest is accepting too much: a missing signature treated as nothing to check, a certificate trusted because it arrived inside the message, SHA-1 or shared-secret signatures, or transforms (processing steps a signature can ask the verifier to apply before hashing) that make the verifier fetch URLs or run programs.
Where Proteles fits: SAML in, OpenID Connect out
Proteles is an OAuth 2.0 and OpenID Connect provider. With SAML enabled for a workspace, it is also a SAML service provider for that workspace: your customer’s identity provider signs their people in to Proteles over SAML, and Proteles validates the assertion and finishes the sign-in through the same function as password and social sign-in. Your applications never parse XML: each one sends a customer’s people to that connection’s Proteles sign-in address, and gets back the OpenID Connect tokens it already handles.
An administrator sets up each connection through the Management API or the TypeScript SDK, entering the identity provider’s entity ID, sign-in URL and signing certificate. Proteles publishes service-provider metadata for the connection, and the ACS URL it advertises is the same stored value the server enforces.
How Proteles specified, built and reviewed it
A testable specification first. Before writing the service provider, we wrote down what one must refuse, as rules derived from the OASIS SAML 2.0 standards, each phrased so it can be tested: reject this, when that. Its main list, completed on 8 September 2026, holds 245 rules in six areas (signatures and keys; subject, audience and recipient; replay and correlation; status and the parser; conditions and timing; metadata and trust), 104 of them marked as authentication bypasses: omit the rule, and an attacker can sign in as someone else.
An attack on the specification. Each area was then given to an AI review agent told to assume an implementation did exactly those checks and nothing more, and to find a way through. That pass found 35 gaps, 12 of them authentication bypasses, and the specification records a rule to close each one, 280 rules in all. One gap was a genuine signed assertion accompanied by an unsigned decoy identity in an extensions block; it is now a named test.
Citations, checked and unchecked. 142 of the 245 rules name the OASIS passage they come from, checked against that text when the specification was written. The other 103 are labelled “recalled, not re-read”, and most of them carry no citation yet.
Code that cites its rules. Checks in the code name the rule they implement, so a reviewer can go from a line of code to the requirement behind it. The specification is a yardstick, not a claim that every rule is met: most of its metadata area covers importing an identity provider’s metadata automatically, which Proteles does not do yet, and deliberate departures from a rule are recorded.
An adversarial review. On 11 September 2026 the service provider went through a structured internal review of five surfaces (signature wrapping, replay and correlation, the HTTP boundary, identity resolution, and completeness against the specification), run by AI review agents under our direction. A separate verifier agent confirmed each higher-severity finding, and every confirmed finding was fixed; lower-severity findings were recorded without that check. Some checks described below, such as the session deadline and the canonicalisation budget, were added after it. It was our own review, not a third-party audit or penetration test.
Tests checked by breaking the code. When we add a test for a security property, we routinely plant the fault it should catch and confirm the test fails. That is done by hand, on individual changes, not by an automated suite, and it has changed the tests: accepting an assertion at the exact instant it expires once survived every test, so that boundary now has its own.
What Proteles checks on every sign-in
This is the service provider as it runs today, as of 25 September 2026.
What it verifies
- Only your certificate counts. Every response needs a valid signature from the certificate your administrator entered, on the assertion or on the whole response; one with neither is refused, and keys carried in the message are ignored. The key must be RSA of at least 2048 bits or ECDSA on P-256, P-384 or P-521, checked at setup and again at every sign-in.
- Identity comes only from the signed bytes. The verifier passes on only the canonicalised bytes of the assertion the signature covered, and nothing else, and each later step accepts only the previous step’s output, so none can be skipped. Tests confirm that when the assertion itself is signed, those bytes hash to the signed digest, and that a decoy in an unsigned extensions block, or an assertion outside the signed element, is never read. Signature wrapping depends on the verifier and the reader looking at different elements; here the reader only ever sees bytes from inside the element the verifier checked, so there is no second lookup to fool.
- Structure, never search. A response in which any ID appears twice is refused, elements are matched by namespace and name only where the protocol puts them, and success is read from the top-level status code alone.
- Addressed to this workspace. Every audience restriction
must name this service provider. A single bearer subject confirmation (its
SubjectConfirmationData, described earlier) must match the recipient, the expiry and the request all at once; they cannot be satisfied piecemeal across several. Recipient and destination are compared with the ACS URL from the connection’s configuration, never one rebuilt from request headers, and the issuer must be this connection’s identity provider. - Strict time. An assertion is refused before its
NotBeforeand from itsNotOnOrAfteronwards, give or take a clock tolerance of 60 seconds by default and never more than five minutes. A timestamp that does not parse is a refusal. - Our request, the same browser. Each sign-in gets a
128-bit random request ID, answerable for ten minutes, and a 256-bit secret in
a
__Host-cookie that isHttpOnlyandSecure, stored only as a SHA-256 hash. A response delivered by another browser is refused without using up the real person’s request, which blocks login forgery on SP-initiated sign-in. The ACS accepts only POST, and the destination after sign-in is fixed when the sign-in starts and held on the server. - No shortened names. Identity is read from canonicalised
bytes with comments already removed, so a
NameIDis the whole signed value, and one with surrounding spaces is refused, not trimmed.
What it bounds and refuses
- Work before authentication. A response may be 1 MiB encoded and 512 KiB decoded, with at most four signatures of one reference each, and canonicalisation has a fixed budget. A Proteles filter runs before the signature library, allows only same-document references and the standard transforms, and refuses any message that asks the verifier to fetch a URL, read a file or run a transform such as XSLT. The SAML routes are rate-limited per client address, 120 requests a minute by default and adjustable by the operator; people who share one outbound address share that budget.
- DTDs and entities, refused outright on every parse.
- Weak algorithms. Digests must be SHA-256 or stronger. SHA-1, MD5, DSA and shared-secret HMAC signatures are refused, as are comment-keeping canonicalisation and unstated algorithms. An identity provider still signing with SHA-1 has to be reconfigured.
- Replays, across every server. One atomic database write claims each assertion ID, and if the replay store cannot answer, the sign-in is refused. An assertion that would stay valid for longer than the connection allows after it arrives, ten minutes by default, is refused outright.
- IdP-initiated sign-in, unless you choose it. It is off for every connection by default. Turned on, each response still works only once, but the login-forgery risk comes with it.
- Takeover by email address. An address asserted by an identity provider is stored as unverified and never links a SAML sign-in to an existing account. Accounts are keyed by workspace, identity provider and subject.
- Telling strangers why. Every response the service provider refuses gets the same redirect; the reason goes to the server log, not to the browser.
Your workspace, and your policies
- Off unless switched on. SAML is disabled for every
workspace by default, only the operator of the Proteles deployment can enable
it (on the managed service, that is us), and each connection must also be
enabled by the workspace’s administrator. Both switches are checked before any
XML is parsed or any signature checked, so a workspace without SAML never
reaches the XML parser.
Configuration needs its own
saml:readandsaml:writeAPI scopes, and one workspace’s connections, replay records and users are invisible to every other. - Your MFA policy still applies. A SAML sign-in finishes in the same function as password and social sign-in, so a workspace that requires a second factor asks SAML users for one, and deactivated accounts are refused. Proteles does not yet read how the identity provider authenticated the person, so those users complete a Proteles second factor as well.
- The identity provider’s deadline holds. If an assertion
carries
SessionNotOnOrAfter, the Proteles session ends then, and so do the access and refresh tokens it issues to your applications.
How it is tested
The service provider’s sign-in path has 181 automated tests. Fifteen are end to end: each run generates an identity-provider key, signs real assertions, posts them through the real HTTP routes and inspects the resulting session, cookies and OpenID Connect tokens, using the real signature verifier and the in-memory store. Twenty-one run against Postgres, including races in which 16 callers compete for one assertion or request and exactly one may win. Refusal tests sit beside cases that must succeed, so a verifier that refused everything could not pass. Not there yet: fuzzing, an automated mutation suite, and any run against a live identity provider such as Okta or Entra ID; every response in the tests is one the tests signed themselves.
What is next
Turning it on
SAML is off for every workspace until the operator of the deployment switches it on (on the managed service, that is us), and Proteles is in preview. It has not yet been tried against a live Okta, Entra ID, ADFS or Google Workspace tenant. If your customers are asking for SAML single sign-on, write to us with what they run and what they need, and we will talk it through.