Authentication Guide
Complete guide to OAuth token-based authentication in the Pricing Engine API.
Overview
The Pricing Engine uses OAuth 2.0 Bearer token authentication. Tokens are issued by an external authentication service (Partner Engine) and validated via webhook on each request.
Key Features
- ✅ OAuth 2.0 compliant
- ✅ Stateless token validation via webhook
- ✅ Single restaurant scope per token
- ✅ Optional caching for performance
- ✅ Fallback header auth (development only)
Authentication Flow
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Partner App │ │ Partner Engine │ │ Pricing Engine │
│ │ │ (OAuth Server) │ │ │
└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
│ 1. Login │ │
│───────────────────────>│ │
│ │ │
│ 2. OAuth Token │ │
│<───────────────────────│ │
│ │ │
│ 3. API Request + Token │ │
│─────────────────────────────────────────────────>│
│ │ │
│ │ 4. Validate Token │
│ │<────────────────────────│
│ │ │
│ │ 5. Token Details │
│ │────────────────────────>│
│ │ │
│ 6. API Response │ │
│<─────────────────────────────────────────────────│
Steps:
- Partner app authenticates with Partner Engine OAuth server
- OAuth server returns Bearer access token
- Partner app includes token in
Authorization: Bearer <token>header - Pricing Engine validates token via webhook to Partner Engine
- Partner Engine returns token details (user_id, restaurant_id, etc.)
- Pricing Engine processes request and returns response
Token Structure
Tokens are opaque strings validated via webhook. The Pricing Engine does NOT decode JWT tokens locally.
Example Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Token Details (from webhook response):
{
"user_id": "12345",
"email": "partner@example.com",
"name": "Partner User",
"role": "partner",
"restaurant_id": "1631",
"expires_at": "2025-12-09T12:00:00Z"
}
Making API Requests
Standard Request
GET /api/partner/v1/pricing_rules HTTP/1.1
Host: localhost:3001
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
cURL Example
curl -X GET http://localhost:3001/api/partner/v1/pricing_rules \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json"
JavaScript Example
const response = await fetch('http://localhost:3001/api/partner/v1/pricing_rules', {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
Ruby Example
require 'faraday'
client = Faraday.new(url: 'http://localhost:3001')
response = client.get('/api/partner/v1/pricing_rules') do |req|
req.headers['Authorization'] = "Bearer #{access_token}"
req.headers['Content-Type'] = 'application/json'
end
Token Validation Process
1. Extract Token
The API extracts the Bearer token from the Authorization header:
Authorization: Bearer <token>
2. Validate via Webhook
The token is sent to the Partner Engine webhook endpoint:
POST https://partner-engine.example.com/oauth/token/info
Authorization: Bearer <token>
3. Cache Response (Optional)
Token details are cached in:
- Redis L1: 60 seconds TTL
- PostgreSQL L2: 5 minutes TTL
4. Set Request Context
User and restaurant data are set in the controller:
current_user_idcurrent_user_emailcurrent_user_namecurrent_restaurant_id
Restaurant Scoping
Each OAuth token is scoped to one restaurant. The restaurant_id is extracted from the token and automatically applied to all queries.
Automatic Scoping
All API endpoints automatically filter by the authenticated restaurant:
# List rules - only returns rules for current_restaurant_id
GET /api/partner/v1/pricing_rules
# Create rule - automatically sets restaurant_id
POST /api/partner/v1/pricing_rules
{
"pricing_rule": {
"name": "Weekend Pricing"
# restaurant_id is set automatically from token
}
}
Multi-Restaurant Access
If a user has access to multiple restaurants, they must obtain separate tokens for each restaurant.
Error Responses
401 Unauthorized
{
"error": "Bearer token required"
}
Causes:
- Missing
Authorizationheader - Invalid token format (must be
Bearer <token>) - Token validation failed
Solution: Include valid Bearer token in request.
403 Forbidden
{
"error": "Access denied for restaurant 123"
}
Causes:
- User doesn’t have access to the requested restaurant
- Token is valid but restaurant_id doesn’t match
Solution: Use token for correct restaurant.
Environment Configuration
Required Variables
# OAuth Token Validation Endpoint
PARTNER_ENGINE_BASE_URL=https://partner-engine.example.com
# Token Cache TTL (seconds)
TOKEN_CACHE_TTL=300
# Redis (for L1 cache)
REDIS_URL=redis://localhost:6379/0
Optional Variables
# Allow X-Restaurant-Id header auth (development only)
ALLOW_HEADER_AUTH=false
# Webhook timeout (seconds)
OAUTH_WEBHOOK_TIMEOUT=5
Development Mode
For local development only, you can enable header-based authentication:
export ALLOW_HEADER_AUTH=true
This bypasses OAuth and uses headers:
GET /api/partner/v1/pricing_rules HTTP/1.1
X-Restaurant-Id: 1631
X-User-Id: 12345
X-User-Name: Test User
X-User-Email: test@example.com
⚠️ WARNING: Never enable ALLOW_HEADER_AUTH in production!
Caching Strategy
3-Tier Cache
| Layer | Storage | TTL | Purpose |
|---|---|---|---|
| L1 | Redis | 60 seconds | Ultra-fast validation |
| L2 | PostgreSQL | 5 minutes | Persistent cache |
| L3 | Webhook | N/A | Source of truth |
Cache Flow
1. Request with Bearer token
↓
2. Check Redis L1 (by token hash)
├─ HIT → Return cached data (< 1ms)
└─ MISS → Check PostgreSQL L2
↓
3. Check PostgreSQL L2
├─ HIT → Store in Redis → Return (< 10ms)
└─ MISS → Call webhook
↓
4. Call Partner Engine webhook (~200ms)
↓
5. Store in PostgreSQL L2 (5 min TTL)
↓
6. Store in Redis L1 (60 sec TTL)
↓
7. Return token details
Clear Cache
# Rails console
docker-compose exec api bundle exec rails console
# Clear all token caches
Auth::TokenCache.clear_all
AccessToken.delete_all
Security Best Practices
Transport Security
- ✅ HTTPS required in production
- ✅ TLS 1.2 minimum
- ✅ Certificate validation enabled
Token Security
- ✅ Tokens transmitted only via
Authorizationheader - ✅ Short token expiration (recommended: 1-2 hours)
- ✅ Tokens stored securely on client (HTTP-only cookies, secure storage)
- ❌ Never include tokens in URLs or logs
API Security
- ✅ Rate limiting per restaurant (100 req/min)
- ✅ Request timeout (5 seconds)
- ✅ Input validation and sanitization
- ✅ SQL injection protection (ActiveRecord)
Troubleshooting
“Bearer token required”
Check request format:
# ✅ Correct
Authorization: Bearer eyJhbGc...
# ❌ Wrong
Authorization: eyJhbGc... # Missing "Bearer"
X-Authorization: Bearer eyJhbGc... # Wrong header
“Token validation failed”
Verify webhook endpoint:
# Test webhook manually
curl -X POST https://partner-engine.example.com/oauth/token/info \
-H "Authorization: Bearer YOUR_TOKEN"
Check environment:
# Verify PARTNER_ENGINE_BASE_URL
echo $PARTNER_ENGINE_BASE_URL
Cache Issues
Clear stale tokens:
# Rails console
Auth::TokenCache.clear_all
AccessToken.where('created_at < ?', 5.minutes.ago).delete_all
Implementation Details
Controller Concern
# app/controllers/concerns/doorkeeper_authentication.rb
module DoorkeeperAuthentication
def authenticate_with_doorkeeper!
token = extract_bearer_token
result = Auth::TokenValidator.call(bearer_token: token)
if result.success?
set_current_user(result.data)
else
render_unauthorized(result.error)
end
end
end
Token Validator Service
# app/services/auth/token_validator.rb
module Auth
class TokenValidator < BaseService
def call
# 1. Check cache
# 2. Call webhook if cache miss
# 3. Store in cache
# 4. Return user data
end
end
end
Next Steps
- Try the API: POSTMAN_GUIDE.md
- Explore Endpoints: API_REFERENCE.md
- See Examples: API_EXAMPLES.md
- Understand Architecture: ARCHITECTURE.md
Last Updated: December 2025
Authentication Method: OAuth 2.0 Bearer Token
Token Validation: Webhook-based
Cache TTL: 60s (Redis), 5min (PostgreSQL)