Backend / Authentication / SSO / 11_python_libraries.md

Python Libraries for SSO

Updated 6 interview angles 7 min read source
On this page14
  1. OIDC / OAuth — pick one
  2. SAML
  3. Authlib — quickstart for OIDC
  4. Authlib — token validation manually
  5. fastapi-sso — for common providers
  6. python3-saml — basic SP setup
  7. Django + OIDC
  8. Managed services — when buying beats building
  9. JWT-only libraries
  10. Testing SSO locally
  11. Common pitfalls
  12. Common interview confusions
  13. Related
  14. Interview angle

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

python
# 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

python
@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.

PKCE

python
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:

python
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:

python
# 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

python
# 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

python
# 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.

Customising user creation

To customize user creation / role mapping, subclass OIDCAuthenticationBackend:

python
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
joserfc full JOSE (JWS, JWE, JWK); from the Authlib author
python-jose avoid in new code: effectively unmaintained, past CVEs
Authlib also handles JWTs; one library does everything
python
# 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 SSO Attack Vectors.

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.

yaml
# 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 6

  • “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.