356 lines
10 KiB
Markdown
356 lines
10 KiB
Markdown
# 13 - Backend Authentication Implementation
|
|
|
|
## Overview
|
|
|
|
This document outlines the authentication requirements and implementation details for the Fotospiel tenant backend. The system uses OAuth2 with PKCE (Proof Key for Code Exchange) for secure authorization, providing tenant-specific access tokens for API operations.
|
|
|
|
## Authentication Flow
|
|
|
|
### 1. Authorization Request
|
|
- **Endpoint**: `GET /api/v1/oauth/authorize`
|
|
- **Method**: GET (redirect from frontend)
|
|
- **Parameters**:
|
|
- `client_id`: Fixed client ID for tenant-admin-app (`tenant-admin-app`)
|
|
- `redirect_uri`: Frontend callback URL (e.g., `https://tenant-admin-app.example.com/auth/callback`)
|
|
- `response_type`: `code`
|
|
- `scope`: `tenant:read tenant:write` (tenant-specific scopes)
|
|
- `state`: CSRF protection state parameter
|
|
- `code_challenge`: PKCE challenge (SHA-256 hash of code verifier)
|
|
- `code_challenge_method`: `S256`
|
|
|
|
**Response**: Redirect to frontend with authorization code and state parameters.
|
|
|
|
### 2. Token Exchange
|
|
- **Endpoint**: `POST /api/v1/oauth/token`
|
|
- **Method**: POST
|
|
- **Content-Type**: `application/x-www-form-urlencoded`
|
|
- **Parameters**:
|
|
- `grant_type`: `authorization_code` or `refresh_token`
|
|
- `client_id`: `tenant-admin-app`
|
|
- `code`: Authorization code (for initial exchange)
|
|
- `redirect_uri`: Same as authorization request
|
|
- `code_verifier`: PKCE verifier (plain text)
|
|
- `refresh_token`: For token refresh (optional)
|
|
|
|
**Response** (JSON):
|
|
```json
|
|
{
|
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
"refresh_token": "def50200...",
|
|
"expires_in": 3600,
|
|
"token_type": "Bearer",
|
|
"scope": "tenant:read tenant:write"
|
|
}
|
|
```
|
|
|
|
### 3. Token Refresh
|
|
- **Endpoint**: `POST /api/v1/oauth/token`
|
|
- **Method**: POST
|
|
- **Content-Type**: `application/x-www-form-urlencoded`
|
|
- **Parameters**:
|
|
- `grant_type`: `refresh_token`
|
|
- `client_id`: `tenant-admin-app`
|
|
- `refresh_token`: Current refresh token
|
|
|
|
**Response**: Same as token exchange (new access and refresh tokens).
|
|
|
|
### 4. Token Validation
|
|
- **Endpoint**: `GET /api/v1/tenant/me`
|
|
- **Method**: GET
|
|
- **Authorization**: `Bearer {access_token}`
|
|
- **Purpose**: Validate token and return tenant information
|
|
|
|
**Response** (JSON):
|
|
```json
|
|
{
|
|
"tenant_id": "tenant-uuid",
|
|
"tenant_name": "Event Photo Company",
|
|
"credits": 150,
|
|
"role": "admin",
|
|
"email": "admin@eventphoto.com"
|
|
}
|
|
```
|
|
|
|
## Security Requirements
|
|
|
|
### PKCE Implementation
|
|
- Validate `code_challenge_method` is `S256`
|
|
- Store `code_challenge` and `code_verifier` association temporarily (Redis, 5-minute expiry)
|
|
- Verify SHA-256 hash of `code_verifier` matches stored `code_challenge` during token exchange
|
|
- Reject requests without valid PKCE parameters
|
|
|
|
### State Validation
|
|
- Generate cryptographically secure state parameter
|
|
- Store state in session or Redis with 5-minute expiry
|
|
- Validate state parameter matches stored value during callback
|
|
- Reject requests with state mismatch (CSRF protection)
|
|
|
|
### Token Security
|
|
- Access tokens: JWT with 1-hour expiry, signed with RS256
|
|
- Refresh tokens: Secure random 128-character strings, stored in database
|
|
- Refresh tokens: Single-use or rotation (new refresh token with each refresh)
|
|
- Rate limiting on token endpoints (100 requests/hour per IP)
|
|
- IP binding for refresh tokens (optional for enhanced security)
|
|
|
|
### Scopes and Permissions
|
|
- `tenant:read`: Access to tenant data (events, photos, members)
|
|
- `tenant:write`: Create/update/delete tenant resources
|
|
- `tenant:admin`: Full administrative access (user management, billing)
|
|
- Token claims include `tenant_id` and `scope` for authorization
|
|
|
|
## Database Schema
|
|
|
|
### oauth_clients Table
|
|
```sql
|
|
CREATE TABLE oauth_clients (
|
|
id VARCHAR(255) PRIMARY KEY,
|
|
client_id VARCHAR(255) UNIQUE NOT NULL,
|
|
client_secret VARCHAR(255),
|
|
tenant_id BIGINT UNSIGNED NULL,
|
|
redirect_uris JSON NULL,
|
|
scopes JSON NULL,
|
|
is_active TINYINT(1) DEFAULT 1,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
CONSTRAINT oauth_clients_tenant_id_foreign FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE SET NULL
|
|
);
|
|
```sql
|
|
CREATE TABLE oauth_clients (
|
|
id VARCHAR(255) PRIMARY KEY,
|
|
client_id VARCHAR(255) UNIQUE NOT NULL,
|
|
client_secret VARCHAR(255),
|
|
redirect_uris TEXT,
|
|
scopes TEXT DEFAULT 'tenant:read tenant:write',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
);
|
|
```
|
|
|
|
### oauth_codes Table (Temporary)
|
|
```sql
|
|
CREATE TABLE oauth_codes (
|
|
id VARCHAR(255) PRIMARY KEY,
|
|
client_id VARCHAR(255) NOT NULL,
|
|
user_id VARCHAR(255) NOT NULL,
|
|
code VARCHAR(255) UNIQUE NOT NULL,
|
|
code_challenge VARCHAR(255) NOT NULL,
|
|
state VARCHAR(255),
|
|
redirect_uri VARCHAR(255),
|
|
scope TEXT,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_code (code),
|
|
INDEX idx_expires (expires_at)
|
|
);
|
|
```
|
|
|
|
### refresh_tokens Table
|
|
```sql
|
|
CREATE TABLE refresh_tokens (
|
|
id VARCHAR(255) PRIMARY KEY,
|
|
tenant_id VARCHAR(255) NOT NULL,
|
|
client_id VARCHAR(255),
|
|
token VARCHAR(255) UNIQUE NOT NULL,
|
|
access_token VARCHAR(255),
|
|
scope TEXT,
|
|
ip_address VARCHAR(45),
|
|
user_agent TEXT,
|
|
expires_at TIMESTAMP,
|
|
revoked_at TIMESTAMP NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
```sql
|
|
CREATE TABLE refresh_tokens (
|
|
id VARCHAR(255) PRIMARY KEY,
|
|
tenant_id VARCHAR(255) NOT NULL,
|
|
token VARCHAR(255) UNIQUE NOT NULL,
|
|
access_token VARCHAR(255),
|
|
expires_at TIMESTAMP NOT NULL,
|
|
scope TEXT,
|
|
ip_address VARCHAR(45),
|
|
user_agent TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
revoked_at TIMESTAMP NULL,
|
|
INDEX idx_tenant (tenant_id),
|
|
INDEX idx_token (token),
|
|
INDEX idx_expires (expires_at)
|
|
);
|
|
```
|
|
|
|
### tenant_tokens Table (JWT Blacklist)
|
|
```sql
|
|
CREATE TABLE tenant_tokens (
|
|
id VARCHAR(255) PRIMARY KEY,
|
|
tenant_id VARCHAR(255) NOT NULL,
|
|
jti VARCHAR(255) UNIQUE NOT NULL, -- JWT ID
|
|
token_type VARCHAR(50) NOT NULL,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
revoked_at TIMESTAMP NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_jti (jti),
|
|
INDEX idx_tenant (tenant_id),
|
|
INDEX idx_expires (expires_at)
|
|
);
|
|
```
|
|
|
|
## API Endpoints
|
|
|
|
### Authentication Endpoints
|
|
| Endpoint | Method | Description | Authentication |
|
|
|----------|--------|-------------|----------------|
|
|
| `/oauth/authorize` | GET | Authorization request | None |
|
|
| `/oauth/token` | POST | Token exchange/refresh | None |
|
|
| `/api/v1/tenant/me` | GET | Validate token | Bearer Token |
|
|
|
|
### Protected Endpoints
|
|
All tenant API endpoints require `Authorization: Bearer {access_token}` header.
|
|
|
|
#### Token Validation Middleware
|
|
```javascript
|
|
// Pseudocode
|
|
function validateTenantToken(req, res, next) {
|
|
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) return res.status(401).json({ error: 'Missing token' });
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
const { tenant_id, scope, exp, jti } = decoded;
|
|
|
|
// Check if token is blacklisted
|
|
const blacklisted = await db.query('SELECT * FROM tenant_tokens WHERE jti = ? AND revoked_at IS NULL', [jti]);
|
|
if (blacklisted.length > 0) {
|
|
return res.status(401).json({ error: 'Token revoked' });
|
|
}
|
|
|
|
// Check expiry
|
|
if (Date.now() >= exp * 1000) {
|
|
return res.status(401).json({ error: 'Token expired' });
|
|
}
|
|
|
|
// Set tenant context
|
|
req.tenant = { id: tenant_id, scope };
|
|
next();
|
|
} catch (error) {
|
|
res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
}
|
|
```
|
|
|
|
## Environment Variables
|
|
|
|
### Backend (.env)
|
|
```
|
|
JWT_SECRET=your-super-secret-jwt-signing-key
|
|
API_BASE_URL=https://api.fotospiel.com
|
|
OAUTH_CLIENT_SECRET=your-oauth-client-secret
|
|
DATABASE_URL=your-database-connection-string
|
|
REDIS_URL=redis://localhost:6379
|
|
```
|
|
|
|
### Frontend (.env)
|
|
```
|
|
VITE_API_URL=https://api.fotospiel.com
|
|
VITE_OAUTH_CLIENT_ID=tenant-admin-app
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Common Error Responses
|
|
```json
|
|
// 400 Bad Request
|
|
{
|
|
"error": "invalid_request",
|
|
"error_description": "Missing required parameter: code"
|
|
}
|
|
|
|
// 401 Unauthorized
|
|
{
|
|
"error": "invalid_token",
|
|
"error_description": "Token signature invalid"
|
|
}
|
|
|
|
// 403 Forbidden
|
|
{
|
|
"error": "insufficient_scope",
|
|
"error_description": "Scope tenant:write required"
|
|
}
|
|
```
|
|
|
|
## Implementation Notes
|
|
|
|
### 1. PKCE Storage
|
|
- Use Redis for temporary code_challenge storage (5-minute TTL)
|
|
- Key format: `pkce:{code_challenge}:{client_id}`
|
|
- Value: JSON with `code_verifier`, `user_id`, `redirect_uri`, `scope`
|
|
|
|
### 2. Refresh Token Rotation
|
|
- Issue new refresh token with each refresh
|
|
- Revoke old refresh token immediately
|
|
- Limit refresh tokens per tenant to 5 active
|
|
|
|
### 3. Key Management
|
|
- RSA key pairs for signing are generated on demand and stored in storage/app/private.key (private) and storage/app/public.key (public).
|
|
- Treat the private key as a secret; rotate it alongside deploys that invalidate tenant tokens.
|
|
|
|
### 4. Rate Limiting
|
|
- Authorization requests: 10/minute per IP
|
|
- Token exchanges: 5/minute per IP
|
|
- Token validation: 100/minute per tenant
|
|
|
|
### 5. Logging and Monitoring
|
|
- Log all authentication attempts (success/failure)
|
|
- Monitor token usage patterns
|
|
- Alert on unusual activity (multiple failed attempts, token anomalies)
|
|
- Track refresh token usage for security analysis
|
|
|
|
### 6. Database Cleanup
|
|
- Cron job to remove expired authorization codes (daily)
|
|
- Remove expired refresh tokens (weekly)
|
|
- Clean blacklisted tokens after expiry (daily)
|
|
|
|
## Testing Requirements
|
|
|
|
### Unit Tests
|
|
- PKCE generation and validation
|
|
- State parameter security
|
|
- Token signing and verification
|
|
- Scope validation middleware
|
|
|
|
### Integration Tests
|
|
- Complete OAuth2 flow (authorize → token → validate)
|
|
- Token refresh cycle
|
|
- Error scenarios (invalid code, expired tokens, state mismatch)
|
|
- Concurrent access testing
|
|
|
|
### Security Tests
|
|
- CSRF protection validation
|
|
- PKCE bypass attempts
|
|
- Token replay attacks
|
|
- Rate limiting enforcement
|
|
|
|
## Deployment Considerations
|
|
|
|
### 1. Secrets Management
|
|
- Store JWT secret and OAuth client secret in secure vault (AWS Secrets Manager, HashiCorp Vault)
|
|
- Rotate secrets every 90 days
|
|
- Use different secrets for dev/staging/production
|
|
|
|
### 2. Certificate Management
|
|
- Use Let's Encrypt or commercial SSL certificates
|
|
- Rotate certificates before expiry
|
|
- Enable HSTS headers
|
|
|
|
### 3. Monitoring
|
|
- Track authentication success/failure rates
|
|
- Monitor token expiry patterns
|
|
- Alert on PKCE validation failures
|
|
- Log all security-related events
|
|
|
|
This implementation provides secure, scalable authentication for the Fotospiel tenant system, following OAuth2 best practices with PKCE for public clients.
|
|
|
|
|
|
|
|
|
|
|
|
|