Authentication
Authentication is a critical component of most web applications, enabling you to identify users, protect resources, and provide personalized experiences. Nexios provides a flexible, robust authentication system that's easy to implement and customize for your specific needs.
Authentication Fundamentals
Authentication in Nexios provides several key benefits:
- Multiple Backends: Session, JWT, API Key, and custom backends
- Middleware Integration: Automatic user attachment to requests
- Flexible User Models: Support for any user data structure
- Security Best Practices: Built-in protection against common attacks
- Easy Testing: Simple mocking and testing utilities
- Production Ready: Scalable and secure for production use
Security Best Practices
When implementing authentication in your Nexios application, follow these security best practices:
- Use HTTPS: Always use HTTPS in production to protect credentials
- Secure Session Storage: Use secure, encrypted session storage
- JWT Security: Use strong secrets and appropriate expiration times
- API Key Rotation: Implement key rotation for long-lived tokens
- Rate Limiting: Protect authentication endpoints from brute force attacks
- Input Validation: Validate all authentication inputs
- Error Messages: Don't reveal sensitive information in error messages
- Logging: Log authentication events for security monitoring
Authentication Flow
The typical authentication flow in Nexios follows these steps:
- User submits credentials (login form, API key, etc.)
- Backend validates credentials against user database
- Authentication token created (session, JWT, etc.)
- Token stored/sent to client (cookie, header, etc.)
- Subsequent requests include token automatically
- Middleware validates token and attaches user to request
- Handler accesses user via
request.user
Core Components
The Nexios authentication system is built around three core components:
Authentication Middleware
: Processes incoming requests, extracts credentials, and attaches user information to the requestAuthentication Backends
: Validate credentials and retrieve user informationUser Objects
: Represent authenticated and unauthenticated users with consistent interfaces
Basic Authentication Setup
To get started with authentication in Nexios, you need to set up an authentication backend and add the authentication middleware:
from nexios import NexiosApp
from nexios.auth.middleware import AuthenticationMiddleware
from nexios.auth.backends.session import SessionAuthBackend
app = NexiosApp()
async def get_user_by_id(user_id: int):
# Load user by ID from your database
user = await db.get_user(user_id)
if user:
return UserModel(
id=user.id,
username=user.username,
email=user.email
)
return None
# Create authentication backend
auth_backend = SessionAuthBackend(authenticate_func=get_user_by_id)
# Add authentication middleware
app.add_middleware(AuthenticationMiddleware(backend=auth_backend))
Once configured, the authentication system will process each request, attempt to authenticate the user, and make the user object available via request.user
:
@app.get("/profile")
async def profile(request, response):
if request.user.is_authenticated:
return response.json({
"id": request.user.id,
"username": request.user.username,
"email": request.user.email
})
else:
return response.redirect("/login")
@app.get("/admin")
async def admin_panel(request, response):
if not request.user.is_authenticated:
return response.json({"error": "Authentication required"}, status_code=401)
if not request.user.is_admin:
return response.json({"error": "Admin access required"}, status_code=403)
return response.json({"message": "Welcome to admin panel"})
Authentication Middleware
The AuthenticationMiddleware
is responsible for processing each request, delegating to the configured backend for authentication, and attaching the resulting user object to the request:
from nexios.auth.middleware import AuthenticationMiddleware
from nexios.auth.backends.apikey import APIKeyBackend
# Create the authentication backend
api_key_backend = APIKeyBackend(
key_name="X-API-Key",
authenticate_func=get_user_by_api_key
)
# Add authentication middleware with the backend
app.add_middleware(AuthenticationMiddleware(backend=api_key_backend))
Middleware Process Flow
The authentication middleware follows this process for each request:
- Request Arrival: When a request arrives, the middleware intercepts it
- Backend Authentication: The middleware calls the authentication backend's
authenticate
method - User Resolution: If authentication succeeds, a user object is returned along with an authentication type
- Request Attachment: The user is attached to
request.scope["user"]
and accessible viarequest.user
- Auth Type Attachment: An authentication type string is also attached to
request.scope["auth"]
- Fallback: If authentication fails, an
UnauthenticatedUser
instance is attached instead
Multiple Authentication Backends
You can configure multiple authentication backends for different scenarios:
from nexios.auth.middleware import AuthenticationMiddleware
from nexios.auth.backends.session import SessionAuthBackend
from nexios.auth.backends.jwt import JWTAuthBackend
# Create multiple backends
session_backend = SessionAuthBackend(authenticate_func=get_user_by_id)
jwt_backend = JWTAuthBackend(
secret_key="your-secret-key",
authenticate_func=get_user_by_id
)
# Add middleware with multiple backends
app.add_middleware(AuthenticationMiddleware(backends=[session_backend, jwt_backend]))
Authentication Backends
Nexios includes several built-in authentication backends and allows you to create custom backends for specific needs.
Built-in Authentication Backends
1. Session Authentication Backend
Session authentication uses server-side sessions to maintain user state:
from nexios.auth.backends.session import SessionAuthBackend
async def get_user_by_id(user_id: int):
# Load user from database
user = await db.get_user(user_id)
if user:
return UserModel(
id=user.id,
username=user.username,
email=user.email,
is_admin=user.is_admin
)
return None
session_backend = SessionAuthBackend(
user_key="user_id", # Session key for user ID
authenticate_func=get_user_by_id # Function to load user by ID
)
app.add_middleware(AuthenticationMiddleware(backend=session_backend))
Session Backend Features:
- Checks for a user ID stored in the session (typically set during login)
- Loads the full user object using the provided loader function
- Returns an authenticated user if found, or an unauthenticated user otherwise
- Works with any session storage backend (database, Redis, etc.)
Login Handler Example:
@app.post("/login")
async def login(request, response):
data = await request.json
username = data.get("username")
password = data.get("password")
# Validate credentials
user = await validate_credentials(username, password)
if not user:
return response.json({"error": "Invalid credentials"}, status_code=401)
# Store user ID in session
request.session["user_id"] = user.id
return response.json({"message": "Login successful"})
@app.post("/logout")
async def logout(request, response):
# Clear session
request.session.clear()
return response.json({"message": "Logout successful"})
2. JWT Authentication Backend
JWT (JSON Web Token) authentication uses stateless tokens:
from nexios.auth.backends.jwt import JWTAuthBackend
import jwt
async def get_user_by_id(user_id: int):
# Load user from database
user = await db.get_user(user_id)
if user:
return UserModel(
id=user.id,
username=user.username,
email=user.email
)
return None
jwt_backend = JWTAuthBackend(
secret_key="your-super-secret-jwt-key",
algorithm="HS256", # Optional, default is HS256
token_prefix="Bearer", # Optional, default is "Bearer"
authenticate_func=get_user_by_id, # Function to load user by ID
auth_header_name="Authorization" # Optional, default is "Authorization"
)
app.add_middleware(AuthenticationMiddleware(backend=jwt_backend))
JWT Backend Features:
- Extracts a JWT token from the Authorization header
- Validates the token signature, expiration, etc.
- Extracts the user ID from the token claims
- Loads the full user object using the provided loader function
- Supports custom claims and validation
JWT Login Handler Example:
import jwt
from datetime import datetime, timedelta
@app.post("/login")
async def login(request, response):
data = await request.json
username = data.get("username")
password = data.get("password")
# Validate credentials
user = await validate_credentials(username, password)
if not user:
return response.json({"error": "Invalid credentials"}, status_code=401)
# Create JWT token
payload = {
"user_id": user.id,
"username": user.username,
"exp": datetime.utcnow() + timedelta(hours=24),
"iat": datetime.utcnow()
}
token = jwt.encode(payload, "your-super-secret-jwt-key", algorithm="HS256")
return response.json({
"message": "Login successful",
"token": token,
"expires_in": 86400 # 24 hours in seconds
})
@app.post("/refresh")
async def refresh_token(request, response):
if not request.user.is_authenticated:
return response.json({"error": "Authentication required"}, status_code=401)
# Create new token
payload = {
"user_id": request.user.id,
"username": request.user.username,
"exp": datetime.utcnow() + timedelta(hours=24),
"iat": datetime.utcnow()
}
token = jwt.encode(payload, "your-super-secret-jwt-key", algorithm="HS256")
return response.json({
"token": token,
"expires_in": 86400
})
3. API Key Authentication Backend
API key authentication is commonly used for service-to-service communication:
from nexios.auth.backends.apikey import APIKeyBackend
async def get_user_by_api_key(api_key: str):
# Lookup user with the given API key
user = await db.find_user_by_api_key(api_key)
if user:
return UserModel(
id=user.id,
username=user.username,
api_key=api_key,
permissions=user.permissions
)
return None
api_key_backend = APIKeyBackend(
key_name="X-API-Key", # Header containing the API key
authenticate_func=get_user_by_api_key # Function to load user by API key
)
app.add_middleware(AuthenticationMiddleware(backend=api_key_backend))
API Key Backend Features:
- Extracts an API key from the specified header
- Loads the full user object using the provided loader function
- Returns an authenticated user if found, or an unauthenticated user otherwise
- Ideal for service-to-service authentication
API Key Management Example:
@app.post("/api-keys")
async def create_api_key(request, response):
if not request.user.is_authenticated:
return response.json({"error": "Authentication required"}, status_code=401)
# Generate API key
api_key = generate_secure_api_key()
# Store API key in database
await db.store_api_key(request.user.id, api_key)
return response.json({
"api_key": api_key,
"created_at": datetime.utcnow().isoformat()
})
@app.delete("/api-keys/{key_id}")
async def revoke_api_key(request, response):
if not request.user.is_authenticated:
return response.json({"error": "Authentication required"}, status_code=401)
key_id = request.path_params.key_id
# Revoke API key
await db.revoke_api_key(request.user.id, key_id)
return response.json({"message": "API key revoked"})
Creating a Custom Authentication Backend
You can create custom authentication backends by implementing the AuthenticationBackend
abstract base class:
from nexios.auth.base import AuthenticationBackend, BaseUser, UnauthenticatedUser
class CustomUser(BaseUser):
def __init__(self, id, username, is_admin=False):
self.id = id
self.username = username
self.is_admin = is_admin
@property
def is_authenticated(self):
return True
def get_display_name(self):
return self.username
class CustomAuthBackend(AuthenticationBackend):
async def authenticate(self, request, response):
# Extract credentials from the request
custom_header = request.headers.get("X-Custom-Auth")
if not custom_header:
return UnauthenticatedUser()
# Validate custom authentication logic
user = await self.validate_custom_auth(custom_header)
if user:
return user, "custom"
return UnauthenticatedUser()
async def validate_custom_auth(self, auth_header):
# Implement your custom authentication logic
# This could involve checking against a database, external service, etc.
if auth_header == "valid-token":
return CustomUser(id=1, username="custom_user", is_admin=True)
return None
# Use the custom backend
custom_backend = CustomAuthBackend()
app.add_middleware(AuthenticationMiddleware(backend=custom_backend))
User Models
Nexios provides flexible user models that you can extend for your specific needs:
Base User Classes
from nexios.auth.base import BaseUser, UnauthenticatedUser
class AuthenticatedUser(BaseUser):
def __init__(self, id, username, email, permissions=None):
self.id = id
self.username = username
self.email = email
self.permissions = permissions or []
@property
def is_authenticated(self):
return True
def get_display_name(self):
return self.username
def has_permission(self, permission):
return permission in self.permissions
class UnauthenticatedUser(BaseUser):
@property
def is_authenticated(self):
return False
def get_display_name(self):
return "Anonymous"
Custom User Model Example
class User(BaseUser):
def __init__(self, id, username, email, role, is_active=True):
self.id = id
self.username = username
self.email = email
self.role = role
self.is_active = is_active
@property
def is_authenticated(self):
return self.is_active
def get_display_name(self):
return self.username
def is_admin(self):
return self.role == "admin"
def is_moderator(self):
return self.role in ["admin", "moderator"]
def can_access_feature(self, feature):
feature_permissions = {
"admin_panel": ["admin"],
"user_management": ["admin", "moderator"],
"content_creation": ["admin", "moderator", "user"]
}
return self.role in feature_permissions.get(feature, [])
Authentication Decorators
Nexios provides decorators to simplify authentication checks:
from nexios.auth.decorator import login_required, permission_required
@app.get("/profile")
@login_required
async def profile(request, response):
# User is guaranteed to be authenticated here
return response.json({
"id": request.user.id,
"username": request.user.username,
"email": request.user.email
})
@app.get("/admin")
@permission_required("admin")
async def admin_panel(request, response):
# User is guaranteed to have admin permission
return response.json({"message": "Welcome to admin panel"})
@app.get("/moderate")
@permission_required(["admin", "moderator"])
async def moderation_panel(request, response):
# User is guaranteed to have admin or moderator permission
return response.json({"message": "Welcome to moderation panel"})
Error Handling
Authentication Exceptions
from nexios.auth.exceptions import AuthenticationFailed, PermissionDenied
@app.get("/protected")
async def protected_route(request, response):
if not request.user.is_authenticated:
raise AuthenticationFailed("Authentication required")
if not request.user.is_admin():
raise PermissionDenied("Admin access required")
return response.json({"message": "Access granted"})
Custom Error Handlers
@app.add_exception_handler(AuthenticationFailed)
async def handle_auth_failed(request, response, exc):
return response.json({
"error": "Authentication failed",
"message": str(exc)
}, status_code=401)
@app.add_exception_handler(PermissionDenied)
async def handle_permission_denied(request, response, exc):
return response.json({
"error": "Permission denied",
"message": str(exc)
}, status_code=403)
Testing Authentication
Mocking Authentication
import pytest
from nexios.testing import TestClient
from nexios.auth.base import BaseUser
class MockUser(BaseUser):
def __init__(self, id=1, username="test_user", is_admin=False):
self.id = id
self.username = username
self.is_admin = is_admin
@property
def is_authenticated(self):
return True
@pytest.fixture
def authenticated_client():
client = TestClient(app)
# Mock authenticated user
client.app.scope["user"] = MockUser(id=1, username="test_user")
client.app.scope["auth"] = "session"
return client
def test_protected_route(authenticated_client):
response = authenticated_client.get("/profile")
assert response.status_code == 200
assert response.json()["username"] == "test_user"
def test_admin_route(authenticated_client):
# Mock admin user
authenticated_client.app.scope["user"] = MockUser(
id=1, username="admin", is_admin=True
)
response = authenticated_client.get("/admin")
assert response.status_code == 200
Integration Testing
async def test_login_flow(client):
# Test login
login_data = {"username": "test_user", "password": "password123"}
response = await client.post("/login", json=login_data)
assert response.status_code == 200
# Test accessing protected route
response = await client.get("/profile")
assert response.status_code == 200
# Test logout
response = await client.post("/logout")
assert response.status_code == 200
# Test accessing protected route after logout
response = await client.get("/profile")
assert response.status_code == 302 # Redirect to login
Security Considerations
Password Security
import bcrypt
def hash_password(password: str) -> str:
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
def verify_password(password: str, hashed: str) -> bool:
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
@app.post("/register")
async def register(request, response):
data = await request.json
username = data.get("username")
password = data.get("password")
email = data.get("email")
# Hash password before storing
hashed_password = hash_password(password)
# Store user in database
user = await db.create_user(username, hashed_password, email)
return response.json({"message": "User created successfully"}, status_code=201)
Rate Limiting
from nexios.middleware import RateLimitMiddleware
# Add rate limiting to authentication endpoints
app.add_middleware(RateLimitMiddleware(
rate_limit=5, # 5 requests
time_window=60 # per minute
))
@app.post("/login")
async def login(request, response):
# Login logic here
pass
Session Security
from nexios.session import SessionMiddleware
# Configure secure session middleware
app.add_middleware(SessionMiddleware(
secret_key="your-secret-key",
max_age=3600, # 1 hour
secure=True, # HTTPS only
httponly=True, # Prevent XSS
samesite="strict" # Prevent CSRF
))
This comprehensive authentication guide covers all aspects of implementing secure authentication in Nexios applications. The authentication system is designed to be flexible, secure, and easy to use while providing the power to handle complex authentication scenarios.