Volley / Templates / SAML single sign-on

Sign-in template

SAML single sign-on sequence diagram

SP-initiated SAML 2.0 sign-in with the decoded AuthnRequest, the self-submitting form, the post to your ACS URL and every check the assertion has to pass.

Numbered sequence diagram of SAML 2.0 SP-initiated single sign-on across User, Browser, App and Identity Provider: redirect with an AuthnRequest, sign-in, an auto-submitting form, POST to the ACS URL, assertion validation, and a session or 403.

Click any arrow in the editor to see its request and response as a table or the raw body. Change the hosts, ids and fields to match your integration, then share a link or embed it in your README.

Step by step

  1. The app sees no session and redirects to the identity provider with a deflated, base64 SAMLRequest and a RelayState.
  2. The user signs in at the identity provider, with MFA if it requires it.
  3. The identity provider returns a form that posts itself to your Assertion Consumer Service URL, carrying the signed SAMLResponse.
  4. Your app validates the assertion: signature, issuer, audience, destination, InResponseTo and the time window.
  5. Your app starts a session and redirects to the RelayState, or answers 403.

Easy to get wrong

  • Check that the assertion itself is signed, not only the outer response, unless your library handles both.
  • Store the request id and reject assertions whose InResponseTo you did not issue, to stop replays.
  • Session cookies set on the cross-site POST need SameSite settings that allow it, or set the cookie on the redirect that follows.

The source

The whole diagram is this plain text. Copy it into your repo, or open it in the editor.

saml-sso.volley
title: SAML 2.0 single sign-on, started by the app
autonumber
order: User, Browser, App, Identity Provider

User -> Browser: Open app.example.com/dashboard
Browser -> App: GET /dashboard
note App: No session, so the app starts SAML sign-in

App --> Browser: 302 to the identity provider
out: Redirect
  Location: https://idp.example.com/sso/saml?SAMLRequest=fZJNT8MwDIb%2FSuV7m6Zs&RelayState=%2Fdashboard

Browser -> Identity Provider: GET /sso/saml
in: AuthnRequest (deflated and base64 encoded in the query, decoded here)
```text
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    ID="_a1b2c3d4" Version="2.0" IssueInstant="2026-10-16T14:00:00Z"
    AssertionConsumerServiceURL="https://app.example.com/saml/acs">
  <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://app.example.com/saml/metadata</saml:Issuer>
</samlp:AuthnRequest>
```

Identity Provider --> Browser: Sign-in page

Browser -> Identity Provider: POST /login
in: Credentials
  username: ada@example.com
  password: <redacted>
...: {fas-user-lock} Multi-factor check, if the IdP requires it

Identity Provider --> Browser: 200 OK, a form that posts itself
out: Auto-submitting form
```text
<form method="post" action="https://app.example.com/saml/acs">
  <input type="hidden" name="SAMLResponse" value="PHNhbWxwOlJlc3BvbnNlIHhtbG5z">
  <input type="hidden" name="RelayState" value="/dashboard">
</form>
<script>document.forms[0].submit()</script>
```

Browser -> App: POST /saml/acs
in: Form
  SAMLResponse: PHNhbWxwOlJlc3BvbnNlIHhtbG5z
  RelayState: /dashboard

App -> App: Validate the assertion
in: Assertion (decoded)
  Issuer: https://idp.example.com
  NameID: ada@example.com
  Audience: https://app.example.com/saml/metadata
  Destination: https://app.example.com/saml/acs
  InResponseTo: _a1b2c3d4
  NotOnOrAfter: 2026-10-16T14:05:00Z
  groups: engineering
note: Check the signature against the IdP certificate, then the audience, destination, `InResponseTo` and time window. A replayed or unsigned assertion must fail.

if: Assertion is valid
  App --> Browser: 302 to /dashboard
  out: Response headers
    Location: /dashboard
    Set-Cookie: session=<redacted>; HttpOnly; Secure; SameSite=Lax
else: Invalid, expired or not for this app
  App --> Browser: 403 Forbidden
end

Related

Make this SAML single sign-on diagram yours.

Open it in the editor, change it to match your integration and share the link. Free, with no account.