Last updated: July 2026
The best Python login page never sees a password. Instead of building a form and hashing credentials yourself, redirect the user to an identity provider like Keycloak, let it run the login, and take back a signed token through the OAuth 2.0 authorization code flow with PKCE. Authlib handles the redirect dance in Flask or Django, and PyJWT verifies the resulting JWT against Keycloak’s published keys. Your database stores zero passwords, so there’s nothing for an attacker to dump.
Why do Python login tutorials teach the wrong thing?
Nearly every top result for “how to create a login page in Python” hands you a Tkinter window or a Flask route that compares a submitted password against a database row. That was fine homework in 2015. In production, it makes your app the thing that gets breached, and it makes you personally responsible for hashing, rate limiting, MFA, password resets, breach monitoring, and every credential-stuffing wave aimed at your users.
Here’s the part those tutorials skip: you can decline the liability entirely. Delegate login to an identity provider, and your app’s job shrinks to two tasks. Redirect the user to the provider. Verify the token that comes back. The login UI, brute-force detection, MFA, and single sign-on across your other apps all live in the provider, maintained by people whose full-time job is authentication.
This guide uses Keycloak because it’s open source, self-hostable, and speaks standard OpenID Connect, but the flow is identical for any OIDC provider. If Keycloak is new to you, our complete Keycloak guide covers the fundamentals.
What does a modern Python login flow look like?
One flow, no exceptions: the authorization code flow with PKCE. RFC 9700, the current OAuth 2.0 Security Best Current Practice, settles the old debates. PKCE is required for authorization code clients, the implicit flow is prohibited, and tokens don’t belong in browser localStorage. If a tutorial shows anything else, close the tab.
The flow itself is five steps:
- The user clicks “Log in”. Your app generates a PKCE code verifier and redirects the browser to Keycloak’s authorization endpoint.
- Keycloak renders the login page. Passwords, MFA prompts, and lockout policies are now its problem, not yours.
- On success, Keycloak redirects back to your callback URL with a one-time authorization code.
- Your server exchanges that code, plus the PKCE verifier and (for confidential clients) the client secret, for an ID token and access token.
- You verify the token, stash it in a server-side session, and set a session cookie. The user is logged in.
The practical win: everything your client library needs (endpoint URLs, supported algorithms, the signing keys) is published in Keycloak’s discovery document at /realms/{realm}/.well-known/openid-configuration, as described in the Keycloak securing-apps documentation. You configure one URL and the library reads the rest.
Configure Keycloak for a Python app
In the Keycloak 26 admin console, create a client under Clients > Create client with the OpenID Connect type. The one setting that matters most is the Client authentication toggle. Switched on, you get a confidential client: Keycloak issues a client secret that your server presents during the code exchange. Server-rendered Python apps should always use this. Switched off, you get a public client, which is meant for apps that can’t keep a secret, like single-page apps or native clients.
Next, set Valid redirect URIs to the exact callback URL of your app, such as https://myapp.example.com/callback. Keycloak allows a wildcard only at the end of the path, and its own documentation flags wildcard redirect URIs as an open-redirect risk. Type the full URI. Future you will be glad.
Honest note: the client settings screen has a lot of toggles, and Keycloak’s admin console doesn’t hide any of them from you. For this tutorial you only need three things: client authentication on, an exact redirect URI, and the standard flow enabled (it is by default).
Build the login page: Flask plus Authlib
For server-rendered Python apps on Flask, Django, or Starlette, Authlib is the mature pick. Version 1.7.2 shipped in May 2026 (PyPI) and its framework clients handle OIDC discovery, state checks, and PKCE for you. Point server_metadata_url at the Keycloak discovery document and request PKCE with code_challenge_method: "S256", which is the only PKCE method Authlib supports. Conveniently, it’s also the only one worth using.
Install the pieces, then wire up three routes:
from flask import Flask, redirect, session, url_for
from authlib.integrations.flask_client import OAuth
app = Flask(__name__)
app.secret_key = "change-me" # load from your environment in real life
oauth = OAuth(app)
oauth.register(
name="keycloak",
client_id="flask-app",
client_secret="your-client-secret",
server_metadata_url=(
"https://id.example.com/realms/myrealm"
"/.well-known/openid-configuration"
),
client_kwargs={
"scope": "openid email profile",
"code_challenge_method": "S256", # PKCE
},
)
@app.route("/login")
def login():
redirect_uri = url_for("callback", _external=True)
return oauth.keycloak.authorize_redirect(redirect_uri)
@app.route("/callback")
def callback():
token = oauth.keycloak.authorize_access_token()
session["user"] = token["userinfo"]
return redirect("/")
@app.route("/")
def index():
user = session.get("user")
if user:
return f"Hello, {user['email']}"
return '<a href="/login">Log in</a>'
That’s the whole login page. authorize_redirect generates the state parameter and PKCE verifier, stores them in the session, and sends the user to Keycloak. authorize_access_token validates the state, exchanges the code and verifier for tokens, verifies the ID token, and hands you the parsed claims in token["userinfo"]. The browser only ever holds Flask’s session cookie. There is no password field, no password column, and no bcrypt-vs-argon2 decision to get wrong.
Verify a Keycloak JWT in FastAPI
API backends face the other half of the problem. Nobody gets redirected; requests arrive carrying an access token in the Authorization header, and you must verify the signature on every single one. Use PyJWT 2.13.0’s PyJWKClient (PyJWT docs): get_signing_key_from_jwt reads the token’s kid header, fetches the matching key from Keycloak’s JWKS endpoint, and caches the key set so you’re not hammering Keycloak per request.
On the FastAPI side, OAuth2AuthorizationCodeBearer extracts the bearer token from the header and, as a bonus, gives Swagger UI a working “Authorize” button wired to your Keycloak realm:
import jwt
from jwt import PyJWKClient
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import OAuth2AuthorizationCodeBearer
KEYCLOAK = "https://id.example.com/realms/myrealm"
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl=f"{KEYCLOAK}/protocol/openid-connect/auth",
tokenUrl=f"{KEYCLOAK}/protocol/openid-connect/token",
refreshUrl=f"{KEYCLOAK}/protocol/openid-connect/token",
scopes={"openid": "OpenID Connect scope"},
)
jwks_client = PyJWKClient(f"{KEYCLOAK}/protocol/openid-connect/certs")
app = FastAPI()
def current_user(token: str = Depends(oauth2_scheme)) -> dict:
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
return jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="fastapi-app",
issuer=KEYCLOAK,
)
except jwt.PyJWTError as exc:
raise HTTPException(status_code=401, detail=str(exc)) from exc
@app.get("/me")
def me(claims: dict = Depends(current_user)):
return {"sub": claims["sub"], "email": claims.get("email")}
jwt.decode enforces the signature, expiry, issuer, and audience in one call. Pin algorithms=["RS256"] explicitly; never let the token’s own header pick the algorithm. One Keycloak-specific gotcha worth knowing: if you hit an audience error, inspect the token’s aud claim. Out of the box, Keycloak often needs an audience mapper on the client before your API’s client ID shows up there. You can paste your token and JWKS URL into our JWKS verifier to test signature verification right in the browser, and our deeper guide on verifying Keycloak access tokens on the backend walks through the edge cases.
One library to actively avoid: python-jose. Versions up to and including 3.3.0 carry CVE-2024-33663, an algorithm-confusion vulnerability, and the advisory’s recommended fix is migrating to PyJWT. Plenty of older FastAPI tutorials still import it. Don’t copy them.
Where should a Python app keep its tokens?
On the server, full stop. RFC 9700 is blunt about browser storage: access and refresh tokens do not belong in localStorage, where any injected script can read them. In a server-rendered Python app the natural home is the server-side session (Redis, a database, or your framework’s session store), with the browser holding nothing but an HttpOnly, Secure session cookie. That’s exactly what the Flask example above does.
Then make stolen refresh tokens worthless. In Keycloak, go to Realm Settings > Tokens, enable Revoke Refresh Token, and set Refresh Token Max Reuse to 0. Each refresh token now works exactly once; Keycloak issues a new one at every refresh and treats a replayed old token as a signal to kill the session. Combined with short access token lifetimes, a leaked token buys an attacker minutes instead of months.
Which Python auth library does which job?
No single package covers login flows, token verification, and Keycloak administration well, and that’s fine, because the good ones compose cleanly. Here’s the July 2026 lineup, with versions checked on PyPI:
| Library | Version | Use it for | Don’t use it for |
|---|---|---|---|
| Authlib | 1.7.2 | Browser login flows in Flask, Django, Starlette | API-only token checks (heavier than you need) |
| PyJWT | 2.13.0 | Verifying JWTs against a JWKS endpoint | Running a login flow (it’s not an OAuth client) |
| python-keycloak | 7.1.1 | Automating the Keycloak admin REST API | The login flow itself |
| python-jose | any | Nothing new | Everything: CVE-2024-33663 |
A note on python-keycloak: version 7.1.1 (February 2026) supports Keycloak 22 through 26 and wraps the admin REST API, so you can script realm, client, and user creation from Python or CI. It’s a great automation tool and the wrong layer for authenticating end users, a distinction its README is upfront about.
Run Keycloak yourself or use a managed host (that’s our corner of the world); the Python code in this guide is identical either way, because it only ever talks to standard OIDC endpoints.
Frequently asked questions
How do I add a login page to a Python app without storing passwords?
Delegate login to an OpenID Connect provider like Keycloak. Your app redirects the user to the provider with Authlib, the provider renders the login form and checks credentials, and your app receives a signed token through the authorization code flow with PKCE. You keep a session, never a password.
What is the best Python library for OpenID Connect in 2026?
Authlib 1.7.2 for server-rendered login flows in Flask, Django, or Starlette, and PyJWT 2.13.0 for verifying tokens in API backends. Add python-keycloak 7.1.1 if you want to automate Keycloak’s admin REST API. They solve different layers of the problem and work well together.
How do I validate a Keycloak JWT in Python?
Point PyJWT’s PyJWKClient at your realm’s /protocol/openid-connect/certs endpoint, call get_signing_key_from_jwt, then jwt.decode with algorithms=["RS256"] plus explicit audience and issuer values. Our guide to verifying Keycloak tokens on the backend covers the full checklist, including the audience mapper gotcha.
Should I use python-jose or PyJWT?
PyJWT. python-jose versions up to and including 3.3.0 are affected by CVE-2024-33663, an algorithm-confusion vulnerability, and the published advisory recommends migrating to PyJWT. PyJWT also ships PyJWKClient with kid matching and JWKS caching built in, which is most of what a token-verifying backend needs.
Where should a Python web app store OAuth tokens?
In a server-side session, with the browser holding only an HttpOnly session cookie. RFC 9700, the current OAuth security best practice, warns against keeping tokens in localStorage. Pair this with Keycloak’s refresh token rotation (Revoke Refresh Token plus Max Reuse 0) so any leaked token dies on first replay.