A lightweight prototype implementing user registration, secure password hashing, and JSON Web Token (JWT) authentication in FastAPI.
Status: Prototype / Learning Project
- User registration
- Password hashing with
pwdlib - JWT-based access tokens
- Short-lived access tokens
- Opaque random refresh tokens
- SHA-256 hashing of refresh tokens before database storage
- Refresh-token rotation
- Server-side refresh-session revocation
- Logout
- Protected routes
- Password change for authenticated users
- Forgot-password flow
- Password-reset tokens
- Password-reset token expiration and revocation
- Automatic invalidation of existing sessions after password changes
password_changed_atsupport for invalidating previously issued JWTs
Client
│
│ POST /auth/login
▼
FastAPI
│
├── Verify email
├── Verify password
│
├── Generate JWT access token
│
└── Generate opaque refresh token
│
└── Store SHA-256 hash in database
▼
Client receives:
├── access_token
└── refresh_token
The access token is a JWT containing the user's ID as the sub claim.
Example payload:
{
"sub": "123",
"iat": 1750000000,
"exp": 1750000900
}The access token is used to access protected endpoints:
Authorization: Bearer <access_token>Refresh tokens are opaque random tokens, not JWTs.
The actual token is given to the client, while only its SHA-256 hash is stored in the database.
Refresh token
│
▼
SHA-256 hash
│
▼
Database
This means the database does not contain the usable refresh token.
Every successful refresh invalidates the old session and creates a new one:
RT1
│
│ POST /auth/refresh
▼
RT1 → revoked
RT2 → created
The client must replace its old refresh token with the newly issued one.
Passwords are never stored directly.
During registration/password changes:
plain password
│
▼
password hashing
│
▼
password_hash
│
▼
database
Password verification is performed against the stored hash during login or password changes.
There are two separate flows.
Used when the user is already authenticated:
POST /auth/change-passwordRequires:
- current password
- new password
- password confirmation
- valid access token
After changing the password:
password_changed_atis updated.- Existing refresh-token sessions are revoked.
- Previously issued access tokens are rejected by
get_current_user.
POST /auth/forgot-passwordThe user provides their email.
The server generates a random password-reset token and stores only its hash.
Reset token
│
├── sent to user
│
└── SHA-256 hash → database
The token expires after the configured reset-token lifetime.
POST /auth/reset-passwordThe user provides:
{
"token": "...",
"new_password": "...",
"confirm_password": "..."
}After a successful reset:
- password is changed
password_changed_atis updated- reset token is revoked
- existing refresh sessions are revoked
POST /auth/logoutLogout uses the refresh token to identify the user's server-side session.
The corresponding Session record is marked:
revoked_at = current_time
The refresh token can no longer be used to obtain new access tokens.
| Method | Endpoint | Authentication | Description |
|---|---|---|---|
POST |
/auth/register |
None | Register a new user |
POST |
/auth/login |
None | Login and obtain tokens |
POST |
/auth/refresh |
Refresh token | Rotate refresh token and obtain a new access token |
POST |
/auth/logout |
Refresh token | Revoke the current session |
GET |
/auth/me |
Access token | Get the authenticated user |
POST |
/auth/forgot-password |
None | Request a password reset |
POST |
/auth/reset-password |
Reset token | Set a new password |
POST |
/auth/change-password |
Access token | Change the current password |
Create a .env file:
DATABASE_URL=postgresql+psycopg://user:pass@localhost:5432/test_db
SECRET_KEY=your-secret-key
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=15
REFRESH_TOKEN_EXPIRE_MINUTES=20000
PASSWORD_RESET_TOKEN_EXPIRE_MINUTES=15Do not commit your .env file to version control.
Assuming PostgreSQL is already set up. Navigate to project directory.
Then:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
rm -rf alembic/versions/
alembic revision --autogenerate -m "create db tables"
alembic upgrade headStart the development server:
uvicorn main:app --reloadThe API will be available at:
http://localhost:8000
Interactive API documentation:
http://localhost:8000/docs