Get AI summaries of any video or article — Sign up free
Python FastAPI Tutorial (Part 10): Authentication - Registration and Login with JWT thumbnail

Python FastAPI Tutorial (Part 10): Authentication - Registration and Login with JWT

Corey Schafer·
6 min read

Based on Corey Schafer's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.

TL;DR

Install Argon 2 support, a JWT library, and Pydantic Settings to implement secure authentication and configuration management.

Briefing

Authentication is wired into the FastAPI app by adding secure password hashing (Argon 2), JWT access tokens, and backend endpoints for registration, login, and “who am I?”—so the system can reliably identify users instead of relying on a hard-coded user ID. The change matters because it lays the foundation for real security: once tokens can be issued and validated, routes can later be protected so only the right users can access or modify data.

The build starts with new dependencies: Argon 2 support for password hashing, a JWT library for token creation/verification, and Pydantic Settings for configuration management. Argon 2 is chosen over BCrypt as the more modern, GPU-resistant approach. Configuration is centralized in a settings module that reads secrets (notably the JWT signing secret) from an .env file or environment variables, with type validation and “fail fast” behavior. A secret key is generated using Python’s secrets module and .env is kept out of Git via gitignore.

Because SQLite makes schema changes awkward, the existing database is deleted to start fresh. The user model is updated by adding a non-nullable password hash field (explicitly “password_hash,” never a plaintext password). On the API schema side, registration requires a password with a minimum length of 8 characters. Response models are split to improve privacy: public user data used in post responses omits email, while a private user schema includes email only when appropriate. A token response schema is also added for login.

Authentication utilities are then created. A password hasher is configured with PWD lib’s Argon 2 “recommended” settings. JWT handling is implemented so tokens include an expiration and store the user ID in the JWT “sub” claim. Verification returns the user ID when the token is valid and not expired; otherwise it yields none. An OAuth2PasswordBearer scheme is configured with a token URL that matches the login endpoint, enabling FastAPI’s built-in “Authorize” workflow in the docs.

User routes are updated accordingly. Registration hashes the password before saving, normalizes email to lowercase, and performs case-insensitive uniqueness checks for both username and email (using SQLAlchemy’s lower() approach). Login accepts form-encoded credentials via OAuth2PasswordRequestForm (using the form field named “username” to carry the email), looks up the user case-insensitively, verifies the password, and returns a JWT access token with token type “bearer.” Importantly, incorrect email and incorrect password produce the same generic 401 error to avoid leaking which accounts exist.

A new /me endpoint is added to fetch the current authenticated user. It extracts the bearer token from the Authorization header, verifies it, converts the subject to an integer defensively, and returns 401 for invalid/expired tokens or missing users. This endpoint gives the front end a server-validated way to determine login state.

Finally, the front end gains registration and login templates. Login posts form data to the token endpoint, stores the returned access token in localStorage, and uses a small JS module to fetch /me, cache the current user, and clear tokens on logout. The navbar updates based on whether a current user is available. Testing confirms registration, login, token storage, and /me validation work.

What remains intentionally unprotected: post creation/edit/delete still uses the old hard-coded user ID in the post flow, so anyone can modify posts for now. The next tutorial is positioned to replace those hard-coded IDs with the authenticated user ID and enforce ownership checks—turning authentication into actual authorization.

Cornell Notes

The app adds a full authentication layer: users register with a password that gets hashed using Argon 2, then log in to receive a JWT access token. The JWT stores the user ID in the “sub” claim and includes an expiration; invalid or expired tokens fail verification. A /me endpoint validates the bearer token and returns the current user, enabling the front end to confirm login state without trusting client-side decoding. Privacy is improved by splitting user response schemas so public responses omit email while private responses include it. These pieces set up the next step—protecting routes and enforcing what authenticated users are allowed to do.

Why is plaintext password storage avoided, and how is the password handled instead?

Passwords are never stored directly. Registration hashes the submitted password using an Argon 2 password hasher configured with PWD lib’s “recommended” settings, then saves the result in a dedicated database column named password_hash. Login verifies credentials by comparing the submitted plaintext password against the stored hash using the hasher’s verify method. This design prevents recovering passwords even if the database is stolen, and Argon 2’s per-hash salt ensures identical passwords produce different hashes.

How does the system decide whether a JWT is valid, and what data does it carry?

JWTs are created with an expiration and signed using a secret key loaded from configuration (settings.secret_key). The user ID is stored in the JWT “sub” claim when the token is created. Verification checks signature integrity and expiration; if the token is invalid or expired, verification returns none. If valid, verification extracts the user ID from “sub” and returns it for downstream authorization logic.

What security measure prevents attackers from learning whether an email exists?

Login returns the same generic 401 Unauthorized error when either the user lookup fails or the password verification fails. The logic checks both conditions (no user found OR password mismatch) and responds with identical error messaging: “incorrect email or password.” That avoids revealing whether a particular email is registered.

Why split user response schemas into public and private?

Public responses are used when returning authors on posts, where exposing email would be a privacy leak. The public schema (user public) includes ID, username, and image info but omits email. The private schema (user private) extends the public schema and adds email, intended for cases like viewing the current user via /me. This ensures email is only returned when the client is authenticated and authorized to see it.

How does the /me endpoint work, and why is it useful for the front end?

The /me endpoint uses OAuth2PasswordBearer to extract the bearer token from the Authorization header. It verifies the token to obtain the user ID from the JWT “sub” claim, converts it to an integer defensively, then fetches the user from the database. If anything fails (missing/invalid token, conversion error, or user not found), it returns 401. The front end calls /me to confirm that the token is still valid and to retrieve full user data from the server, rather than trusting client-side JWT decoding.

What does the front end do with the access token, and what are the tradeoffs?

After successful login, the front end stores the access token in localStorage. It then calls /me with the token in the Authorization header to populate the current user state. The tradeoff is security: localStorage is vulnerable to cross-site scripting (XSS) because malicious scripts can read stored tokens. The implementation mitigates exposure by using short token expiration, and it notes that HTTP-only cookies would be safer for browser-only apps, but localStorage is chosen here for broader client compatibility.

Review Questions

  1. What fields and normalization rules are applied to username and email during registration to enforce uniqueness safely?
  2. Describe the JWT claims used and the failure behavior when a token is expired or invalid.
  3. Why does the app return a generic 401 error for both “email not found” and “wrong password” cases?

Key Points

  1. 1

    Install Argon 2 support, a JWT library, and Pydantic Settings to implement secure authentication and configuration management.

  2. 2

    Add password_hash to the user model and hash passwords with Argon 2; never store plaintext passwords.

  3. 3

    Use separate user schemas (public vs private) so post responses omit email while authenticated “current user” responses can include it.

  4. 4

    Create JWT access tokens that sign with a secret key and store the user ID in the sub claim with an expiration time.

  5. 5

    Implement registration and login endpoints with case-insensitive uniqueness checks and generic 401 errors to avoid account enumeration.

  6. 6

    Add a /me endpoint that validates bearer tokens server-side and returns the authenticated user or 401 on failure.

  7. 7

    Update the front end with registration/login forms, token storage, and an authorization-state module that caches current user data and clears tokens on logout.

Highlights

JWTs carry the user ID in the sub claim and are rejected when expired or tampered with, enabling reliable server-side identity checks.
Login intentionally returns the same 401 error whether the email is unknown or the password is wrong, reducing the risk of account enumeration.
User privacy improves by removing email from public post author responses and only returning it in authenticated contexts via /me.
The /me endpoint validates the bearer token and fetches the user, giving the front end a trustworthy way to determine login state.
Argon 2 hashing is configured with recommended settings, and password_hash is stored as a non-nullable database field.

Topics

  • JWT Authentication
  • Argon 2 Password Hashing
  • FastAPI OAuth2
  • Pydantic Settings
  • Case-Insensitive User Lookup

Mentioned