Understanding Single Sign-On Authentication Flows
How SAML 2.0 and OpenID Connect enable users to authenticate once across multiple applications using identity provider assertions.
Introduction
Single Sign-On (SSO) allows users to authenticate once — with their company’s identity provider (Okta, Azure Entra ID, Google Workspace) — and then access multiple applications without re-entering credentials. From a user’s perspective, they log in to one place and everything else “just works.” From a security perspective, SSO centralizes authentication: credentials are managed in one place, MFA is enforced uniformly, and account deprovisioning (when an employee leaves) immediately revokes access to all connected applications.
Two protocols dominate enterprise SSO: SAML 2.0 (XML-based, older, common in enterprise SaaS) and OpenID Connect (OIDC) (JSON/JWT-based, modern, preferred for web and mobile apps).
SAML 2.0 SSO Flow (SP-Initiated)
flowchart TD
A["1. User Accesses Service Provider"]
B["2. SP Redirects to Identity Provider"]
C["3. User Authenticates with IdP"]
D["4. IdP Posts SAML Assertion to SP"]
E["5. SP Validates and Creates Session"]
A --> B
B --> C
C --> D
D --> E
Step 1: User Accesses Service Provider
User navigates to: https://app.example.com/dashboard (Service Provider)
SP checks: No active session
SP generates: AuthnRequest XML
Step 2: SP Redirects to Identity Provider
HTTP 302 Redirect to:
https://sso.company.com/saml/sso?
SAMLRequest=base64(deflate(AuthnRequest_XML))&
RelayState=https://app.example.com/dashboard&
SigAlg=http://www.w3.org/2001/04/xmldsig-more#rsa-sha256&
Signature=...
AuthnRequest XML (decoded):
<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="_abc123"
Version="2.0"
IssueInstant="2024-03-15T14:00:00Z"
Destination="https://sso.company.com/saml/sso"
AssertionConsumerServiceURL="https://app.example.com/saml/acs">
<saml:Issuer>https://app.example.com/saml/metadata</saml:Issuer>
</samlp:AuthnRequest>
Step 3: User Authenticates with IdP
User sees: Company SSO login page (Okta/Entra ID)
If already logged in via prior SSO session: skip (no password required)
If not: Enter corporate credentials + MFA
IdP validates credentials, checks user is allowed to access this SP
Step 4: IdP Posts SAML Assertion to SP
<!-- IdP POSTs this to app.example.com/saml/acs -->
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<saml:Assertion>
<saml:Issuer>https://sso.company.com</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
[email protected]
</saml:NameID>
</saml:Subject>
<saml:Conditions
NotBefore="2024-03-15T14:00:00Z"
NotOnOrAfter="2024-03-15T14:05:00Z"> <!-- 5-minute validity window -->
<saml:AudienceRestriction>
<saml:Audience>https://app.example.com/saml/metadata</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AttributeStatement>
<saml:Attribute Name="email">
<saml:AttributeValue>[email protected]</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="groups">
<saml:AttributeValue>engineering</saml:AttributeValue>
<saml:AttributeValue>admin</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
<saml:Signature><!-- XML Digital Signature (RSA-SHA256) --></saml:Signature>
</saml:Assertion>
</samlp:Response>
Step 5: SP Validates and Creates Session
from onelogin.saml2.auth import OneLogin_Saml2_Auth
def saml_acs(request):
auth = OneLogin_Saml2_Auth(request, custom_base_path="/saml/settings")
auth.process_response()
if auth.is_authenticated():
attributes = auth.get_attributes()
email = attributes.get('email', [None])[0]
groups = attributes.get('groups', [])
# Create application session
session['user'] = email
session['groups'] = groups
return redirect(session.get('RelayState', '/dashboard'))
else:
errors = auth.get_errors()
return render_error(errors)
OpenID Connect (OIDC) SSO Flow
OIDC is OAuth 2.0 + an identity layer — the same flow as OAuth but with an additional ID token (JWT) containing user identity claims:
# Using authlib for OIDC in Flask
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
oauth.register(
name='company_sso',
server_metadata_url='https://sso.company.com/.well-known/openid-configuration',
client_id='CLIENT_ID',
client_secret='CLIENT_SECRET',
client_kwargs={'scope': 'openid email profile groups'}
)
@app.route('/login')
def login():
redirect_uri = url_for('authorize', _external=True)
return oauth.company_sso.authorize_redirect(redirect_uri)
@app.route('/authorize')
def authorize():
token = oauth.company_sso.authorize_access_token()
user_info = token['userinfo'] # Decoded ID token claims
session['user'] = user_info['email']
session['groups'] = user_info.get('groups', [])
return redirect('/')
Key Takeaways
- SAML 2.0 uses XML assertions and browser POST — common in enterprise SaaS (Salesforce, Workday). Complex but mature.
- OIDC uses JWT ID tokens and JSON — preferred for modern web/mobile apps. Simpler, more developer-friendly.
- SSO provides centralized authentication — MFA enforcement, session policies, and deprovisioning apply uniformly across all connected apps.
- Audience restriction in SAML / audience claim in OIDC prevents an assertion/token issued for App A from being replayed at App B.
- IdPs maintain an SSO session cookie — subsequent SP logins skip credential entry if the IdP session is valid (the “single” in single sign-on).
Historical figures, architectures, and capabilities are for informational purposes only. Not technical, professional, legal, or financial advice. Sources: Research papers, developer documentation.