669 lines
21 KiB
Markdown
669 lines
21 KiB
Markdown
# 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. |