updated the docs, removed oauth and introduced sanctum pat
This commit is contained in:
@@ -2,418 +2,128 @@
|
||||
|
||||
## 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. Additionally, session-based authentication is used for web interfaces like the checkout wizard, supporting both email and username login.
|
||||
Tenant authentication now uses a hybrid Sanctum setup:
|
||||
|
||||
## Session-Based Authentication (Web/Checkout)
|
||||
- **Tenant Admin PWA** obtains Laravel Sanctum personal access tokens (PATs) via first-party endpoints under `/api/v1/tenant-auth/*`. Tokens carry ability strings (e.g. `tenant-admin`, `tenant:{id}`) and are required for all `/api/v1/tenant/*` routes.
|
||||
- **First-party web flows** (marketing checkout, Filament admin, Google OAuth sign-in) rely on the classic session guard. When a session-based login needs API access (e.g. inside the PWA shell after a Google callback) the frontend exchanges the session for a PAT using the same Sanctum endpoints.
|
||||
- **Legacy compatibility** is preserved through `/api/v1/tenant/me`, which now proxies the Sanctum-backed data while keeping the historical payload shape consumed by older clients.
|
||||
|
||||
### Checkout Login Flow
|
||||
All previous OAuth2/PKCE code paths, tables, and environment variables have been removed. No external authorization server is required.
|
||||
|
||||
## Tenant Admin PAT Flow
|
||||
|
||||
### Login
|
||||
- **Endpoint**: `POST /api/v1/tenant-auth/login`
|
||||
- **Body** (`application/json`):
|
||||
```json
|
||||
{
|
||||
"login": "tenant@example.com",
|
||||
"password": "secret"
|
||||
}
|
||||
```
|
||||
`login` accepts either an email address or username. Passwords are validated against the user record.
|
||||
- **Response** (`200`):
|
||||
```json
|
||||
{
|
||||
"token": "plain-text-pat",
|
||||
"token_type": "Bearer",
|
||||
"abilities": ["tenant-admin", "tenant:42"],
|
||||
"user": {
|
||||
"id": 17,
|
||||
"email": "tenant@example.com",
|
||||
"name": "Tenant Admin",
|
||||
"role": "tenant_admin",
|
||||
"tenant_id": 42
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Failure codes**:
|
||||
- `422` with `login` field errors for invalid credentials, unverified email addresses, or roles without tenant access.
|
||||
- `429` throttle when the `tenant-auth` rate limit is exceeded.
|
||||
|
||||
Every successful login revokes any previous `tenant-admin` PAT for that user before issuing a new token. PATs are plain-text once in the response; the hash is stored in `personal_access_tokens`.
|
||||
|
||||
### Token Exchange (session → PAT)
|
||||
- **Endpoint**: `POST /api/v1/tenant-auth/exchange`
|
||||
- **Guards**: `web` session (cookies) with Sanctum stateful middleware.
|
||||
- **Response**: identical payload to `/login`.
|
||||
- **Usage**: The Tenant Admin PWA calls this after a browser session login (e.g. Google Socialite callback or marketing checkout user who opens the PWA shell) to synchronise state without a password prompt.
|
||||
|
||||
### Token Introspection & Logout
|
||||
- **`GET /api/v1/tenant-auth/me`** (auth:sanctum) returns the current user, tenant snapshot, and active abilities for the PAT.
|
||||
- **`POST /api/v1/tenant-auth/logout`** invalidates the current PAT and clears it from storage.
|
||||
- **Legacy Proxy**: `/api/v1/tenant/me` now delegates to the new controller to return the historical payload seen by existing tooling while relying on Sanctum for authentication.
|
||||
|
||||
### Calling Tenant APIs
|
||||
All tenant APIs continue to require the abilities enforced by middleware:
|
||||
- `auth:sanctum` authenticates the PAT.
|
||||
- `tenant.admin` gate checks that the user has `tenant-admin` or `super-admin` ability.
|
||||
- `tenant.isolation` ensures the `tenant:{id}` ability matches the route tenant, guarding cross-tenant access.
|
||||
|
||||
Requests must send the PAT in the `Authorization: Bearer {token}` header. Tokens have no server-side expiration (`sanctum.expiration = null`); clients should refresh proactively (e.g. re-login) when a token is revoked or rejected.
|
||||
|
||||
## Session-Based Flows
|
||||
|
||||
### Marketing Checkout Login
|
||||
- **Endpoint**: `POST /checkout/login`
|
||||
- **Method**: POST
|
||||
- **Content-Type**: `application/json`
|
||||
- **Parameters**:
|
||||
- `identifier`: Email or username (required, string)
|
||||
- `password`: User's password (required, string)
|
||||
- `remember`: Remember me flag (optional, boolean)
|
||||
- `locale`: Language locale (optional, string, e.g., 'de')
|
||||
- **Body**: `{ identifier, password, locale? }`
|
||||
- **Remember behaviour**: the checkbox is hidden on the new Tenant Admin login screen but applied automatically – sessions persist via `Auth::login($user, true)`.
|
||||
- Successful logins store a `redirect_intent` in the session when the flow originates from `/event-admin/*`, ensuring post-login navigation goes to `/event-admin/dashboard`.
|
||||
|
||||
**Authentication Logic**:
|
||||
- Validate input using Laravel Validator.
|
||||
- Search for user by email or username using Eloquent query: `User::where('email', $identifier)->orWhere('username', $identifier)->first()`.
|
||||
- Verify password with `Hash::check()`.
|
||||
- If valid, log in user with `Auth::login($user, $remember)` and regenerate session.
|
||||
- Set `pending_purchase = true` if a package is selected (from session) and not already set, wrapped in DB transaction.
|
||||
- Return JSON response with user data for AJAX handling in frontend.
|
||||
### Tenant Admin PWA Login Screen
|
||||
- The React shell renders `/event-admin/login` inside the PWA layout. It uses the same credentials form as `/checkout/login` but hits `/api/v1/tenant-auth/login` with fetch requests.
|
||||
- After a successful PAT response, the token is persisted to both `localStorage` and `sessionStorage`; abilities are cached client-side to power feature flags.
|
||||
|
||||
**Response** (JSON, 200 OK):
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"id": 1,
|
||||
"email": "user@example.com",
|
||||
"name": "John Doe",
|
||||
"pending_purchase": true
|
||||
},
|
||||
"message": "Login erfolgreich"
|
||||
}
|
||||
```
|
||||
### Google Sign-In
|
||||
- Socialite handles the Google redirect/callback under `/event-admin/auth/google`.
|
||||
- On success the user is logged into the web session, their email is marked verified (if needed), and the browser redirects back into the PWA shell.
|
||||
- The shell immediately calls `/api/v1/tenant-auth/exchange` to obtain a PAT, keeping the flow consistent with password logins.
|
||||
- Failures redirect back to the login page with query-string error codes that the PWA surfaces via i18n strings.
|
||||
|
||||
**Error Response** (JSON, 422 Unprocessable Entity):
|
||||
```json
|
||||
{
|
||||
"errors": {
|
||||
"identifier": ["Ungültige Anmeldedaten."]
|
||||
}
|
||||
}
|
||||
```
|
||||
## Middleware & Guards
|
||||
|
||||
**Security**:
|
||||
- CSRF protection via `web` middleware.
|
||||
- Rate limiting recommended (add `throttle:6,1` middleware).
|
||||
- Password hashing with Laravel's `Hash` facade.
|
||||
- Session regeneration after login to prevent fixation attacks.
|
||||
| Middleware | Purpose |
|
||||
| --- | --- |
|
||||
| `auth:sanctum` | Validates PATs issued by Sanctum. |
|
||||
| `tenant.admin` | Ensures the authenticated user is a tenant admin or super admin. |
|
||||
| `tenant.isolation` | Loads the tenant model by token ability and adds it to the request for downstream controllers. |
|
||||
| `throttle:tenant-auth` | Rate limits login, exchange, and logout endpoints. |
|
||||
| `EncryptCookies`, `AddQueuedCookiesToResponse`, `StartSession` | Applied to exchange routes so session cookies are available. |
|
||||
|
||||
### Integration with Standard Laravel Auth
|
||||
- Leverages `AuthenticatedSessionController` for core logic where possible, but custom handling for identifier flexibility and checkout context.
|
||||
- Compatible with Inertia.js for SPA responses.
|
||||
## Security Considerations
|
||||
|
||||
## OAuth2 Authentication (API)
|
||||
- PATs are hashed at rest (`personal_access_tokens`), revocation is handled via database deletes.
|
||||
- Tokens are limited to one active entry per user (`tenant-admin` name). Issuing a new token automatically stairs old devices out.
|
||||
- All auth endpoints return generic validation errors to avoid username enumeration.
|
||||
- Session regeneration occurs on every successful login (password or Google) to prevent fixation.
|
||||
- Frontend storage keeps a second copy of the PAT in `sessionStorage` to reduce exposure when a user clears only persistent storage.
|
||||
- Rate limits: configure `tenant-auth` in `RouteServiceProvider` (default `10 requests / minute`).
|
||||
- CSP / XSRF: stateful Sanctum middleware is configured with `SANCTUM_STATEFUL_DOMAINS` so SPA requests inherit `XSRF-TOKEN` cookies automatically.
|
||||
|
||||
### 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`
|
||||
## Database & Infrastructure
|
||||
|
||||
**Response**: Redirect to frontend with authorization code and state parameters.
|
||||
- OAuth tables (`oauth_clients`, `oauth_codes`, `refresh_tokens`) have been removed. Sanctum uses the stock `personal_access_tokens` migration.
|
||||
- No background jobs are required for PAT issuance or rotation.
|
||||
- Audit requirements should log `personal_access_tokens` changes via database auditing (e.g. telescope/horizon logs) if needed.
|
||||
|
||||
### 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)
|
||||
## Configuration & Environment
|
||||
|
||||
**Response** (JSON):
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refresh_token": "def50200...",
|
||||
"expires_in": 3600,
|
||||
"token_type": "Bearer",
|
||||
"scope": "tenant:read tenant:write"
|
||||
}
|
||||
```
|
||||
Set the following environment variables to support the hybrid flow:
|
||||
|
||||
### 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
|
||||
- `SANCTUM_STATEFUL_DOMAINS` – comma-separated list of domains running the Tenant Admin PWA (TWA, Capacitor, staging). Ensures cookies are considered first-party for the exchange endpoint.
|
||||
- `SANCTUM_TOKEN_PREFIX` – optional prefix to enable secret-scanning detection.
|
||||
- Remove the deprecated OAuth variables:
|
||||
- `VITE_OAUTH_CLIENT_ID`
|
||||
- `OAUTH_JWT_KID`
|
||||
- `OAUTH_KEY_STORE`
|
||||
- `OAUTH_REFRESH_*`
|
||||
|
||||
**Response**: Same as token exchange (new access and refresh tokens).
|
||||
Google OAuth credentials (`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`) remain for Socialite but only power the browser session login; token exchange happens through Sanctum.
|
||||
|
||||
### 4. Token Validation
|
||||
- **Endpoint**: `GET /api/v1/tenant/me`
|
||||
- **Method**: GET
|
||||
- **Authorization**: `Bearer {access_token}`
|
||||
- **Purpose**: Validate token and return tenant information
|
||||
## Testing
|
||||
|
||||
**Response** (JSON):
|
||||
```json
|
||||
{
|
||||
"tenant_id": "tenant-uuid",
|
||||
"tenant_name": "Event Photo Company",
|
||||
"credits": 150,
|
||||
"role": "admin",
|
||||
"email": "admin@eventphoto.com"
|
||||
}
|
||||
```
|
||||
- Feature tests cover `/api/v1/tenant-auth/login`, `/exchange`, and `/logout` (`tests/Feature/Auth/TenantProfileApiTest.php`, `tests/Feature/TenantCreditsTest.php`).
|
||||
- Frontend Playwright fixtures (`tests/e2e/utils/test-fixtures.ts`) provide helpers to obtain PATs when seeding test users.
|
||||
- When adding new tenant API endpoints ensure coverage under Sanctum by using `->actingAs($user, 'sanctum')` in PHPUnit tests and by asserting abilities with `Sanctum::actingAs()` helpers where required.
|
||||
|
||||
## 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 |
|
||||
| `/checkout/login` | POST | Session login for checkout (email/username) | None |
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
> **Hinweis:** Der Wert von `VITE_OAUTH_CLIENT_ID` dient jetzt als alleinige Quelle der Wahrheit für den Tenant-Admin-OAuth-Client. Der Seeder `OAuthClientSeeder` greift auf `config/services.php` zu, das wiederum diesen Env-Wert ausliest und passende Redirect-URIs generiert (`/event-admin/auth/callback` für DEV und APP_URL). Stimmt der Wert im Frontend nicht mit dem Seeder überein, schlägt der PKCE-Login mit `invalid_client` fehl.
|
||||
|
||||
## 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"
|
||||
}
|
||||
|
||||
// 422 Unprocessable Entity (Checkout Login)
|
||||
{
|
||||
"errors": {
|
||||
"identifier": ["Ungültige Anmeldedaten."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
- Checkout login: 6/minute per IP (add throttle middleware)
|
||||
|
||||
### 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
|
||||
- Log checkout login attempts with identifier type (email/username)
|
||||
|
||||
### 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
|
||||
- Checkout login with email and username
|
||||
|
||||
### Integration Tests
|
||||
- Complete OAuth2 flow (authorize → token → validate)
|
||||
- Token refresh cycle
|
||||
- Error scenarios (invalid code, expired tokens, state mismatch)
|
||||
- Concurrent access testing
|
||||
- Checkout login flow with pending_purchase
|
||||
|
||||
### Security Tests
|
||||
- CSRF protection validation
|
||||
- PKCE bypass attempts
|
||||
- Token replay attacks
|
||||
- Rate limiting enforcement
|
||||
- Username/email ambiguity handling
|
||||
|
||||
## 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
|
||||
- Monitor checkout login success rates and identifier usage
|
||||
|
||||
This implementation provides secure, scalable authentication for the Fotospiel tenant system, following OAuth2 best practices with PKCE for public clients and flexible session auth for web flows.
|
||||
This Sanctum-based approach keeps login logic inside the Laravel application, avoids custom OAuth infrastructure, and works uniformly across web, PWA, and the planned React Native wrapper.
|
||||
|
||||
Reference in New Issue
Block a user