Skip to main content

Libreria de autenticacion OIDC con Keycloak para FastAPI, Flask y Django

Project description

🛡️ Auth Guardian

Add Keycloak authentication to FastAPI, Flask or Django in minutes — without wrestling with OIDC.

PyPI version Python versions License Keycloak 26

English · Español


auth-guardian gives you ready-to-use OIDC login, endpoint protection and role-based access with Keycloak. You focus on your app; the library handles the token dance, the anti-CSRF state, introspection and logout with revocation.

from fastapi import Depends, FastAPI
from auth_guardian import AuthGuardian, create_auth_router

app = FastAPI()
auth = AuthGuardian()                       # reads config from the environment
app.include_router(create_auth_router(auth))  # /login, /oidc/callback, /logout

@app.get("/profile")
async def profile(user=Depends(auth.get_current_user)):
    return {"hello": user["preferred_username"]}

That's all you need for Keycloak authentication.

▶️ Want to try it right now? A complete, self-contained demo (Keycloak + FastAPI, one command) lives in examples/fastapi-keycloak.


Table of contents


🤔 Why auth-guardian?

Wiring OIDC with Keycloak by hand means: building the authorization URL, handling the state, exchanging the code for tokens, validating the access_token on every request, refreshing it, revoking it on logout, extracting roles… and repeating it in every project.

auth-guardian packages all of that behind a small, stable, framework-agnostic API (FastAPI, Flask, Django), so integrating Keycloak takes a couple of lines.

Features

  • Complete OIDC login/login, /oidc/callback and /logout routes ready to mount.
  • Multi-framework — FastAPI, Flask and Django with the same configuration.
  • PKCE (S256) + nonce by default — as recommended by Keycloak 26 and OAuth 2.1, even for confidential clients.
  • Device flow — browserless login (CLI, TV, IoT) with the Device Authorization Grant.
  • Backchannel logout — receives and validates Keycloak's logout to end sessions server-side.
  • Endpoint protectionget_current_user as a dependency/decorator.
  • Role-based accessrequire_role("admin") with realm and client roles.
  • Two validation modes — introspection (instant revocation) or local signature (max performance) with cached JWKS.
  • Automatic silent token refresh — when the access token expires, the FastAPI dependency renews it transparently with the refresh token (works in both introspection and local modes).
  • Secure logout — revokes the refresh token in Keycloak before clearing cookies.
  • Signed anti-CSRF state in the OIDC flow.
  • userinfo — fresh user claims straight from Keycloak.
  • Errors with context — failures carry Keycloak's exact OAuth error/error_description (invalid_grant, etc.) so you can diagnose in seconds.
  • Fail fast and clear — if configuration is missing, it tells you at startup.
  • Admin API — create users in Keycloak from your backend (optional).
  • Multi-tenant — resolve the realm per request with tenant_resolver.

Compatibility

Supported versions
Python 3.10+
FastAPI 0.118 – 0.139+
Flask 3.x
Django 4.2 – 5.x
Keycloak 26 (and any standard OIDC-compliant provider)

Installation

pip install auth-guardian            # FastAPI (default)
pip install "auth-guardian[flask]"   # Flask
pip install "auth-guardian[django]"  # Django

Configuration

auth-guardian reads these environment variables. The required ones make the library fail at startup with a clear message if they are missing:

Variable Required Description
KEYCLOAK_BASE_URL Public Keycloak URL (e.g. https://sso.mydomain.com).
KEYCLOAK_REALM Realm name.
KEYCLOAK_CLIENT_ID Client ID (confidential client).
KEYCLOAK_CLIENT_SECRET Client secret. Also signs the anti-CSRF state.
AUTH_TOKEN_VALIDATION introspection (default) or local. See below.
IS_PROD true marks cookies as Secure (HTTPS). Defaults to false.

💡 The aliases AUTH_BASE_URL, AUTH_REALM and AUTH_CLIENT_ID are also accepted.

💡 Behind Docker/proxy you can separate the public URL from the internal one by passing internal_url to AuthConfig (the browser uses the public one; the backend, the internal one).


Quickstart

FastAPI

from typing import Any
from fastapi import Depends, FastAPI
from auth_guardian import AuthGuardian, create_auth_router

app = FastAPI()
auth = AuthGuardian()

# Mounts /login, /oidc/callback and /logout
app.include_router(
    create_auth_router(auth, login_redirect_url="/profile", logout_redirect_url="/login")
)

@app.get("/profile")
async def profile(user: dict[str, Any] = Depends(auth.get_current_user)):
    return {"user": user["preferred_username"], "email": user.get("email")}

@app.get("/admin")
async def admin(user: dict[str, Any] = Depends(auth.require_role("admin"))):
    return {"ok": True}

Flask

from flask import Flask, jsonify, g
from auth_guardian import AuthGuardian, create_flask_integration

app = Flask(__name__)
auth = AuthGuardian()
flask_auth = create_flask_integration(auth)

flask_auth.register_auth_routes(app, login_redirect_url="/profile", logout_redirect_url="/login")

@app.get("/profile")
@flask_auth.require_auth()
def profile():
    return jsonify(g.auth_user)

@app.get("/admin")
@flask_auth.require_role("admin")
def admin():
    return jsonify({"ok": True})

Django

from django.http import JsonResponse
from django.urls import path
from auth_guardian import AuthGuardian, create_django_integration

auth = AuthGuardian()
django_auth = create_django_integration(auth)

@django_auth.require_auth()
def profile(request):
    return JsonResponse(request.auth_user)

@django_auth.require_role("admin")
def admin(request):
    return JsonResponse({"ok": True})

urlpatterns = [
    *django_auth.build_auth_urlpatterns(login_redirect_url="/profile/", logout_redirect_url="/login/"),
    path("profile/", profile),
    path("admin/", admin),
]

Login options (SSO and PKCE)

All three frameworks accept the same options when registering the routes:

create_auth_router(
    auth,
    login_redirect_url="/profile",  # where the user goes after logging in
    logout_redirect_url="/login",   # where they go after logout (or if login fails)
    prompt="login",                 # "login" ALWAYS forces credentials;
                                    # None honors the active Keycloak SSO session
    use_pkce=True,                  # PKCE S256 (recommended; keep it on unless you have a reason)
    use_nonce=True,                 # OIDC nonce (verified against the id_token)
    on_login_success=my_hook,       # callback with the token payload after a successful login
)

💡 Real SSO: with prompt=None, a user with an active Keycloak session gets in directly without re-entering credentials. The default "login" forces re-authentication on every login (useful for sensitive apps).


Token validation: introspection vs local

On every protected request, auth-guardian validates the access_token. Choose the mode with AUTH_TOKEN_VALIDATION:

Mode How it validates Advantage Cost
introspection (default) Asks Keycloak (/token/introspect) on every request Instant revocation One network call per request
local Verifies the JWT signature against the JWKS (cached, TTL 300s) Very fast, no network per request Revocation isn't instant (use short-lived tokens)

Rule of thumb: introspection for maximum security; local when throughput matters.


Role-based protection

# A single role
Depends(auth.require_role("admin"))

# Any of several roles
Depends(auth.require_role("admin", "auditor"))

Considers both realm roles (realm_access.roles) and client roles (resource_access). If the user lacks the role, it responds 403.


How it works

1. OIDC login

sequenceDiagram
    participant U as User
    participant A as Your API
    participant K as Keycloak
    U->>A: GET /login
    A->>K: Redirect (authorization request + signed state)
    K-->>U: Login screen
    U->>K: Credentials
    K-->>A: Redirect /oidc/callback?code=...
    A->>K: Exchange code for tokens
    K-->>A: access_token + refresh_token
    A-->>U: Set cookies + redirect to the app

2. Protected request (introspection)

sequenceDiagram
    participant C as Client
    participant A as Your API
    participant K as Keycloak
    C->>A: Request with token
    A->>K: POST /token/introspect
    K-->>A: active: true  → 200 OK
    K-->>A: active: false → 401 Unauthorized

3. Logout

sequenceDiagram
    participant U as User
    participant A as Your API
    participant K as Keycloak
    U->>A: GET /logout
    A->>K: POST /revoke (refresh_token)
    K-->>A: 200 / 204
    A-->>U: Clear cookies + redirect

Error handling

Every exception inherits from clear types and never leaks internal details to the client:

Exception When it's raised
TokenValidationError Token invalid, expired or issued for another client.
KeycloakAPIError Failure talking to Keycloak. Carries status_code, detail and the exact OAuth error: error (e.g. invalid_grant) and error_description.
KeycloakAuthError Base class of the above.
try:
    await auth.oidc_client.refresh_access_token(refresh)
except KeycloakAPIError as exc:
    # exc.error == "invalid_grant" · exc.error_description == "Token is not active"
    log.warning("Refresh failed: %s (%s)", exc.error, exc.error_description)
Situation Response to the client
active: false / invalid token 401 Unauthorized
Insufficient role 403 Forbidden
Keycloak unavailable 503 Service Unavailable (without exposing internals)
from auth_guardian import KeycloakAPIError

try:
    ...
except KeycloakAPIError as exc:
    raise HTTPException(status_code=exc.status_code, detail=exc.detail)

Public API reference

The public contract is kept small and stable on purpose:

Symbol What it is
AuthGuardian Entry point. Methods: get_current_user, require_role(*roles), authenticate_token, revoke_token, startup/shutdown.
AuthConfig Advanced configuration (internal_url, issuer, token_validation, jwks_cache_ttl_seconds…).
create_auth_router(auth, ...) FastAPI router with /login, /oidc/callback, /logout.
create_flask_integration(auth) Flask integration (register_auth_routes, require_auth, require_role).
create_django_integration(auth) Django integration (build_auth_urlpatterns, require_auth, require_role).
AuthOIDCClient Low-level OIDC client (token, refresh, revoke, fetch_userinfo, get_service_account_token, admin API).
extract_client_roles(payload, client_id) Extracts client roles from the token.
generate_pkce_pair() PKCE (code_verifier, code_challenge) S256 pair for custom flows.
Revocation backends MemoryRevocationBackend, DatabaseRevocationBackend, AutoRevocationBackend, NullRevocationBackend.
Exceptions KeycloakAuthError, TokenValidationError, KeycloakAPIError.

Advanced

userinfo: fresh user claims

info = await auth.oidc_client.fetch_userinfo(access_token)
# {"sub": "...", "email": "...", "preferred_username": "..."}

Machine-to-machine (service account tokens)

For service-to-service calls with no user, use the client_credentials grant with this client's own service account. Enable Service accounts on the Keycloak client.

token = await auth.get_service_account_token()          # optionally scope="my-api"
access_token = token["access_token"]                    # cache until ~expires_in
# call another API: headers={"Authorization": f"Bearer {access_token}"}

Active Directory / LDAP attributes and custom claims

get_current_user returns the full token claims (a plain dict), so any claim Keycloak puts in the token is available to your app — including attributes federated from Active Directory / LDAP. Nothing to change in the library; you wire the attribute in Keycloak and just read it:

  1. User Federation → LDAP maps the AD attribute to a Keycloak user attribute.
  2. Add a protocol mapper (User Attribute → token claim) on the client/scope.
  3. Read it in your app: user.get("department").

Common AD attributes and the claim they usually land in:

AD attribute Typical claim Example
displayName name John Smith
sAMAccountName preferred_username JSmith
userPrincipalName upn / preferred_username JSmith@domain.com
mail email jsmith@domain.com
givenName given_name John
sn family_name Smith
department department (custom) Sales
title title (custom) Manager
memberOf groups (via Group mapper) Managers
@app.get("/me")
async def me(user: dict = Depends(auth.get_current_user)):
    return {
        "name": user.get("name"),            # displayName
        "email": user.get("email"),          # mail
        "department": user.get("department"),  # custom AD mapper
    }

💡 Prefer auth.oidc_client.fetch_userinfo(access_token) when you want the freshest attribute values without re-issuing the token.

Organizations (Keycloak 26)

Keycloak 26 can add an organization claim (id + attributes) to tokens. Since the full payload is returned, read it directly: user.get("organization"). For per-realm multi-tenancy see Multi-tenant.

⚠️ Lightweight access tokens (Keycloak 26): if you enable them on the client, the access token is trimmed and may not carry roles/attributes. Use introspection mode (default) or fetch_userinfo so role checks and claims keep working.

Multi-tenant (one realm per request)

Resolve the realm dynamically (by subdomain, header, etc.). Per-realm clients are cached automatically:

def resolve_realm(request) -> str:
    return request.headers.get("X-Tenant", "default-realm")

auth = AuthGuardian(tenant_resolver=resolve_realm)

Lifecycle (HTTP resources)

app = FastAPI(lifespan=auth.lifespan())   # automatic startup/shutdown

Revocation backends (local mode)

In local validation, access tokens revoked with auth.revoke_token(token) are rejected by checking a backend by JTI:

Backend Use
MemoryRevocationBackend (default) Single process.
DatabaseRevocationBackend Shared across workers/instances (SQLAlchemy).
AutoRevocationBackend Picks based on configuration.
NullRevocationBackend Disables the check.

Customizing cookies

The cookie names (access_token, id_token, refresh_token) can be changed with cookie_name, cookie_id_name and cookie_refresh_name when registering the routes in any of the three frameworks.

Device flow (browserless login)

For CLIs, TVs or IoT (Device Authorization Grant, RFC 8628). Enable OAuth 2.0 Device Authorization Grant on the Keycloak client.

dev = await auth.oidc_client.request_device_authorization()
print(f"Go to {dev['verification_uri']} and enter the code: {dev['user_code']}")

# Poll until the user approves (honors interval and expiration)
tokens = await auth.oidc_client.poll_until_authorized(dev)

Backchannel logout

Receives the logout Keycloak sends when a session ends (OIDC Back-Channel Logout). Register POST /backchannel-logout as the client's Backchannel logout URL in Keycloak.

from auth_guardian import create_backchannel_logout_route

async def end_session(claims):
    # invalidate your local session by claims["sub"] and/or claims["sid"]
    ...

app.include_router(create_backchannel_logout_route(auth, end_session))

The route validates the logout_token (signature, issuer, audience, event, no nonce) and only then calls your callback.


Configuring the client in Keycloak

  1. Create an OIDC client in your realm.
  2. Enable Client authentication (confidential client).
  3. Copy the Client SecretKEYCLOAK_CLIENT_SECRET.
  4. In Valid Redirect URIs, add your callback URL (e.g. https://yourapp.com/oidc/callback).
  5. Define the roles (realm or client) according to your authorization model.

Behind a reverse proxy, make sure to forward Host and X-Forwarded-* so the Redirect URIs match.


Security

  • PKCE (S256) + nonce enabled by default in the authorization flow, as recommended by Keycloak 26 and OAuth 2.1 — they protect the code and prevent id_token replay.
  • Signed state (HMAC-SHA256) in the OIDC flow to prevent CSRF.
  • Cookies HttpOnly + SameSite=Lax (+ Secure with IS_PROD=true); logout revokes the refresh token in Keycloak before clearing them.
  • No detail leakage: Keycloak errors are translated into generic responses for the end client (the detail stays in your logs).
  • With AUTH_TOKEN_VALIDATION=local, use short-lived tokens to bound the revocation window (the library warns you at startup if the lifespan is high).

Skill for AI agents

If you work with coding assistants (Claude, Cursor, etc.), the library ships a skill in skills/auth-guardian/SKILL.md with everything an agent needs to integrate it well (API, patterns, common pitfalls).

  • With library-skills: uvx library-skills detects and installs it.
  • Manual: copy skills/auth-guardian/ into your project's .claude/skills/ (or .agents/skills/).

Troubleshooting

Missing required configuration variables

A required variable is missing. Check the Configuration section and set KEYCLOAK_BASE_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID and KEYCLOAK_CLIENT_SECRET.

ModuleNotFoundError: No module named 'flask' / 'django'

You're using the adapter without installing the extra: pip install "auth-guardian[flask]" or "auth-guardian[django]".

Introspection returns 401 or 403

The client isn't confidential or the secret is wrong. Check KEYCLOAK_CLIENT_SECRET and that Client authentication is enabled in Keycloak.

Login fails at the callback (Redirect URI mismatch)

Keycloak's Redirect URI doesn't match your app's. Review Valid Redirect URIs and, if you're behind a proxy, the X-Forwarded-* headers.


Development and contributing

git clone <repo> && cd auth-guardian
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

pytest          # tests
ruff check src  # lint
mypy src        # types

PRs are welcome. Keep the public contract (__all__) small and stable, and back your changes with tests.

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

auth_guardian-0.1.40.tar.gz (53.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

auth_guardian-0.1.40-py3-none-any.whl (42.0 kB view details)

Uploaded Python 3

File details

Details for the file auth_guardian-0.1.40.tar.gz.

File metadata

  • Download URL: auth_guardian-0.1.40.tar.gz
  • Upload date:
  • Size: 53.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for auth_guardian-0.1.40.tar.gz
Algorithm Hash digest
SHA256 8af85febab678df29e95d9d7ffde3d7bf094bb8788a99500c2b471db2d2777d9
MD5 1fcb76a5908866e2a5c676b141555e0a
BLAKE2b-256 925efe4793b029faec5fbf15bf58e26501bd66b8576db8b2905f77e641bd5e25

See more details on using hashes here.

File details

Details for the file auth_guardian-0.1.40-py3-none-any.whl.

File metadata

  • Download URL: auth_guardian-0.1.40-py3-none-any.whl
  • Upload date:
  • Size: 42.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for auth_guardian-0.1.40-py3-none-any.whl
Algorithm Hash digest
SHA256 e76e49a551ada9dc1d7ed31d3851c5637195664f9913943439dc63c516ac3a92
MD5 b0bee2ecf24c3d7b857ee46dce9a6b93
BLAKE2b-256 8771f65a80dbe967fd3e63a86e87de596e8c1df909b3d5f185020a3da9ac6e95

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page