Proteles

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:

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.

The three parties in SAML, and the trust between them The identity provider and the service provider exchange metadata once, in advance: entity IDs, endpoints and the signing certificate. With the browser bindings, they do not talk directly during a sign-in. The person's browser carries the signed assertion from the identity provider and posts it to the service provider's ACS URL, and it can read or alter anything it carries. Identity provider your customer’s directory; holds the private signing key Service provider your application; holds the IdP’s certificate set up once, in advance metadata: entity IDs, endpoints, certificate The person’s browser carries every message, and can read or alter any of them signed assertion posted to the ACS URL
Trust comes from metadata exchanged once, in advance. With the browser bindings, every message in a sign-in travels through the browser, so every message has to protect itself.

How a sign-in works

The common case starts at your application, and is called SP-initiated sign-in:

  1. The person opens your application and chooses to sign in with their company account.
  2. The service provider creates an AuthnRequest (“please authenticate someone and send the answer to my ACS URL”) with a fresh ID it remembers.
  3. 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).
  4. 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.
  5. The identity provider returns a Response containing a signed Assertion, in a page whose form posts itself to the ACS URL.
  6. The browser delivers it.
  7. 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.
  8. 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.

An SP-initiated SAML sign-in, step by step Three lanes: service provider, browser and identity provider. 1: the browser opens the application and picks single sign-on. 2: the service provider sends an AuthnRequest with a fresh ID and its ACS URL. 3: the browser carries it to the identity provider by HTTP-Redirect or HTTP-POST. 4: the person signs in with a password, MFA or an existing session. 5: the identity provider returns a signed Response in a form that posts itself. 6: the browser posts it to the ACS URL, with the RelayState if the request carried one. 7: the service provider validates it and starts a session. 8: the person is signed in. Service provider your application Browser the courier Identity provider your customer’s directory 1 Opens the app, picks SSO 2 Sends an AuthnRequest fresh ID; reply-to ACS URL 3 Carries it to the IdP HTTP-Redirect or HTTP-POST 4 The person signs in password, MFA, or a session 5 Returns a signed Response a form that posts itself 6 Posts it to the ACS URL SAMLResponse, any RelayState 7 Validates it, then starts a session 8 The person is signed in
SP-initiated sign-in. Steps 5 and 6 carry the signed response through the browser; step 7 is where a service provider earns its keep.

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.

The anatomy of a SAML Response and its Assertion Nested boxes show a Response containing an Issuer, a Status and an Assertion. The Assertion contains an Issuer, a Signature, a Subject with a NameID and a SubjectConfirmation, Conditions with an AudienceRestriction, an AuthnStatement and an AttributeStatement. Notes beside each part say what the service provider checks: the Destination, and InResponseTo if the service provider sent a request; that both Issuers are the configured identity provider; that success is read from the top-level status code only; that the assertion ID is recorded so it can be used once; that the signature verifies with the configured certificate; that the identity is read only from signed bytes; that the SubjectConfirmationData inside the SubjectConfirmation names your ACS URL as its Recipient, expires soon, and answers your request if there was one; that the time window fits and the AudienceRestriction holds an Audience naming you; that SessionNotOnOrAfter caps the session; and that attributes are only as trustworthy as the directory. <Response> <Issuer> <Status> <Assertion> <Issuer> <Signature> <Subject> <NameID> <SubjectConfirmation> <Conditions> <AudienceRestriction> <AuthnStatement> <AttributeStatement> Destination: your ACS URL. InResponseTo: your request, if any. Issuer, here and in the assertion: the IdP set up for this connection. Success, read from the top-level status code only. Its ID is recorded, so the assertion can be used once. Verified with the certificate you configured, never a key it carries. Who: taken only from signed bytes. Its SubjectConfirmationData: Recipient: your ACS URL. NotOnOrAfter: soon. InResponseTo: yours, if any. Only between NotBefore and NotOnOrAfter, and only for an Audience that names you. When and how they signed in; SessionNotOnOrAfter caps the session. Email, name, groups: only as trustworthy as the directory.
A Response and its Assertion. Each note is a check the service provider makes before it believes the part beside it.

SAML and OpenID Connect

OpenID Connect solves the same problem with newer building blocks.

SAML 2.0OpenID Connect
Standardised2005, by OASIS2014, by the OpenID Foundation
Built onXML and XML SignatureOAuth 2.0, JSON, and signed JSON Web Tokens
How messages travelMostly through the browser: redirects and self-submitting formsA browser redirect, then a direct request from the application to the provider for tokens
What gets verifiedA signature over part of an XML documentA signature over a whole, compact token
Best suited toBrowser sign-in to web applicationsWeb, 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).

Signature wrapping: what was signed is not what was read A Response contains two assertions. The first, with ID evil, names ceo@customer.example and has no signature; the attacker wrote it. The second, with ID a1, sits inside an Extensions element, names attacker@customer.example and carries a genuine signature whose reference points to a1. The signature check looks up a1 wherever it is, recomputes its digest and reports a valid signature. The application reads the first assertion it finds and signs in the CEO. Both checks pass, and nothing ties what was verified to what was read. <Response> <Assertion ID="evil"> <NameID>ceo@customer.example no signature; the attacker wrote it <Extensions> <Assertion ID="a1"> <NameID>attacker@customer.example <Signature> Reference URI="#a1" digest matches: genuine The application reads the first assertion it finds, and signs in the CEO. None of it was signed. The signature check looks up #a1 wherever it is, recomputes its digest, and reports a valid signature. Both checks pass. Nothing ties what was verified to what was read.
Signature wrapping. The signature check and the identity lookup each work correctly, on different elements.

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.

Proteles as a bridge from SAML to OpenID Connect Your customer's identity provider sends a signed SAML assertion to your Proteles workspace. Proteles validates the assertion, applies your MFA policy and starts a session, then signs people in to your web, single-page, mobile and API applications with OpenID Connect tokens. Your customer’s identity provider where their people and policies live Proteles your workspace validates the assertion, applies your MFA policy, starts a session Your applications web, single-page, mobile and APIs SAML assertion OIDC tokens
SAML between your customer’s identity provider and Proteles; OpenID Connect between Proteles and your applications. Your applications speak one protocol, whatever each customer runs.

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

What it bounds and refuses

Your workspace, and your policies

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.

Talk to us See the application quickstarts