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
algorithmsin 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). Orfastapi-ssofor major providers if you want less code. Validate ID tokens withauthlib.joseor 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 pinalgorithmsto 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=Noneor not passing it, which allows the token’s ownalgfield to control validation — leads to algorithm confusion attacks. Always pin.