initial commit

This commit is contained in:
2025-10-19 22:09:35 +03:00
commit 6d593b4554
114 changed files with 23622 additions and 0 deletions

View File

@ -0,0 +1,669 @@
# JWT Tokens
JSON Web Tokens (JWT) form the backbone of modern web authentication. This comprehensive guide explains how the boilerplate implements a secure, stateless authentication system using access and refresh tokens.
## Understanding JWT Authentication
JWT tokens are self-contained, digitally signed packages of information that can be safely transmitted between parties. Unlike traditional session-based authentication that requires server-side storage, JWT tokens are stateless - all the information needed to verify a user's identity is contained within the token itself.
### Why Use JWT?
**Stateless Design**: No need to store session data on the server, making it perfect for distributed systems and microservices.
**Scalability**: Since tokens contain all necessary information, they work seamlessly across multiple servers without shared session storage.
**Security**: Digital signatures ensure tokens can't be tampered with, and expiration times limit exposure if compromised.
**Cross-Domain Support**: Unlike cookies, JWT tokens work across different domains and can be used in mobile applications.
## Token Types
The authentication system uses a **dual-token approach** for maximum security and user experience:
### Access Tokens
Access tokens are short-lived credentials that prove a user's identity for API requests. Think of them as temporary keys that grant access to protected resources.
- **Purpose**: Authenticate API requests and authorize actions
- **Lifetime**: 30 minutes (configurable) - short enough to limit damage if compromised
- **Storage**: Authorization header (`Bearer <token>`) - sent with each API request
- **Usage**: Include in every call to protected endpoints
**Why Short-Lived?** If an access token is stolen (e.g., through XSS), the damage window is limited to 30 minutes before it expires naturally.
### Refresh Tokens
Refresh tokens are longer-lived credentials used solely to generate new access tokens. They provide a balance between security and user convenience.
- **Purpose**: Generate new access tokens without requiring re-login
- **Lifetime**: 7 days (configurable) - long enough for good UX, short enough for security
- **Storage**: Secure HTTP-only cookie - inaccessible to JavaScript, preventing XSS attacks
- **Usage**: Automatically used by the browser when access tokens need refreshing
**Why HTTP-Only Cookies?** This prevents malicious JavaScript from accessing refresh tokens, providing protection against XSS attacks while allowing automatic renewal.
## Token Creation
Understanding how tokens are created helps you customize the authentication system for your specific needs.
### Creating Access Tokens
Access tokens are generated during login and token refresh operations. The process involves encoding user information with an expiration time and signing it with your secret key.
```python
from datetime import timedelta
from app.core.security import create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES
# Basic access token with default expiration
access_token = await create_access_token(data={"sub": username})
# Custom expiration for special cases (e.g., admin sessions)
custom_expires = timedelta(minutes=60)
access_token = await create_access_token(
data={"sub": username},
expires_delta=custom_expires
)
```
**When to Customize Expiration:**
- **High-security environments**: Shorter expiration (15 minutes)
- **Development/testing**: Longer expiration for convenience
- **Admin operations**: Variable expiration based on sensitivity
### Creating Refresh Tokens
Refresh tokens follow the same creation pattern but with longer expiration times. They're typically created only during login.
```python
from app.core.security import create_refresh_token, REFRESH_TOKEN_EXPIRE_DAYS
# Standard refresh token
refresh_token = await create_refresh_token(data={"sub": username})
# Extended refresh token for "remember me" functionality
extended_expires = timedelta(days=30)
refresh_token = await create_refresh_token(
data={"sub": username},
expires_delta=extended_expires
)
```
### Token Structure
JWT tokens consist of three parts separated by dots: `header.payload.signature`. The payload contains the actual user information and metadata.
```python
# Access token payload structure
{
"sub": "username", # Subject (user identifier)
"exp": 1234567890, # Expiration timestamp (Unix)
"token_type": "access", # Distinguishes from refresh tokens
"iat": 1234567890 # Issued at (automatic)
}
# Refresh token payload structure
{
"sub": "username", # Same user identifier
"exp": 1234567890, # Longer expiration time
"token_type": "refresh", # Prevents confusion/misuse
"iat": 1234567890 # Issue timestamp
}
```
**Key Fields Explained:**
- **`sub` (Subject)**: Identifies the user - can be username, email, or user ID
- **`exp` (Expiration)**: Unix timestamp when token becomes invalid
- **`token_type`**: Custom field preventing tokens from being used incorrectly
- **`iat` (Issued At)**: Useful for token rotation and audit trails
## Token Verification
Token verification is a multi-step process that ensures both the token's authenticity and the user's current authorization status.
### Verifying Access Tokens
Every protected endpoint must verify the access token before processing the request. This involves checking the signature, expiration, and blacklist status.
```python
from app.core.security import verify_token, TokenType
# Verify access token in endpoint
token_data = await verify_token(token, TokenType.ACCESS, db)
if token_data:
username = token_data.username_or_email
# Token is valid, proceed with request processing
else:
# Token is invalid, expired, or blacklisted
raise UnauthorizedException("Invalid or expired token")
```
### Verifying Refresh Tokens
Refresh token verification follows the same process but with different validation rules and outcomes.
```python
# Verify refresh token for renewal
token_data = await verify_token(token, TokenType.REFRESH, db)
if token_data:
# Generate new access token
new_access_token = await create_access_token(
data={"sub": token_data.username_or_email}
)
return {"access_token": new_access_token, "token_type": "bearer"}
else:
# Refresh token invalid - user must log in again
raise UnauthorizedException("Invalid refresh token")
```
### Token Verification Process
The verification process includes several security checks to prevent various attack vectors:
```python
async def verify_token(token: str, expected_token_type: TokenType, db: AsyncSession) -> TokenData | None:
# 1. Check blacklist first (prevents use of logged-out tokens)
is_blacklisted = await crud_token_blacklist.exists(db, token=token)
if is_blacklisted:
return None
try:
# 2. Verify signature and decode payload
payload = jwt.decode(token, SECRET_KEY.get_secret_value(), algorithms=[ALGORITHM])
# 3. Extract and validate claims
username_or_email: str | None = payload.get("sub")
token_type: str | None = payload.get("token_type")
# 4. Ensure token type matches expectation
if username_or_email is None or token_type != expected_token_type:
return None
# 5. Return validated data
return TokenData(username_or_email=username_or_email)
except JWTError:
# Token is malformed, expired, or signature invalid
return None
```
**Security Checks Explained:**
1. **Blacklist Check**: Prevents use of tokens from logged-out users
2. **Signature Verification**: Ensures token hasn't been tampered with
3. **Expiration Check**: Automatically handled by JWT library
4. **Type Validation**: Prevents refresh tokens from being used as access tokens
5. **Subject Validation**: Ensures token contains valid user identifier
## Client-Side Authentication Flow
Understanding the complete authentication flow helps frontend developers integrate properly with the API.
### Recommended Client Flow
**1. Login Process**
```javascript
// Send credentials to login endpoint
const response = await fetch('/api/v1/login', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'username=user&password=pass',
credentials: 'include' // Important: includes cookies
});
const { access_token, token_type } = await response.json();
// Store access token in memory (not localStorage)
sessionStorage.setItem('access_token', access_token);
```
**2. Making Authenticated Requests**
```javascript
// Include access token in Authorization header
const response = await fetch('/api/v1/protected-endpoint', {
headers: {
'Authorization': `Bearer ${sessionStorage.getItem('access_token')}`
},
credentials: 'include'
});
```
**3. Handling Token Expiration**
```javascript
// Automatic token refresh on 401 errors
async function apiCall(url, options = {}) {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${sessionStorage.getItem('access_token')}`
},
credentials: 'include'
});
// If token expired, try to refresh
if (response.status === 401) {
const refreshResponse = await fetch('/api/v1/refresh', {
method: 'POST',
credentials: 'include' // Sends refresh token cookie
});
if (refreshResponse.ok) {
const { access_token } = await refreshResponse.json();
sessionStorage.setItem('access_token', access_token);
// Retry original request
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${access_token}`
},
credentials: 'include'
});
} else {
// Refresh failed - redirect to login
window.location.href = '/login';
}
}
return response;
}
```
**4. Logout Process**
```javascript
// Clear tokens and call logout endpoint
await fetch('/api/v1/logout', {
method: 'POST',
credentials: 'include'
});
sessionStorage.removeItem('access_token');
// Refresh token cookie is cleared by server
```
### Cookie Configuration
The refresh token cookie is configured for maximum security:
```python
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True, # Prevents JavaScript access (XSS protection)
secure=True, # HTTPS only in production
samesite="Lax", # CSRF protection with good usability
max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
)
```
**SameSite Options:**
- **`Lax`** (Recommended): Cookies sent on top-level navigation but not cross-site requests
- **`Strict`**: Maximum security but may break some user flows
- **`None`**: Required for cross-origin requests (must use with Secure)
## Token Blacklisting
Token blacklisting solves a fundamental problem with JWT tokens: once issued, they remain valid until expiration, even if the user logs out. Blacklisting provides immediate token revocation.
### Why Blacklisting Matters
Without blacklisting, logged-out users could continue accessing your API until their tokens naturally expire. This creates security risks, especially on shared computers or if tokens are compromised.
### Blacklisting Implementation
The system uses a database table to track invalidated tokens:
```python
# models/token_blacklist.py
class TokenBlacklist(Base):
__tablename__ = "token_blacklist"
id: Mapped[int] = mapped_column(primary_key=True)
token: Mapped[str] = mapped_column(unique=True, index=True) # Full token string
expires_at: Mapped[datetime] = mapped_column() # When to clean up
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
```
**Design Considerations:**
- **Unique constraint**: Prevents duplicate entries
- **Index on token**: Fast lookup during verification
- **Expires_at field**: Enables automatic cleanup of old entries
### Blacklisting Tokens
The system provides functions for both single token and dual token blacklisting:
```python
from app.core.security import blacklist_token, blacklist_tokens
# Single token blacklisting (for specific scenarios)
await blacklist_token(token, db)
# Dual token blacklisting (standard logout)
await blacklist_tokens(access_token, refresh_token, db)
```
### Blacklisting Process
The blacklisting process extracts the expiration time from the token to set an appropriate cleanup schedule:
```python
async def blacklist_token(token: str, db: AsyncSession) -> None:
# 1. Decode token to extract expiration (no verification needed)
payload = jwt.decode(token, SECRET_KEY.get_secret_value(), algorithms=[ALGORITHM])
exp_timestamp = payload.get("exp")
if exp_timestamp is not None:
# 2. Convert Unix timestamp to datetime
expires_at = datetime.fromtimestamp(exp_timestamp)
# 3. Store in blacklist with expiration
await crud_token_blacklist.create(
db,
object=TokenBlacklistCreate(token=token, expires_at=expires_at)
)
```
**Cleanup Strategy**: Blacklisted tokens can be automatically removed from the database after their natural expiration time, preventing unlimited database growth.
## Login Flow Implementation
### Complete Login Endpoint
```python
@router.post("/login", response_model=Token)
async def login_for_access_token(
response: Response,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Annotated[AsyncSession, Depends(async_get_db)],
) -> dict[str, str]:
# 1. Authenticate user
user = await authenticate_user(
username_or_email=form_data.username,
password=form_data.password,
db=db
)
if not user:
raise HTTPException(
status_code=401,
detail="Incorrect username or password"
)
# 2. Create access token
access_token = await create_access_token(data={"sub": user["username"]})
# 3. Create refresh token
refresh_token = await create_refresh_token(data={"sub": user["username"]})
# 4. Set refresh token as HTTP-only cookie
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=True,
samesite="strict",
max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
)
return {"access_token": access_token, "token_type": "bearer"}
```
### Token Refresh Endpoint
```python
@router.post("/refresh", response_model=Token)
async def refresh_access_token(
response: Response,
db: Annotated[AsyncSession, Depends(async_get_db)],
refresh_token: str = Cookie(None)
) -> dict[str, str]:
if not refresh_token:
raise HTTPException(status_code=401, detail="Refresh token missing")
# 1. Verify refresh token
token_data = await verify_token(refresh_token, TokenType.REFRESH, db)
if not token_data:
raise HTTPException(status_code=401, detail="Invalid refresh token")
# 2. Create new access token
new_access_token = await create_access_token(
data={"sub": token_data.username_or_email}
)
# 3. Optionally create new refresh token (token rotation)
new_refresh_token = await create_refresh_token(
data={"sub": token_data.username_or_email}
)
# 4. Blacklist old refresh token
await blacklist_token(refresh_token, db)
# 5. Set new refresh token cookie
response.set_cookie(
key="refresh_token",
value=new_refresh_token,
httponly=True,
secure=True,
samesite="strict",
max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
)
return {"access_token": new_access_token, "token_type": "bearer"}
```
### Logout Implementation
```python
@router.post("/logout")
async def logout(
response: Response,
db: Annotated[AsyncSession, Depends(async_get_db)],
current_user: dict = Depends(get_current_user),
token: str = Depends(oauth2_scheme),
refresh_token: str = Cookie(None)
) -> dict[str, str]:
# 1. Blacklist access token
await blacklist_token(token, db)
# 2. Blacklist refresh token if present
if refresh_token:
await blacklist_token(refresh_token, db)
# 3. Clear refresh token cookie
response.delete_cookie(
key="refresh_token",
httponly=True,
secure=True,
samesite="strict"
)
return {"message": "Successfully logged out"}
```
## Authentication Dependencies
### get_current_user
```python
async def get_current_user(
db: AsyncSession = Depends(async_get_db),
token: str = Depends(oauth2_scheme)
) -> dict:
# 1. Verify token
token_data = await verify_token(token, TokenType.ACCESS, db)
if not token_data:
raise HTTPException(status_code=401, detail="Invalid token")
# 2. Get user from database
user = await crud_users.get(
db=db,
username=token_data.username_or_email,
schema_to_select=UserRead
)
if user is None:
raise HTTPException(status_code=401, detail="User not found")
return user
```
### get_optional_user
```python
async def get_optional_user(
db: AsyncSession = Depends(async_get_db),
token: str = Depends(optional_oauth2_scheme)
) -> dict | None:
if not token:
return None
try:
return await get_current_user(db=db, token=token)
except HTTPException:
return None
```
### get_current_superuser
```python
async def get_current_superuser(
current_user: dict = Depends(get_current_user)
) -> dict:
if not current_user.get("is_superuser", False):
raise HTTPException(
status_code=403,
detail="Not enough permissions"
)
return current_user
```
## Configuration
### Environment Variables
```bash
# JWT Configuration
SECRET_KEY=your-secret-key-here
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7
# Security Headers
SECURE_COOKIES=true
CORS_ORIGINS=["http://localhost:3000", "https://yourapp.com"]
```
### Security Configuration
```python
# app/core/config.py
class Settings(BaseSettings):
SECRET_KEY: SecretStr
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
# Cookie settings
SECURE_COOKIES: bool = True
COOKIE_DOMAIN: str | None = None
COOKIE_SAMESITE: str = "strict"
```
## Security Best Practices
### Token Security
- **Use strong secrets**: Generate cryptographically secure SECRET_KEY
- **Rotate secrets**: Regularly change SECRET_KEY in production
- **Environment separation**: Different secrets for dev/staging/production
- **Secure transmission**: Always use HTTPS in production
### Cookie Security
- **HttpOnly flag**: Prevents JavaScript access to refresh tokens
- **Secure flag**: Ensures cookies only sent over HTTPS
- **SameSite attribute**: Prevents CSRF attacks
- **Domain restrictions**: Set cookie domain appropriately
### Implementation Security
- **Input validation**: Validate all token inputs
- **Rate limiting**: Implement login attempt limits
- **Audit logging**: Log authentication events
- **Token rotation**: Regularly refresh tokens
## Common Patterns
### API Key Authentication
For service-to-service communication:
```python
async def get_api_key_user(
api_key: str = Header(None),
db: AsyncSession = Depends(async_get_db)
) -> dict:
if not api_key:
raise HTTPException(status_code=401, detail="API key required")
# Verify API key
user = await crud_users.get(db=db, api_key=api_key)
if not user:
raise HTTPException(status_code=401, detail="Invalid API key")
return user
```
### Multiple Authentication Methods
```python
async def get_authenticated_user(
db: AsyncSession = Depends(async_get_db),
token: str = Depends(optional_oauth2_scheme),
api_key: str = Header(None)
) -> dict:
# Try JWT token first
if token:
try:
return await get_current_user(db=db, token=token)
except HTTPException:
pass
# Fall back to API key
if api_key:
return await get_api_key_user(api_key=api_key, db=db)
raise HTTPException(status_code=401, detail="Authentication required")
```
## Troubleshooting
### Common Issues
**Token Expired**: Implement automatic refresh using refresh tokens
**Invalid Signature**: Check SECRET_KEY consistency across environments
**Blacklisted Token**: User logged out - redirect to login
**Missing Token**: Ensure Authorization header is properly set
### Debugging Tips
```python
# Enable debug logging
import logging
logging.getLogger("app.core.security").setLevel(logging.DEBUG)
# Test token validation
async def debug_token(token: str, db: AsyncSession):
try:
payload = jwt.decode(token, SECRET_KEY.get_secret_value(), algorithms=[ALGORITHM])
print(f"Token payload: {payload}")
is_blacklisted = await crud_token_blacklist.exists(db, token=token)
print(f"Is blacklisted: {is_blacklisted}")
except JWTError as e:
print(f"JWT Error: {e}")
```
This comprehensive JWT implementation provides secure, scalable authentication for your FastAPI application.