mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
253 lines
7.9 KiB
Plaintext
253 lines
7.9 KiB
Plaintext
---
|
|
title: Authentication
|
|
---
|
|
|
|
This guide covers how to implement authentication in your Python application using Stack Auth. We'll walk through all the available authentication methods and show you how to handle user sessions.
|
|
|
|
## Overview
|
|
|
|
Stack Auth supports multiple authentication methods that you can enable or disable based on your needs:
|
|
|
|
- **[Email/Password](./password.mdx)** - Traditional email and password authentication
|
|
- **[Magic Links (OTP)](./magic-links.mdx)** - Passwordless authentication via email
|
|
- **[OAuth Providers](./oauth.mdx)** - Sign in with Google, GitHub, Facebook, and more
|
|
- **[Passkeys](./passkeys.mdx)** - Modern biometric authentication
|
|
- **[Anonymous Users](./anonymous.mdx)** - Guest users that can be upgraded later
|
|
|
|
## Basic Authentication Flow
|
|
|
|
All authentication methods in Stack Auth follow a similar pattern:
|
|
|
|
1. **Initiate authentication** - Start the sign-in/sign-up process
|
|
2. **User verification** - User completes authentication (password, email link, OAuth, etc.)
|
|
3. **Receive tokens** - Get access and refresh tokens
|
|
4. **Manage session** - Use tokens to authenticate API requests
|
|
|
|
Here's the basic structure for handling authentication:
|
|
|
|
```python
|
|
from typing import Optional
|
|
import requests
|
|
|
|
class AuthManager:
|
|
def __init__(self, stack_auth_client):
|
|
self.client = stack_auth_client
|
|
self.current_user_token: Optional[str] = None
|
|
|
|
def authenticate_request(self, access_token: str) -> dict:
|
|
"""Verify and get user info from access token"""
|
|
return self.client._make_request(
|
|
'GET',
|
|
'/api/v1/users/me',
|
|
access_type="client",
|
|
access_token=access_token
|
|
)
|
|
|
|
def refresh_token(self, refresh_token: str) -> dict:
|
|
"""Refresh an expired access token"""
|
|
return self.client._make_request(
|
|
'POST',
|
|
'/api/v1/auth/sessions/current/refresh',
|
|
access_type="client",
|
|
json={'refresh_token': refresh_token}
|
|
)
|
|
|
|
def sign_out(self, access_token: str) -> bool:
|
|
"""Sign out the current user"""
|
|
try:
|
|
self.client._make_request(
|
|
'DELETE',
|
|
'/api/v1/auth/sessions/current',
|
|
access_type="client",
|
|
access_token=access_token
|
|
)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
# Initialize auth manager
|
|
auth_manager = AuthManager(stack_auth)
|
|
```
|
|
|
|
## Token Management
|
|
|
|
Stack Auth uses two types of tokens:
|
|
|
|
- **Access Token** - Short-lived token for API requests (typically 1 hour)
|
|
- **Refresh Token** - Long-lived token to get new access tokens (typically 30 days)
|
|
|
|
### Storing Tokens Securely
|
|
|
|
For web applications, store tokens securely:
|
|
|
|
```python
|
|
from flask import session
|
|
import jwt
|
|
from datetime import datetime
|
|
|
|
def store_tokens_securely(access_token: str, refresh_token: str):
|
|
"""Store tokens in secure HTTP-only cookies or session"""
|
|
# For session storage
|
|
session['access_token'] = access_token
|
|
session['refresh_token'] = refresh_token
|
|
|
|
# For cookie storage (recommended for web apps)
|
|
# Set secure, HTTP-only cookies
|
|
response.set_cookie(
|
|
'access_token',
|
|
access_token,
|
|
httponly=True,
|
|
secure=True,
|
|
samesite='Strict'
|
|
)
|
|
|
|
def get_current_user_from_token(access_token: str) -> Optional[dict]:
|
|
"""Extract user info from access token"""
|
|
try:
|
|
# Verify token and get user
|
|
user_info = auth_manager.authenticate_request(access_token)
|
|
return user_info
|
|
except Exception:
|
|
return None
|
|
```
|
|
|
|
## Middleware for Authentication
|
|
|
|
Here's how to create middleware that automatically handles authentication:
|
|
|
|
```python
|
|
from functools import wraps
|
|
from flask import request, jsonify, g
|
|
|
|
def require_auth(f):
|
|
"""Decorator that requires authentication"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
# Get token from header or cookie
|
|
access_token = request.headers.get('Authorization')
|
|
if access_token and access_token.startswith('Bearer '):
|
|
access_token = access_token[7:]
|
|
elif 'access_token' in request.cookies:
|
|
access_token = request.cookies['access_token']
|
|
else:
|
|
return jsonify({'error': 'Authentication required'}), 401
|
|
|
|
# Verify token and get user
|
|
try:
|
|
user_info = auth_manager.authenticate_request(access_token)
|
|
g.current_user = user_info
|
|
g.access_token = access_token
|
|
return f(*args, **kwargs)
|
|
except Exception as e:
|
|
return jsonify({'error': 'Invalid token'}), 401
|
|
|
|
return decorated_function
|
|
|
|
def optional_auth(f):
|
|
"""Decorator for optional authentication"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
access_token = request.headers.get('Authorization')
|
|
if access_token and access_token.startswith('Bearer '):
|
|
access_token = access_token[7:]
|
|
try:
|
|
user_info = auth_manager.authenticate_request(access_token)
|
|
g.current_user = user_info
|
|
g.access_token = access_token
|
|
except Exception:
|
|
g.current_user = None
|
|
else:
|
|
g.current_user = None
|
|
|
|
return f(*args, **kwargs)
|
|
|
|
return decorated_function
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Protected Route
|
|
|
|
```python
|
|
from flask import Flask, g, jsonify
|
|
|
|
app = Flask(__name__)
|
|
|
|
@app.route('/profile')
|
|
@require_auth
|
|
def get_profile():
|
|
"""Get current user's profile - requires authentication"""
|
|
return jsonify({
|
|
'user': g.current_user,
|
|
'message': 'This is your profile'
|
|
})
|
|
|
|
@app.route('/public')
|
|
@optional_auth
|
|
def public_endpoint():
|
|
"""Public endpoint with optional user context"""
|
|
if g.current_user:
|
|
return jsonify({
|
|
'message': f'Hello {g.current_user.get("display_name", "User")}!'
|
|
})
|
|
else:
|
|
return jsonify({'message': 'Hello anonymous user!'})
|
|
```
|
|
|
|
### Manual Token Refresh
|
|
|
|
```python
|
|
def refresh_user_session(refresh_token: str) -> Optional[dict]:
|
|
"""Refresh user session and return new tokens"""
|
|
try:
|
|
token_response = auth_manager.refresh_token(refresh_token)
|
|
return {
|
|
'access_token': token_response['access_token'],
|
|
'refresh_token': token_response['refresh_token']
|
|
}
|
|
except Exception as e:
|
|
print(f"Token refresh failed: {e}")
|
|
return None
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
Common authentication errors and how to handle them:
|
|
|
|
```python
|
|
class AuthError(Exception):
|
|
def __init__(self, message: str, status_code: int = 401):
|
|
self.message = message
|
|
self.status_code = status_code
|
|
super().__init__(self.message)
|
|
|
|
def handle_auth_errors(func):
|
|
"""Decorator to handle common authentication errors"""
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except requests.exceptions.HTTPError as e:
|
|
if e.response.status_code == 401:
|
|
raise AuthError("Invalid or expired token", 401)
|
|
elif e.response.status_code == 403:
|
|
raise AuthError("Insufficient permissions", 403)
|
|
else:
|
|
raise AuthError("Authentication error", e.response.status_code)
|
|
except Exception as e:
|
|
raise AuthError(f"Unexpected error: {str(e)}", 500)
|
|
return wrapper
|
|
```
|
|
|
|
## Next Steps
|
|
|
|
Choose the authentication method that best fits your application:
|
|
|
|
1. **[Email/Password Authentication](./password.mdx)** - Most common, familiar to users
|
|
2. **[Magic Link Authentication](./magic-links.mdx)** - Modern passwordless approach
|
|
3. **[OAuth Authentication](./oauth.mdx)** - Easy sign-in with existing accounts
|
|
4. **[Passkey Authentication](./passkeys.mdx)** - Most secure, future-proof option
|
|
5. **[Anonymous Authentication](./anonymous.mdx)** - Great for guest users
|
|
|
|
You can also enable multiple methods simultaneously to give users options.
|