backend / authentication / sso / 11_python_libraries.md

Python Libraries for SSO

6 interview angles 7 min read source

Python Libraries for SSO

The Python SSO ecosystem. What to use for what; what to avoid; how the popular options integrate with FastAPI/Django/Flask.

OIDC / OAuth — pick one

Library Use case
Authlib the general-purpose default for OAuth 2.0 + OIDC clients
python-social-auth many providers (Google, GitHub, Facebook, …) — older, broader
mozilla-django-oidc Django + OIDC, opinionated
fastapi-sso FastAPI + many providers (Google, Microsoft, …)
flask-dance / authlib-flask Flask

For greenfield Python: Authlib. It supports OAuth 1, OAuth 2.0, OIDC, JWT, JWS/JWE in one library; integrations for FastAPI, Django, Flask, Starlette.

SAML

Library Notes
python3-saml the most-used SAML SP library; OneLogin-maintained; has had CVEs (keep updated)
pysaml2 more featureful, also more complex; supports IdP role too
django-saml2-auth Django integration on top of pysaml2

SAML libraries are not interchangeable. python3-saml is easier; pysaml2 is more flexible. Both work for SP usage.

Don’t hand-roll SAML. XML signature validation has too many subtle attack surfaces (canonicalization, XSW). Use a library.

Authlib — quickstart for OIDC

# pip install Authlib httpx

from authlib.integrations.starlette_client import OAuth

oauth = OAuth()
oauth.register(
    name="acme",
    server_metadata_url="https://acme.okta.com/.well-known/openid-configuration",
    client_id="...",
    client_secret="...",
    client_kwargs={"scope": "openid profile email"},
)

In a FastAPI app:

@app.get("/login")
async def login(request: Request):
    redirect_uri = request.url_for("auth_callback")
    return await oauth.acme.authorize_redirect(request, redirect_uri)

@app.get("/auth/callback")
async def auth_callback(request: Request):
    token = await oauth.acme.authorize_access_token(request)
    user = token.get("userinfo") or await oauth.acme.userinfo(token=token)
    request.session["user"] = dict(user)
    return RedirectResponse(url="/")

Authlib handles: state, nonce, PKCE (if configured), JWKS fetching, ID token validation. The library does the protocol; you do the app logic.

For PKCE:

oauth.register(
    name="acme",
    server_metadata_url="...",
    client_id="...",
    client_kwargs={"scope": "openid profile email", "code_challenge_method": "S256"},
)

Authlib — token validation manually

For APIs receiving JWT access tokens:

from authlib.jose import jwt, JsonWebKey

def validate_token(token: str) -> dict:
    jwks = JsonWebKey.import_key_set(httpx.get(JWKS_URL).json())
    claims = jwt.decode(
        token,
        key=jwks,
        claims_options={
            "iss": {"essential": True, "value": "https://idp.example.com"},
            "aud": {"essential": True, "value": "api.example.com"},
        },
    )
    claims.validate()       # checks exp, nbf
    return claims

claims.validate() checks exp (with default leeway) and nbf. The claims_options enforces iss and aud.

Cache the JWKS — don’t fetch on every request.

fastapi-sso — for common providers

If you’re connecting to one of the major providers (Google, Microsoft, etc.) and want minimum code:

# pip install fastapi-sso

from fastapi_sso.sso.google import GoogleSSO

google_sso = GoogleSSO(
    client_id="...",
    client_secret="...",
    redirect_uri="https://yourapp.com/google/callback",
)

@app.get("/google/login")
async def google_login():
    return await google_sso.get_login_redirect()

@app.get("/google/callback")
async def google_callback(request: Request):
    user = await google_sso.verify_and_process(request)
    # user.email, user.id, user.display_name, etc.

Comes with provider classes for Google, Microsoft, Facebook, GitHub, GitLab, Spotify, etc. Underneath, it’s the same OAuth code-flow logic.

python3-saml — basic SP setup

# pip install python3-saml

from onelogin.saml2.auth import OneLogin_Saml2_Auth

settings = {
    "strict": True,
    "debug": False,
    "sp": {
        "entityId": "https://yourapp.com/saml/metadata",
        "assertionConsumerService": {
            "url": "https://yourapp.com/saml/acs",
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
        },
        "x509cert": SP_CERT,
        "privateKey": SP_KEY,
    },
    "idp": {
        "entityId": "https://idp.example.com",
        "singleSignOnService": {
            "url": "https://idp.example.com/sso",
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
        },
        "x509cert": IDP_CERT,
    },
}

def saml_login(request):
    auth = OneLogin_Saml2_Auth(prepare_request(request), settings)
    return auth.login()        # returns redirect URL

def saml_acs(request):
    auth = OneLogin_Saml2_Auth(prepare_request(request), settings)
    auth.process_response()
    errors = auth.get_errors()
    if errors:
        raise ValueError(f"SAML errors: {errors}")
    if not auth.is_authenticated():
        raise NotAuthenticated()
    attributes = auth.get_attributes()
    nameid = auth.get_nameid()
    # create local session from attributes + nameid

prepare_request() transforms your framework’s request into the dict python3-saml expects. Examples in the docs for Flask, Django, FastAPI.

Keep python3-saml current — XSW-class CVEs have hit it; upgrade religiously.

Django + OIDC

# pip install mozilla-django-oidc

# settings.py
INSTALLED_APPS = [..., "mozilla_django_oidc"]

AUTHENTICATION_BACKENDS = (
    "mozilla_django_oidc.auth.OIDCAuthenticationBackend",
)

OIDC_RP_CLIENT_ID = os.environ["OIDC_CLIENT_ID"]
OIDC_RP_CLIENT_SECRET = os.environ["OIDC_CLIENT_SECRET"]
OIDC_OP_AUTHORIZATION_ENDPOINT = "https://idp.example.com/authorize"
OIDC_OP_TOKEN_ENDPOINT = "https://idp.example.com/token"
OIDC_OP_USER_ENDPOINT = "https://idp.example.com/userinfo"
OIDC_OP_JWKS_ENDPOINT = "https://idp.example.com/.well-known/jwks.json"
OIDC_RP_SIGN_ALGO = "RS256"

# urls.py
urlpatterns = [
    path("oidc/", include("mozilla_django_oidc.urls")),
    ...
]

Django’s LoginRequiredMixin and @login_required work; request.user is populated from the OIDC claims.

To customize user creation / role mapping, subclass OIDCAuthenticationBackend:

class CustomOIDCBackend(OIDCAuthenticationBackend):
    def create_user(self, claims):
        user = super().create_user(claims)
        user.first_name = claims.get("given_name", "")
        user.last_name = claims.get("family_name", "")
        groups = claims.get("groups", [])
        user.is_staff = "admins" in groups
        user.save()
        return user

    def update_user(self, user, claims):
        user.first_name = claims.get("given_name", "")
        # ... etc.
        user.save()
        return user

Managed services — when buying beats building

For B2B SaaS with multi-tenant SSO + SCIM:

Service What
WorkOS designed for B2B SaaS; SSO + SCIM + Directory Sync; SDK + admin UI for customer onboarding
Auth0 full IdP + connections to external IdPs; SCIM as add-on
AWS Cognito federation hub for AWS-based apps
Frontegg similar to WorkOS, B2B-focused
Stytch passwordless + SSO + SCIM
Okta CIC (Customer Identity Cloud) the Auth0 acquisition; same product
Clerk newer, focused on consumer + B2B with developer-friendly UX

These services handle:

  • Per-tenant IdP setup (admin UI for customers).
  • Protocol nuances (SAML XSW protection, OIDC compliance).
  • SCIM endpoints (so each customer’s IdP can sync users).
  • Logging, audit, anomaly detection.

Cost: typically $0.01–$0.10 per active user per month, or flat rate per tenant. For a SaaS at $50/user/mo, the math is usually favorable.

When to buy:

  • You’re hiring sales people, not auth engineers.
  • Multi-tenant SSO is required for enterprise deals.
  • Time-to-revenue matters more than vendor lock-in.

When to build:

  • One IdP, one customer (your own org).
  • Strong custom UX requirements.
  • Existing in-house identity expertise.

JWT-only libraries

If you just need to validate JWTs (not run the full OIDC flow):

Library Notes
PyJWT most common; install with [crypto] extra for asymmetric algorithms
python-jose broader (JWE/JWS); slower release pace
Authlib also handles JWTs; one library does everything
# pip install "PyJWT[crypto]"

import jwt

claims = jwt.decode(
    token,
    key=public_key,
    algorithms=["RS256"],
    audience="api.example.com",
    issuer="https://idp.example.com",
)

Pin algorithms to a fixed list. Never pass algorithms=None or trust the token’s alg field — see 08_attack_vectors.md.

Testing SSO locally

Two options:

Option How
Mock IdP run a fake IdP locally (e.g. dex, KeyCloak, Mock OAuth2 Server)
Production IdP with test app use Okta/Auth0 free tier; configure a test app pointing at localhost

Dex and Keycloak both run in Docker; preconfigure users/groups; treat as a real IdP in tests.

# docker-compose for Keycloak
services:
  keycloak:
    image: quay.io/keycloak/keycloak:latest
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    command: start-dev
    ports:
      - "8080:8080"

For unit tests, mock the OIDC flow at the library level — Authlib has helpers for this; or stub the HTTP calls.

Common pitfalls

  • PyJWT.decode(token, options={"verify_signature": False}) in dev, accidentally shipped to prod — accepts ANY token. Never disable signature verification.
  • Not pinning algorithms in JWT decode — alg confusion attacks.
  • Pinning to an outdated library version with known CVEs — keep python3-saml current.
  • Hardcoding endpoints instead of using discovery — breaks when IdP rotates URLs.
  • Storing client secrets in code — env vars / secret manager only.
  • One client_secret across environments (dev/staging/prod) — rotate per env.

Common interview confusions

  • “Authlib is just for OAuth.” — handles OAuth 1, OAuth 2.0, OIDC, JWT, JWS/JWE. One library for almost everything.
  • “python3-saml is the only SAML option.” — pysaml2 is the alternative; more featureful, also more complex. Pick based on needs.
  • “Buying SSO means giving up control.” — managed services expose configuration APIs and webhooks. You retain control of user data and authorization decisions; just the protocol mechanics are outsourced.

Interview angle

  • “What library would you use for OIDC in a FastAPI app?” — Authlib (authlib.integrations.starlette_client). Or fastapi-sso for major providers if you want less code. Validate ID tokens with authlib.jose or PyJWT pinned to RS256.
  • “What about SAML?” — python3-saml (most common) or pysaml2 (more featureful). Don’t hand-roll SAML — too many security pitfalls in XML signature validation.
  • “When would you use a managed SSO service like WorkOS or Auth0?” — when multi-tenant SSO + SCIM is required for enterprise sales and you’d otherwise spend ongoing dev time on per-IdP integration debugging. Common in B2B SaaS.
  • “How do you validate a JWT in Python?”jwt.decode(token, key=public_key, algorithms=["RS256"], audience=..., issuer=...). Always pin algorithms to a fixed list.
  • “How do you test SSO locally?” — run dex or Keycloak in Docker as a local IdP; or use a free Okta/Auth0 dev tenant with a test app pointing at localhost.
  • “What’s a common security mistake when validating JWTs in Python?” — passing algorithms=None or not passing it, which allows the token’s own alg field to control validation — leads to algorithm confusion attacks. Always pin.