Skip to content

SAML Authentication — Browser-Based Single Sign-On for Enterprise APIs

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about SAML Authentication. We cover key concepts, practical examples, and best practices to help you master this topic.

SAML (Security Assertion Markup Language) is an XML-based single sign-on protocol that allows users to authenticate with an identity provider and receive signed assertions that grant access to service providers.

What You'll Learn

SAML roles (IdP and SP), SAML assertion structure, SP-initiated and IdP-initiated SSO, exchanging SAML assertions for API tokens, and common SAML libraries for implementation.

Why It Matters

SAML is the dominant SSO protocol for enterprise applications. Supporting SAML authentication allows your API to integrate with corporate identity providers like Okta, Azure AD, and OneLogin.

Real-World Use

Salesforce, AWS, and Google Workspace support SAML SSO. Durga Antivirus Pro supports SAML for enterprise customers, allowing them to use their Okta or Azure AD for partner portal authentication.

sequenceDiagram
    participant User as User
    participant SP as Service Provider (Your API)
    participant IdP as Identity Provider (Okta/Azure AD)

    User->>SP: Access protected resource
    SP->>User: Redirect to IdP with SAML AuthnRequest
    User->>IdP: Authenticate (username/password + MFA)
    IdP->>User: POST SAML Response (signed assertion)
    User->>SP: POST assertion to ACS URL
    SP->>SP: Validate assertion signature
    SP->>SP: Extract user attributes (email, groups)
    SP->>User: Issue JWT token for API access

Code Example: SAML Authentication with Python

from flask import Flask, request, redirect, jsonify
from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.utils import OneLogin_Saml2_Utils
import jwt, datetime, secrets, os

app = Flask(__name__)
SECRET = os.environ.get("JWT_SECRET", "dev-secret")

# SAML configuration
SAML_CONFIG = {
    "strict": True,
    "debug": True,
    "sp": {
        "entityId": "https://api.durga-antivirus.com/saml/metadata",
        "assertionConsumerService": {
            "url": "https://api.durga-antivirus.com/api/auth/saml/acs",
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
        },
        "singleLogoutService": {
            "url": "https://api.durga-antivirus.com/api/auth/saml/sls",
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        },
        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
    },
    "idp": {
        "entityId": os.environ.get("SAML_IDP_ENTITY_ID"),
        "singleSignOnService": {
            "url": os.environ.get("SAML_SSO_URL"),
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        },
        "x509cert": os.environ.get("SAML_IDP_CERT", "").replace("\\n", "\n")
    }
}

def init_saml_auth(req):
    """Initialize SAML authentication from Flask request."""
    auth = OneLogin_Saml2_Auth(req, SAML_CONFIG)
    return auth

def prepare_flask_request(request):
    """Convert Flask request to format expected by python3-saml."""
    url_data = {
        "http_host": request.host,
        "server_port": request.host.split(":")[1] if ":" in request.host else "443",
        "script_name": request.path,
        "get_data": request.args.to_dict(),
        "post_data": request.form.to_dict(),
        "query_string": request.query_string.decode() if request.query_string else ""
    }
    return url_data

@app.route("/api/auth/saml/login")
def saml_login():
    """Initiate SP-initiated SAML SSO."""
    req = prepare_flask_request(request)
    auth = init_saml_auth(req)
    return redirect(auth.login())

Code Example: SAML Assertion Consumer Service

@app.route("/api/auth/saml/acs", methods=["POST"])
def saml_acs():
    """Assertion Consumer Service — receive SAML response."""
    req = prepare_flask_request(request)
    auth = init_saml_auth(req)

    # Process SAML response
    auth.process_response()
    errors = auth.get_errors()

    if errors:
        return jsonify({
            "error": "SAML authentication failed",
            "details": errors
        }), 401

    if not auth.is_authenticated():
        return jsonify({"error": "Not authenticated"}), 401

    # Extract user attributes from SAML assertion
    attributes = auth.get_attributes()
    name_id = auth.get_nameid()

    # Build user profile from SAML claims
    user_data = {
        "name_id": name_id,
        "email": attributes.get("email", [name_id])[0],
        "first_name": attributes.get("givenName", [""])[0],
        "last_name": attributes.get("sn", [""])[0],
        "groups": attributes.get("memberOf", []),
        "idp": SAML_CONFIG["idp"]["entityId"]
    }

    # Issue API token
    access_token = jwt.encode({
        "sub": user_data["email"],
        "name": f"{user_data['first_name']} {user_data['last_name']}".strip(),
        "groups": user_data["groups"],
        "auth_method": "saml",
        "iat": datetime.datetime.utcnow(),
        "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24)
    }, SECRET, algorithm="HS256")

    return jsonify({
        "access_token": access_token,
        "token_type": "Bearer",
        "expires_in": 86400,
        "user": {
            "email": user_data["email"],
            "groups": user_data["groups"]
        }
    })

Code Example: SAML Metadata and Logout

@app.route("/api/auth/saml/metadata")
def saml_metadata():
    """Return SAML SP metadata XML for IdP configuration."""
    req = prepare_flask_request(request)
    auth = init_saml_auth(req)
    metadata = auth.get_settings().get_sp_metadata()
    errors = auth.get_settings().validate_metadata(metadata)

    if errors:
        return jsonify({"error": "Invalid metadata", "details": errors}), 500

    return metadata, 200, {"Content-Type": "application/xml"}

@app.route("/api/auth/saml/sls", methods=["GET", "POST"])
def saml_sls():
    """Single Logout Service."""
    req = prepare_flask_request(request)
    auth = init_saml_auth(req)

    url = auth.process_slo(delete_session_cb=lambda: clear_session())
    errors = auth.get_errors()

    if url:
        return redirect(url)

    return jsonify({"message": "Logged out"})

def clear_session():
    """Clear user session on SAML logout."""
    # Invalidate any active tokens for this session
    pass

# API token exchange for SAML-authenticated requests
@app.route("/api/auth/saml/exchange", methods=["POST"])
def exchange_saml_token():
    """Exchange a SAML assertion for a scoped API token."""
    assertion = request.json.get("saml_assertion")
    requested_scopes = request.json.get("scopes", [])
    saml_settings = OneLogin_Saml2_Settings(SAML_CONFIG)

    # Validate and decrypt assertion
    response = OneLogin_Saml2_Response(
        saml_settings, assertion
    )

    if not response.is_valid():
        return jsonify({"error": "Invalid SAML assertion"}), 401

    # Extract attributes and issue scoped token
    attributes = response.get_attributes()
    user_groups = attributes.get("memberOf", [])

    # Map SAML groups to API scopes
    allowed_scopes = map_groups_to_scopes(user_groups)
    effective_scopes = [s for s in requested_scopes if s in allowed_scopes]

    token = jwt.encode({
        "sub": response.get_nameid(),
        "scopes": effective_scopes,
        "auth_method": "saml_exchange",
        "iat": datetime.datetime.utcnow(),
        "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
    }, SECRET, algorithm="HS256")

    return jsonify({"access_token": token, "scopes": effective_scopes})

Common Mistakes

1. Not Validating the SAML Response Signature

SAML responses must be signed by the IdP. Without signature validation, an attacker can forge assertions. Always verify the XML signature against the IdP's certificate.

2. Ignoring the NotOnOrAfter Condition

SAML assertions include a validity window. Tokens presented before or after the window must be rejected. Always validate timestamps.

3. Not Handling RelayState

The RelayState parameter preserves the user's original request URL. Failing to redirect after successful authentication breaks the user flow.

4. SAML Response Replay

SAML responses should include unique IDs and timestamps. Track used assertion IDs to prevent replay attacks.

5. Confusing SAML with OAuth2

SAML is browser-based SSO using XML assertions. OAuth2 uses JSON tokens. For API-to-API auth, prefer OAuth2. For browser SSO, SAML is appropriate.

Practice Questions

  1. What are the two main roles in SAML?
  2. How does SP-initiated SAML SSO differ from IdP-initiated?
  3. Why must SAML assertions be signed?
  4. What is the purpose of the ACS (Assertion Consumer Service) URL?
  5. How do you map SAML group attributes to API scopes?

Answers:

  1. The Identity Provider (IdP) authenticates users and issues assertions. The Service Provider (SP) consumes assertions to grant access.
  2. SP-initiated: the user tries to access the SP first, which redirects to the IdP. IdP-initiated: the user starts at the IdP portal and clicks the application.
  3. The signature ensures the assertion was issued by the trusted IdP and has not been modified. Without it, an attacker can forge access.
  4. The ACS URL is where the IdP POSTs the SAML response. The SP receives the assertion at this endpoint and processes authentication.
  5. Create a mapping table in your application: SAML group "VPN-Users" maps to scope "vpn:read", group "VPN-Admins" maps to "vpn:write". Apply during token issuance.

Challenge: Build a SAML SP that integrates with a SAML test IdP (like samltest.id), processes assertions, maps group attributes to API scopes, and issues JWTs for API access.

FAQ

Is SAML better than OAuth2?

They serve different purposes. SAML is designed for browser-based enterprise SSO. OAuth2 is designed for delegated API access. Many organizations use both.

Can SAML be used for API authentication?

Indirectly. SAML authenticates the user in the browser. The resulting token or assertion can be exchanged for API tokens.

What is the difference between SAML and OpenID Connect?

SAML uses XML assertions and is designed for enterprise SSO. OpenID Connect uses JWT and is built on OAuth2 for modern web and mobile apps.

How do I handle SAML logout?

Implement Single Logout (SLO). The SP sends a LogoutRequest to the IdP, which logs the user out of all connected applications.

What libraries should I use for SAML?

Python: python3-saml (OneLogin). Node: passport-saml. Java: OpenSAML. Ruby: ruby-saml. Choose a library that is actively maintained.

How do I test SAML integration?

Use samltest.id for a free test IdP, or run a local Keycloak instance as the IdP. Test both SP-initiated and IdP-initiated flows.

Mini Project

Build a SAML SP that integrates with a test IdP (Keycloak or samltest.id), processes SAML assertions, maps group attributes to API scopes, issues JWTs, and provides metadata XML for IdP configuration.

What's Next

Now learn about Certificate-Based Authentication with mTLS for mutual TLS authentication between services.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro