Skip to content

Commit 990d46d

Browse files
stackjayjaygaha
authored andcommitted
day #48 fastapi #11 JWT auth & authorization
1 parent 80a32b8 commit 990d46d

31 files changed

Lines changed: 888 additions & 0 deletions

workspace/7_framework/fastapi/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ uvicorn main:app --reload
105105
Learn to implement caching with Redis, protect endpoints with rate limiting, and defer long-running jobs with background tasks.
106106
_Includes: `fastapi-cache2`, `slowapi`, `BackgroundTasks`, lifespan events, and testing._
107107

108+
- [Day 11: Production-Ready Auth with JWT](day11/README.md)
109+
Learn to build a production-ready authentication system with JWT, including project structure, configuration management, password hashing, and dependency injection.
110+
_Includes: `pydantic-settings`, `passlib`, `python-jose`, SQLAlchemy, and comprehensive testing._
111+
108112
---
109113

110114
## Recommended Project Structure
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# API Settings
2+
PROJECT_NAME="FastAPI Auth Tutorial"
3+
API_V1_STR=/api/v1
4+
5+
# JWT Settings
6+
SECRET_KEY=your-secret-key-here-make-it-very-long-and-secure
7+
ALGORITHM=HS256
8+
ACCESS_TOKEN_EXPIRE_MINUTES=30
9+
REFRESH_TOKEN_EXPIRE_DAYS=7
10+
11+
# Database
12+
DATABASE_URL=sqlite:///./auth_demo.db
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# FastAPI Day 11: Production-Ready Auth with JWT
2+
3+
Welcome to **Day 11** of the FastAPI tutorial series! Today, we're building a production-ready authentication system using JSON Web Tokens (JWT). You'll learn how to structure a FastAPI application with a clean separation of concerns, manage configurations, and implement secure password handling and token-based authentication.
4+
5+
---
6+
7+
## What You'll Learn
8+
9+
- **Project Structure**: Organize your code into a scalable and maintainable structure with `src`, `core`, `database`, `models`, `routers`, `schemas`, and `services`.
10+
- **Configuration Management**: Use `pydantic-settings` to manage application settings from environment variables.
11+
- **Database Integration**: Connect to a database using SQLAlchemy and create tables based on your models.
12+
- **Password Hashing**: Securely hash and verify passwords with `passlib`.
13+
- **JWT Authentication**: Implement access and refresh tokens for secure authentication.
14+
- **Dependency Injection**: Use FastAPI's dependency injection system to manage database sessions and user authentication.
15+
- **Testing**: Write comprehensive tests for your authentication system using `pytest`.
16+
17+
---
18+
19+
## Key Concepts
20+
21+
For this tutorial, we've structured the application to be more modular and scalable.
22+
23+
- `src/main.py`: The application's entry point. It initializes the FastAPI app and includes the routers.
24+
- `src/core/config.py`: Defines the application's configuration using `pydantic-settings`.
25+
- `src/core/security.py`: Contains all the logic for password hashing and JWT creation and verification.
26+
- `src/database/connection.py`: Manages the database connection and session.
27+
- `src/database/crud.py`: Contains functions for creating, reading, and updating data in the database.
28+
- `src/models/user.py`: Defines the SQLAlchemy user model.
29+
- `src/routers/auth.py`: Defines the authentication-related endpoints (`/register`, `/login`, `/refresh`).
30+
- `src/routers/users.py`: Defines user-related endpoints (`/me`).
31+
- `src/routers/dependencies.py`: Defines dependencies for getting the current authenticated user.
32+
- `src/schemas/user.py`: Defines the Pydantic models for user-related data.
33+
- `src/schemas/token.py`: Defines the Pydantic models for token-related data.
34+
- `src/services/auth_service.py`: Contains the business logic for authentication.
35+
- `src/services/user_service.py`: Contains the business logic for user-related operations.
36+
- `tests/`: Contains all the tests for the application.
37+
38+
### 1. Configuration with `pydantic-settings`
39+
40+
We use `pydantic-settings` to manage our application's configuration. This allows us to define our settings in a Pydantic model and automatically load them from environment variables or a `.env` file.
41+
42+
```python-beginner/workspace/7_framework/fastapi/day11/src/core/config.py#L1-L18
43+
from pydantic_settings import BaseSettings
44+
45+
class Settings(BaseSettings):
46+
# JWT Settings
47+
SECRET_KEY: str = "your-secret-key-change-in-production"
48+
ALGORITHM: str = "HS256"
49+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
50+
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
51+
52+
# Database
53+
DATABASE_URL: str = "sqlite:///./app.db"
54+
55+
# API Settings
56+
API_V1_STR: str = "/api/v1"
57+
PROJECT_NAME: str = "FastAPI Auth Tutorial"
58+
59+
class Config:
60+
env_file = ".env"
61+
62+
settings = Settings()
63+
```
64+
65+
### 2. Password Hashing and JWTs with `passlib` and `python-jose`
66+
67+
We use `passlib` to hash passwords and `python-jose` to create and verify JWTs.
68+
69+
- **Password Hashing**: We use `passlib`'s `CryptContext` to hash and verify passwords securely.
70+
71+
```python-beginner/workspace/7_framework/fastapi/day11/src/core/security.py#L6-L7
72+
# Password hashing
73+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
74+
```
75+
76+
- **JWT Creation**: We have functions to create both access and refresh tokens, which encapsulate user identity and expiration data.
77+
78+
```python-beginner/workspace/7_framework/fastapi/day11/src/core/security.py#L9-L22
79+
def create_access_token(subject: Union[str, Any], expires_delta: Optional[timedelta] = None) -> str:
80+
"""Create JWT access token"""
81+
if expires_delta:
82+
expire = datetime.now() + expires_delta
83+
else:
84+
expire = datetime.now() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
85+
86+
to_encode = {"exp": expire, "sub": str(subject), "type": "access"}
87+
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
88+
return encoded_jwt
89+
```
90+
91+
### 3. Dependency Injection for Security
92+
93+
We use FastAPI's powerful dependency injection system to protect endpoints and retrieve the current authenticated user. This keeps the authentication logic clean and reusable.
94+
95+
```python-beginner/workspace/7_framework/fastapi/day11/src/routers/dependencies.py#L8-L34
96+
async def get_current_user(
97+
credentials: HTTPAuthorizationCredentials = Depends(security),
98+
db: Session = Depends(get_db)
99+
) -> User:
100+
"""Get current authenticated user"""
101+
credentials_exception = HTTPException(
102+
status_code=status.HTTP_401_UNAUTHORIZED,
103+
detail="Could not validate credentials",
104+
headers={"WWW-Authenticate": "Bearer"},
105+
)
106+
107+
if not credentials:
108+
raise credentials_exception
109+
110+
user_id = verify_token(credentials.credentials)
111+
if user_id is None:
112+
raise credentials_exception
113+
114+
user = get_user_by_id(db, int(user_id))
115+
if user is None:
116+
raise credentials_exception
117+
118+
if not user.is_active:
119+
raise HTTPException(
120+
status_code=status.HTTP_400_BAD_REQUEST,
121+
detail="Inactive user"
122+
)
123+
124+
return user
125+
```
126+
127+
---
128+
129+
## Next Steps
130+
131+
- Install the dependencies: `pip install -r requirements.txt`.
132+
- Create a `.env` file from the `.env.example` and set your `SECRET_KEY`.
133+
- Run the application with `uvicorn src.main:app --reload`.
134+
- Use an API client like `curl` or Postman to test the endpoints:
135+
- `POST /api/v1/auth/register` to create a new user.
136+
- `POST /api/v1/auth/login` to get an access and refresh token.
137+
- `GET /api/v1/users/me` with the access token in the `Authorization` header to get your user profile.
138+
- `POST /api/v1/auth/refresh` with the refresh token to get a new set of tokens.
139+
- Run the automated tests with `python -m pytest`.
140+
141+
---
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
asyncio_default_fixture_loop_scope = function
3+
pythonpath = . src
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
fastapi>=0.104.1
2+
uvicorn>=0.24.0
3+
python-jose[cryptography]>=3.3.0
4+
python-multipart>=0.0.6
5+
passlib[bcrypt]>=1.7.4
6+
sqlalchemy>=2.0.23
7+
pytest>=7.4.3
8+
pytest-asyncio>=0.21.1
9+
httpx>=0.25.2
10+
pydantic_settings
11+
pydantic[email]

workspace/7_framework/fastapi/day11/src/__init__.py

Whitespace-only changes.

workspace/7_framework/fastapi/day11/src/core/__init__.py

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pydantic_settings import BaseSettings
2+
3+
class Settings(BaseSettings):
4+
# JWT Settings
5+
SECRET_KEY: str = "your-secret-key-change-in-production"
6+
ALGORITHM: str = "HS256"
7+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
8+
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
9+
10+
# Database
11+
DATABASE_URL: str = "sqlite:///./app.db"
12+
13+
# API Settings
14+
API_V1_STR: str = "/api/v1"
15+
PROJECT_NAME: str = "FastAPI Auth Tutorial"
16+
17+
class Config:
18+
env_file = ".env"
19+
20+
settings = Settings()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from datetime import datetime, timedelta
2+
from typing import Any, Union, Optional
3+
from jose import jwt, JWTError
4+
from passlib.context import CryptContext
5+
from src.core.config import settings
6+
7+
# Password hashing
8+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
9+
10+
def create_access_token(subject: Union[str, Any], expires_delta: Optional[timedelta] = None) -> str:
11+
"""Create JWT access token"""
12+
if expires_delta:
13+
expire = datetime.now() + expires_delta
14+
else:
15+
expire = datetime.now() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
16+
17+
to_encode = {"exp": expire, "sub": str(subject), "type": "access"}
18+
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
19+
return encoded_jwt
20+
21+
def create_refresh_token(subject: Union[str, Any]) -> str:
22+
"""Create JWT refresh token"""
23+
expire = datetime.now() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
24+
to_encode = {"exp": expire, "sub": str(subject), "type": "refresh"}
25+
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
26+
return encoded_jwt
27+
28+
def verify_token(token: str, token_type: str = "access") -> Optional[str]:
29+
"""Verify JWT token and return subject"""
30+
try:
31+
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
32+
user_id: str = payload.get("sub")
33+
token_type_claim: str = payload.get("type")
34+
35+
if user_id is None or token_type_claim != token_type:
36+
return None
37+
return user_id
38+
except JWTError:
39+
return None
40+
41+
def verify_password(plain_password: str, hashed_password: str) -> bool:
42+
"""Verify a plain password against hashed password"""
43+
return pwd_context.verify(plain_password, hashed_password)
44+
45+
def hash_password(password: str) -> str:
46+
"""Hash a password"""
47+
return pwd_context.hash(password)

workspace/7_framework/fastapi/day11/src/database/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)