mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
452 lines
15 KiB
Plaintext
452 lines
15 KiB
Plaintext
---
|
|
title: "Backend Integration"
|
|
description: "Learn how to integrate Stack Auth's backend into your Python application"
|
|
---
|
|
|
|
To authenticate your Python server endpoints, you need to send the user's access token in the headers of the request to your server, and then make a request to Stack Auth's server API to verify the user identity.
|
|
|
|
## Sending requests to your server endpoints
|
|
|
|
On the client side (frontend), you can retrieve the access token from the `user` object by calling `user.getAuthJson()`. This will return an object containing `accessToken`.
|
|
|
|
Then, you can call your Python server endpoint with the access token in the headers:
|
|
|
|
```typescript
|
|
// Frontend code
|
|
const { accessToken } = await user.getAuthJson();
|
|
const response = await fetch('/api/users/me', {
|
|
headers: {
|
|
'X-Stack-Access-Token': accessToken,
|
|
},
|
|
// your other options and parameters
|
|
});
|
|
```
|
|
|
|
## Authenticating users on Python server endpoints
|
|
|
|
Stack Auth provides two methods for authenticating users on your Python server endpoints:
|
|
|
|
1. **JWT Verification**: A fast, lightweight approach that validates the user's token locally without making external requests. Ideal for high-performance applications.
|
|
2. **REST API Verification**: Makes a request to Stack Auth's servers to validate the token and retrieve comprehensive user information. Best when you need complete, up-to-date user data.
|
|
|
|
### Using JWT
|
|
|
|
JWT verification is faster and reduces external dependencies. Install the required packages:
|
|
|
|
```bash
|
|
pip install PyJWT[crypto] requests
|
|
```
|
|
|
|
Here's how to implement JWT verification in your Python backend:
|
|
|
|
```python
|
|
import jwt
|
|
from jwt import PyJWKClient
|
|
from jwt.exceptions import InvalidTokenError
|
|
|
|
# You can cache this and refresh it with a low frequency
|
|
jwks_client = PyJWKClient("https://api.stack-auth.com/api/v1/projects/<your-project-id>/.well-known/jwks.json")
|
|
|
|
def verify_jwt_token(access_token):
|
|
"""
|
|
Verify JWT token and extract user information
|
|
Returns user data or None if invalid
|
|
"""
|
|
try:
|
|
signing_key = jwks_client.get_signing_key_from_jwt(access_token)
|
|
payload = jwt.decode(
|
|
access_token,
|
|
signing_key.key,
|
|
algorithms=["ES256"],
|
|
audience="<your-project-id>",
|
|
|
|
)
|
|
|
|
return {
|
|
'user_id': payload['sub'],
|
|
'is_anonymous': payload.get('role') == 'anon'
|
|
}
|
|
except InvalidTokenError:
|
|
return None
|
|
except Exception:
|
|
return None
|
|
|
|
# Example usage
|
|
access_token = 'access token from the headers'
|
|
user_data = verify_jwt_token(access_token)
|
|
if user_data:
|
|
print(f'Authenticated user with ID: {user_data["user_id"]}')
|
|
else:
|
|
print('Invalid user')
|
|
```
|
|
|
|
Now you can use this JWT verification in your Python web framework. Here are examples for different frameworks:
|
|
|
|
<Tabs defaultValue="flask">
|
|
<TabsList>
|
|
<TabsTrigger value="flask">Flask</TabsTrigger>
|
|
<TabsTrigger value="fastapi">FastAPI</TabsTrigger>
|
|
<TabsTrigger value="django">Django</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="flask">
|
|
```python
|
|
from flask import Flask, request, jsonify
|
|
from functools import wraps
|
|
|
|
app = Flask(__name__)
|
|
|
|
def authenticate_user(request):
|
|
"""Extract and verify access token from request headers"""
|
|
access_token = request.headers.get('X-Stack-Access-Token')
|
|
if not access_token:
|
|
return None
|
|
|
|
return verify_jwt_token(access_token)
|
|
|
|
def require_auth(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
user = authenticate_user(request)
|
|
if not user:
|
|
return jsonify({'error': 'Unauthorized'}), 401
|
|
return f(user, *args, **kwargs)
|
|
return decorated_function
|
|
|
|
@app.route('/api/users/me')
|
|
@require_auth
|
|
def get_current_user(user):
|
|
return jsonify({
|
|
'user_id': user['user_id'],
|
|
'is_anonymous': user['is_anonymous']
|
|
})
|
|
```
|
|
</TabsContent>
|
|
<TabsContent value="fastapi">
|
|
```python
|
|
from fastapi import FastAPI, HTTPException, Depends, Header
|
|
from typing import Optional
|
|
|
|
app = FastAPI()
|
|
|
|
async def get_current_user(x_stack_access_token: Optional[str] = Header(None)):
|
|
if not x_stack_access_token:
|
|
raise HTTPException(status_code=401, detail="Access token required")
|
|
|
|
user_data = verify_jwt_token(x_stack_access_token)
|
|
if not user_data:
|
|
raise HTTPException(status_code=401, detail="Invalid access token")
|
|
|
|
return user_data
|
|
|
|
@app.get("/api/users/me")
|
|
async def read_current_user(user: dict = Depends(get_current_user)):
|
|
return {
|
|
"user_id": user["user_id"],
|
|
"is_anonymous": user["is_anonymous"]
|
|
}
|
|
```
|
|
</TabsContent>
|
|
<TabsContent value="django">
|
|
```python
|
|
from django.http import JsonResponse
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
def authenticate_user(request):
|
|
"""Extract and verify access token from request headers"""
|
|
access_token = request.META.get('HTTP_X_STACK_ACCESS_TOKEN')
|
|
if not access_token:
|
|
return None
|
|
|
|
return verify_jwt_token(access_token)
|
|
|
|
@csrf_exempt
|
|
def protected_view(request):
|
|
user = authenticate_user(request)
|
|
if not user:
|
|
return JsonResponse({'error': 'Unauthorized'}, status=401)
|
|
|
|
return JsonResponse({
|
|
'user_id': user['user_id'],
|
|
'is_anonymous': user['is_anonymous']
|
|
})
|
|
|
|
# Or as a decorator
|
|
def auth_required(view_func):
|
|
def wrapper(request, *args, **kwargs):
|
|
user = authenticate_user(request)
|
|
if not user:
|
|
return JsonResponse({'error': 'Unauthorized'}, status=401)
|
|
request.user = user
|
|
return view_func(request, *args, **kwargs)
|
|
return wrapper
|
|
|
|
@auth_required
|
|
def my_protected_view(request):
|
|
return JsonResponse({'message': f'Hello, {request.user["user_id"]}!'})
|
|
```
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
### Using the REST API
|
|
|
|
For cases where you need complete user information including email, you can use the `stack_auth_request` helper function from the [setup guide](../getting-started/setup):
|
|
|
|
```python
|
|
def authenticate_user_with_api(access_token):
|
|
"""
|
|
Authenticate user and get complete profile via REST API
|
|
Returns full user information including email, display name, etc.
|
|
"""
|
|
try:
|
|
user_data = stack_auth_request('GET', 'api/v1/users/me', headers={
|
|
'x-stack-access-token': access_token
|
|
})
|
|
return {
|
|
'id': user_data['id'],
|
|
'display_name': user_data['display_name'],
|
|
'primary_email': user_data['primary_email'],
|
|
'primary_email_verified': user_data['primary_email_verified'],
|
|
'profile_image_url': user_data['profile_image_url'],
|
|
'signed_up_at_millis': user_data['signed_up_at_millis'],
|
|
'last_active_at_millis': user_data['last_active_at_millis'],
|
|
'has_password': user_data['has_password'],
|
|
'is_anonymous': user_data['is_anonymous'],
|
|
'oauth_providers': user_data['oauth_providers']
|
|
}
|
|
except Exception as e:
|
|
print(f"Authentication failed: {e}")
|
|
return None
|
|
|
|
# Example usage
|
|
access_token = request.headers.get('X-Stack-Access-Token')
|
|
if access_token:
|
|
user_info = authenticate_user_with_api(access_token)
|
|
if user_info:
|
|
print(f"Authenticated user: {user_info['primary_email']}")
|
|
print(f"Display name: {user_info['display_name']}")
|
|
else:
|
|
print("Authentication failed")
|
|
```
|
|
|
|
## Environment Configuration
|
|
|
|
As shown in the [setup guide](../getting-started/setup), make sure you have your Stack Auth credentials configured:
|
|
|
|
```python
|
|
import os
|
|
|
|
stack_project_id = os.getenv("STACK_PROJECT_ID")
|
|
stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY")
|
|
stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")
|
|
```
|
|
|
|
And ensure you have the `stack_auth_request` helper function available from the setup guide.
|
|
|
|
## Error Handling Best Practices
|
|
|
|
```python
|
|
from enum import Enum
|
|
from django.http import JsonResponse
|
|
|
|
class AuthError(Enum):
|
|
MISSING_TOKEN = "Access token required"
|
|
INVALID_TOKEN = "Invalid or expired access token"
|
|
SERVER_ERROR = "Authentication server error"
|
|
|
|
def safe_authenticate_user(request):
|
|
"""
|
|
Robust authentication with proper error handling
|
|
"""
|
|
access_token = request.headers.get('X-Stack-Access-Token')
|
|
|
|
if not access_token:
|
|
return None, AuthError.MISSING_TOKEN
|
|
|
|
try:
|
|
user_data = verify_jwt_token(access_token)
|
|
if user_data:
|
|
return user_data, None
|
|
else:
|
|
return None, AuthError.INVALID_TOKEN
|
|
except Exception as e:
|
|
print(f"Authentication error: {e}")
|
|
return None, AuthError.SERVER_ERROR
|
|
|
|
# Usage in your endpoints
|
|
def protected_endpoint(request):
|
|
user, error = safe_authenticate_user(request)
|
|
|
|
if error:
|
|
return JsonResponse({'error': error.value}, status=401)
|
|
|
|
# User is authenticated, proceed with your logic
|
|
return JsonResponse({'user': user})
|
|
```
|
|
|
|
## Complete Backend Integration Example
|
|
|
|
Here's a comprehensive example that demonstrates both JWT and REST API authentication working together:
|
|
|
|
```python
|
|
import os
|
|
import jwt
|
|
import requests
|
|
from jwt import PyJWKClient
|
|
from jwt.exceptions import InvalidTokenError
|
|
from enum import Enum
|
|
|
|
# Setup (from setup guide)
|
|
stack_project_id = os.getenv("STACK_PROJECT_ID")
|
|
stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY")
|
|
stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")
|
|
|
|
if not stack_project_id:
|
|
raise RuntimeError("STACK_PROJECT_ID is not set")
|
|
|
|
def stack_auth_request(method, endpoint, **kwargs):
|
|
res = requests.request(
|
|
method,
|
|
f'https://api.stack-auth.com/{endpoint}',
|
|
headers={
|
|
'x-stack-access-type': 'server',
|
|
'x-stack-project-id': stack_project_id,
|
|
'x-stack-publishable-client-key': stack_publishable_client_key,
|
|
'x-stack-secret-server-key': stack_secret_server_key,
|
|
**kwargs.pop('headers', {}),
|
|
},
|
|
timeout=10,
|
|
**kwargs,
|
|
)
|
|
if res.status_code >= 400:
|
|
raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}")
|
|
return res.json()
|
|
|
|
# JWT verification setup
|
|
jwks_client = PyJWKClient(f"https://api.stack-auth.com/api/v1/projects/{stack_project_id}/.well-known/jwks.json")
|
|
|
|
def verify_jwt_token(access_token):
|
|
"""Fast JWT verification - returns basic user info"""
|
|
try:
|
|
signing_key = jwks_client.get_signing_key_from_jwt(access_token)
|
|
payload = jwt.decode(
|
|
access_token,
|
|
signing_key.key,
|
|
algorithms=["ES256"],
|
|
audience=stack_project_id
|
|
)
|
|
|
|
return {
|
|
'user_id': payload['sub'],
|
|
'is_anonymous': payload.get('role') == 'anon'
|
|
}
|
|
except (InvalidTokenError, Exception):
|
|
return None
|
|
|
|
def get_full_user_info(access_token):
|
|
"""REST API call - returns complete user profile"""
|
|
try:
|
|
user_data = stack_auth_request('GET', 'api/v1/users/me', headers={
|
|
'x-stack-access-token': access_token
|
|
})
|
|
return user_data
|
|
except Exception:
|
|
return None
|
|
|
|
class AuthenticationService:
|
|
@staticmethod
|
|
def authenticate_request(request, require_full_profile=False):
|
|
"""
|
|
Authenticate a request with optional full profile retrieval
|
|
|
|
Args:
|
|
request: The HTTP request object
|
|
require_full_profile: If True, fetches complete user info via REST API
|
|
|
|
Returns:
|
|
User data dictionary or None if authentication fails
|
|
"""
|
|
access_token = request.headers.get('X-Stack-Access-Token')
|
|
if not access_token:
|
|
return None
|
|
|
|
if require_full_profile:
|
|
# Use REST API for complete user information
|
|
return get_full_user_info(access_token)
|
|
else:
|
|
# Use JWT for fast authentication
|
|
return verify_jwt_token(access_token)
|
|
|
|
@staticmethod
|
|
def require_auth(require_full_profile=False):
|
|
"""Decorator for protecting endpoints"""
|
|
def decorator(func):
|
|
def wrapper(request, *args, **kwargs):
|
|
user = AuthenticationService.authenticate_request(request, require_full_profile)
|
|
if not user:
|
|
return {'error': 'Unauthorized'}, 401
|
|
return func(request, user, *args, **kwargs)
|
|
return wrapper
|
|
return decorator
|
|
|
|
# Example usage in different scenarios
|
|
@AuthenticationService.require_auth(require_full_profile=False)
|
|
def fast_protected_endpoint(request, user):
|
|
"""Fast endpoint using JWT verification"""
|
|
return {
|
|
'message': f'Hello user {user["user_id"]}!',
|
|
'is_anonymous': user['is_anonymous']
|
|
}
|
|
|
|
@AuthenticationService.require_auth(require_full_profile=True)
|
|
def profile_endpoint(request, user):
|
|
"""Endpoint that needs complete user info"""
|
|
return {
|
|
'user_id': user['id'],
|
|
'display_name': user['display_name'],
|
|
'email': user['primary_email'],
|
|
'email_verified': user['primary_email_verified'],
|
|
'profile_image': user['profile_image_url'],
|
|
'is_anonymous': user['is_anonymous']
|
|
}
|
|
|
|
# Error handling example
|
|
class AuthError(Enum):
|
|
MISSING_TOKEN = "Access token required"
|
|
INVALID_TOKEN = "Invalid or expired access token"
|
|
SERVER_ERROR = "Authentication server error"
|
|
|
|
def safe_authenticate(request, require_full_profile=False):
|
|
"""Authentication with comprehensive error handling"""
|
|
access_token = request.headers.get('X-Stack-Access-Token')
|
|
|
|
if not access_token:
|
|
return None, AuthError.MISSING_TOKEN
|
|
|
|
try:
|
|
if require_full_profile:
|
|
user_data = get_full_user_info(access_token)
|
|
else:
|
|
user_data = verify_jwt_token(access_token)
|
|
|
|
if user_data:
|
|
return user_data, None
|
|
else:
|
|
return None, AuthError.INVALID_TOKEN
|
|
except Exception as e:
|
|
print(f"Authentication error: {e}")
|
|
return None, AuthError.SERVER_ERROR
|
|
```
|
|
|
|
## Performance Considerations
|
|
|
|
- **JWT Verification**: Faster, no external requests, but limited user data (only `user_id` and `is_anonymous`)
|
|
- **REST API Verification**: Slower, requires network calls, but provides complete user information including email, profile, etc.
|
|
- **Hybrid Approach**: Use JWT for basic authentication, then fetch full profile only when needed
|
|
- **Caching**: Consider caching JWKs and user data for better performance
|
|
- **Connection Pooling**: Use session objects for REST API calls to reuse connections
|
|
|
|
Choose the appropriate method based on your endpoint's requirements:
|
|
- Use **JWT** for high-performance endpoints that only need user ID
|
|
- Use **REST API** when you need complete user profiles, email verification status, etc.
|
|
- Use **hybrid approach** to optimize performance while maintaining flexibility
|