Overview
Torn uses JWT (JSON Web Token) authentication with a multi-tenant architecture. Users authenticate at the global SaaS level and then access specific tenants (companies) based on their permissions.
Authentication Flow
User Login
Send credentials to obtain a JWT access token and list of available tenants.
Token Storage
Store the access token securely (e.g., in memory, secure storage).
Tenant Selection
Choose a tenant from the available tenants list.
API Requests
Include the token in the Authorization header and tenant ID in the X-Tenant-Id header.
Login Endpoints
OAuth2 Token Login
OAuth2-compatible endpoint using form data.
POST /auth/token
Content-Type: application/x-www-form-urlencoded
[email protected]&password=secretpassword
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"user": {
"id": 1,
"email": "[email protected]",
"full_name": "John Doe",
"is_active": true,
"is_superuser": false
},
"available_tenants": [
{
"id": 1,
"name": "ACME Corp",
"rut": "76123456-7",
"role_name": "ADMINISTRADOR",
"is_active": true,
"max_users": 10,
"permissions": {
"sales": true,
"inventory": true,
"reports": true
}
}
]
}
JSON Login
Alternative endpoint accepting JSON body.
POST /auth/login
Content-Type: application/json
{
"email": "[email protected]",
"password": "secretpassword"
}
Both endpoints return the same response structure. Use /auth/login for modern JSON-based clients.
Token Configuration
Tokens are configured with the following parameters:
JWT signing key (configured via environment variable)
ACCESS_TOKEN_EXPIRE_MINUTES
Token expiration time in minutes (12 hours)
Token structure from app/utils/security.py:23:
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
Making Authenticated Requests
Global Endpoints
For global endpoints (like /auth/users/me), only the Authorization header is required:
GET /auth/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Tenant-Scoped Endpoints
For tenant-specific endpoints, include both headers:
GET /customers
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Tenant-Id: 1
All tenant-scoped endpoints require the X-Tenant-Id header. Missing this header will result in a 422 validation error.
Tenant-Aware Authentication
Access Validation
The system validates tenant access through the get_current_tenant_user dependency in app/dependencies/tenant.py:59:
async def get_current_tenant_user(
x_tenant_id: Annotated[int, Header(description="ID del Tenant a consultar")],
current_user: Annotated[SaaSUser, Depends(get_current_global_user)],
global_db: Session = Depends(get_global_db)
) -> TenantUser:
"""Valida y retorna la membresía (TenantUser) del usuario global en el Inquilino solicitado."""
tenant_user = global_db.query(TenantUser).filter(
TenantUser.user_id == current_user.id,
TenantUser.tenant_id == x_tenant_id,
TenantUser.is_active == True
).first()
if not tenant_user and not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No tienes acceso a este Inquilino / Empresa."
)
Database Schema Isolation
Each tenant has its own PostgreSQL schema for complete data isolation. The get_tenant_db dependency automatically routes queries to the correct schema based on the X-Tenant-Id header.
Session Validation
Validate the current session and refresh tenant list:
GET /auth/validate
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Response:
{
"user": {
"id": 1,
"email": "[email protected]",
"full_name": "John Doe",
"is_active": true,
"is_superuser": false
},
"available_tenants": [
{
"id": 1,
"name": "ACME Corp",
"rut": "76123456-7",
"role_name": "ADMINISTRADOR",
"is_active": true,
"max_users": 10,
"permissions": {...}
}
]
}
User Profile
Retrieve the current user’s profile:
GET /auth/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Error Responses
401 Unauthorized
Invalid credentials or expired token:
{
"detail": "Incorrect email or password"
}
403 Forbidden
User doesn’t have access to the requested tenant:
{
"detail": "No tienes acceso a este Inquilino / Empresa."
}
404 Not Found
Tenant not found or inactive:
{
"detail": "Inquilino no encontrado o inactivo."
}
Security Best Practices
Use HTTPS
Always use HTTPS in production to protect tokens in transit.
Secure Token Storage
Store tokens securely. Avoid localStorage in web applications; prefer memory or secure HTTP-only cookies.
Handle Token Expiration
Implement token refresh logic or prompt users to re-authenticate after 12 hours.
Validate Tenant Access
Always verify that users have access to the tenant before displaying sensitive data.
Example: Complete Authentication Flow
import requests
# 1. Login
response = requests.post(
"https://api.torn.cl/auth/login",
json={
"email": "[email protected]",
"password": "secretpassword"
}
)
data = response.json()
access_token = data["access_token"]
available_tenants = data["available_tenants"]
# 2. Select tenant
tenant_id = available_tenants[0]["id"]
# 3. Make authenticated request
headers = {
"Authorization": f"Bearer {access_token}",
"X-Tenant-Id": str(tenant_id)
}
response = requests.get(
"https://api.torn.cl/customers",
headers=headers
)
customers = response.json()