Security
Password hashing, session management, rate limiting, and tenant isolation.
Password Security
Hashing: Argon2id with the following parameters:
| Parameter | Value |
|---|---|
| Memory cost | 64 MB |
| Time cost | 3 iterations |
| Parallelism | 4 |
| Salt length | 16 bytes |
| Hash length | 32 bytes |
Policy:
- Minimum 8 characters, maximum 128
- No complexity requirements (per NIST SP 800-63B)
- Checked against HaveIBeenPwned via k-anonymity API
- Checked against top 100k common passwords
- Context-specific checks: rejects passwords containing the user's email, username, or domain
Session Security
| Property | Value |
|---|---|
| Access token type | JWT (RS256) |
| Access token lifetime | 15 minutes |
| Refresh token type | Opaque (stored hashed in DB) |
| Refresh token lifetime | 7 days |
| Refresh token rotation | Yes (new token on each refresh) |
| Reuse detection | Yes (all sessions revoked if a token is reused) |
Cookies
| Cookie | Flags |
|---|---|
zautha_session | httpOnly, secure, sameSite=Lax |
zautha_refresh_token | httpOnly, secure, sameSite=Strict, path=/v1/auth/refresh |
CSRF protection: Double-submit cookie pattern. The zautha_csrf cookie value must be sent in the X-CSRF-Token header.
Rate Limiting
Implemented via Redis sliding window counters.
| Endpoint | Limit | Window | Key |
|---|---|---|---|
| Sign-in | 5 | 1 min | IP + email |
| Sign-up | 3 | 1 min | IP |
| Password reset | 3 | 1 hour | |
| Email verification | 5 | 1 hour | |
| Global | 1000 | 1 min | IP |
Account lockout: After 5 consecutive failed sign-in attempts, the account is locked with progressive durations: 15m, 30m, 1h, 2h.
Fail-closed: When Redis is unavailable, both the rate limiter and lockout service deny requests rather than allowing them through.
Tenant Isolation
Tenant isolation is enforced at multiple layers:
- Database — Every tenant-scoped table has a
tenant_idcolumn. EF Core global query filters ensure all queries includeWHERE tenant_id = @current_tenant. - API — The
tenant_idis derived from the authenticated session's JWTtidclaim. It cannot be overridden by the client. - Cache — Redis keys are prefixed with
tenant:{tenant_id}:. - Logs and events — All log entries and domain events include
tenant_id. - Testing — Integration tests verify that a session from tenant A cannot access data belonging to tenant B.
Webhook Signatures
Webhook payloads are signed with HMAC-SHA256:
X-Zautha-Signature: t=1704067200,v1=abc123...The signature is computed as:
HMAC-SHA256(key: webhook_secret, message: "{timestamp}.{raw_body}")Webhook secrets are stored encrypted with AES-GCM.
JWT Structure
{
"header": {
"alg": "RS256",
"typ": "JWT",
"kid": "key_abc123"
},
"payload": {
"iss": "https://auth.zautha.com",
"aud": "proj_xyz789",
"sub": "user_def456",
"tid": "tenant_ghi012",
"iat": 1704067200,
"exp": 1704070800,
"org_id": "org_jkl345",
"org_role": "admin"
}
}