Chapter 2.2 — Authentication
1. Purpose
Authentication is the gateway to every other module in travoBooks. This chapter defines how identities are proved — for human users, partner-employee logins, customer self-service accounts, and machine-to-machine API consumers — and how sessions are issued, validated, and revoked. It also defines password policy, multi-factor enrolment, lockout behaviour, and personal-access-token (PAT) lifecycle.
Authorisation — what an authenticated principal is allowed to do — is covered in Chapter 2.1 (Users, Roles & RBAC) and Chapter 2.3 (Permission Catalog).
2. Why it matters in modern travel accounting
A travel-accounting platform sits on top of irreplaceable financial state: a single compromised accountant session can post fraudulent journal entries that take weeks to unwind. The platform also issues real airline tickets — each one a regulated, settled financial obligation with the BSP. Authentication failures translate directly into financial loss, ADM exposure, and customer trust damage.
Industry-specific pressure on auth design: - Agents work from mobile devices on public Wi-Fi. - Mid-office staff at large agencies use shared workstations. - Corporate clients expect SSO into their identity provider. - API integration partners need long-lived, scoped credentials. - IATA / BSP audits require demonstrable identity assurance.
3. Industry relevance
The travoBooks auth model aligns with NIST SP 800-63B Authenticator Assurance Level 2 for staff users handling financial postings, and Level 3 for partner-admin operations (high-impact configuration changes, payouts). Customer self-service accounts default to Level 1 with optional MFA upgrade.
4. Compliance considerations
- PCI DSS 8.x — strong authentication for all access to systems handling cardholder data (travoBooks does not store PANs, but the gateway-token paths still require strong auth).
- GDPR Art. 32 — appropriate technical measures including authentication strength proportional to data sensitivity.
- SOC 2 CC6 — logical access controls; documented authentication mechanisms.
- IATA Resolution 818g — agent identity controls for IATA-accredited members.
- Local data-protection laws (Bangladesh DPA, India DPDP, EU GDPR) — record of consent, breach-notification triggers.
5. Business logic
5.1 Authentication factors supported
| Factor | Type | Use |
|---|---|---|
| Password (Argon2id) | Something you know | Primary for human users |
| TOTP (RFC 6238) | Something you have | MFA — required for partner_admin, accountant, approver roles |
| WebAuthn / Passkey | Something you have/are | Phase 2 — preferred MFA |
| Email-OTP | Something you have | Fallback MFA; account recovery |
| SMS-OTP | Something you have | Customer self-service MFA |
| SAML 2.0 / OIDC SSO | Federated | Corporate partners (Phase 2) |
| Personal Access Token (PAT) | Bearer credential | Machine-to-machine API |
| Mutual TLS (mTLS) | Certificate | High-value integration partners (Phase 2) |
5.2 Password policy
| Rule | Value |
|---|---|
| Minimum length | 12 characters |
| Composition | No mandatory mix; passphrase encouraged |
| Breach check | Pwned Passwords API (k-anonymity); rejection if found |
| History | Last 12 passwords cannot be reused |
| Maximum age | 365 days (rotation required) |
| Lockout threshold | 5 failed attempts within 15 minutes |
| Lockout duration | Exponential — 1m, 5m, 15m, 1h, 24h |
| Reset method | Email link + MFA confirmation if enrolled |
| Hashing | Argon2id, memory=64MB, time=4, parallelism=2 |
| Pepper | Per-deployment secret stored in KMS |
Passwords are never logged, returned in API responses, or written to disk outside the hashed password_hash column.
5.3 Session model
Sessions are server-side, identified by an opaque session ID in a __Host-travoBooks-Session cookie.
| Attribute | Value |
|---|---|
| Cookie name | __Host-travoBooks-Session |
| Cookie flags | HttpOnly; Secure; SameSite=Strict; Path=/ |
| Session ID format | 256-bit cryptographically random, base64url-encoded |
| Session store | Redis (per-deployment) |
| Idle timeout | 30 minutes (configurable per partner) |
| Absolute lifetime | 12 hours (re-authentication required after) |
| Concurrent sessions per user | Limited to 5; oldest evicted |
| Rotation on privilege elevation | New session ID issued |
| Binding | IP address (range-tolerant), User-Agent fingerprint, partner_id |
Session records also store a risk_score updated by signals (geo change, new device, unusual timing) — high-risk sessions trigger step-up MFA.
5.4 Multi-factor authentication
MFA enrolment is mandatory for:
- Any role with is_financial = TRUE (accountant, partner_admin, approver, cashier)
- Any user with API key creation permission
- Any user in a partner with mfa_required = TRUE policy
MFA enrolment is optional but recommended for all other users.
MFA enrolment flow:
1. User logs in with password.
2. System prompts MFA enrolment (TOTP QR code or WebAuthn registration).
3. User scans QR / registers passkey.
4. User enters one current OTP to confirm enrolment.
5. System generates 10 single-use backup codes, shown once, stored hashed.
6. Enrolment is recorded in user_mfa_methods with enrolled_at.
MFA verification flow at login: 1. Password verified → MFA challenge issued. 2. User submits OTP / passkey assertion. 3. On success, full session is established. 4. On 3 consecutive MFA failures within 5 minutes, lock the account.
5.5 Personal Access Tokens (PAT)
PATs let users and machine identities authenticate to the API without a session.
| Attribute | Value |
|---|---|
| Format | tgc_<env>_<random_32> (e.g., tvb_live_a1b2c3...) |
| Storage | Argon2id-hashed in personal_access_tokens table |
| Display | Full token shown once at creation; only prefix shown thereafter |
| Scopes | Subset of user's permissions; cannot exceed |
| Expiry | Mandatory; max 365 days; default 90 days |
| IP allow-list | Optional CIDR list enforced at request time |
| Rotation | UI-driven; superseded tokens kept for 24h grace period |
| Revocation | Immediate (cache-busted across all app nodes) |
| Audit | Every PAT use logged with request_id, ip, route, partner_id |
5.6 SSO (Phase 2)
For partners with enterprise IdPs:
- OIDC (preferred) — travoBooks acts as relying party.
- SAML 2.0 — for legacy IdPs.
- JIT provisioning — first-time SAML/OIDC users get a viewer role; partner_admin must elevate.
- SCIM 2.0 — automatic user lifecycle sync (Phase 3).
6. Inputs → processing → outputs
Login (password + MFA)
Input: {partner_slug, email, password, mfa_code?}
Processing:
1. Resolve partner_id from slug.
2. Look up user by (partner_id, email).
3. Bcrypt-compare password against password_hash.
4. Apply rate-limit (Redis sliding window keyed by (partner_id, email) and by source IP).
5. If MFA required, issue or verify challenge.
6. Create session record in Redis.
7. Set session cookie.
8. Emit auth.login.success audit event.
Output: Redirect to dashboard; cookie set.
API call with PAT
Input: HTTP request with Authorization: Bearer tvb_live_<token>
Processing:
1. Parse token, look up by prefix in personal_access_tokens.
2. Argon2id verify the suffix against stored hash.
3. Check expires_at, revoked_at.
4. Check IP allow-list if configured.
5. Load associated user + permission set.
6. Attach principal to request context.
7. Increment pat.usage_count; update last_used_at.
8. Emit auth.pat.used audit event.
Output: Request proceeds to controller with authenticated principal.
7. Module dependencies
| Direction | Module |
|---|---|
| Depends on | User Management (Ch 2.1), Multi-Tenancy (Ch 1.1), Audit Logs (Ch 8.2) |
| Depended on by | Every authenticated endpoint in the platform |
8. Security & permissions
- Authentication endpoints have stricter rate-limits than other routes.
- Failed login attempts emit telemetry that feeds the WAF and abuse-detection system.
- Account-takeover signals trigger automatic forced password reset and notification.
- Tokens are never returned twice — if lost, they must be recreated.
- The
last_password_change_attimestamp is tracked; downstream PATs older than this timestamp can optionally be force-rotated by policy.
9. Validation rules
| Code | Condition |
|---|---|
AUTH_INVALID_CREDENTIALS |
Email/password mismatch (generic — never reveal which) |
AUTH_ACCOUNT_LOCKED |
Account locked due to failed attempts |
AUTH_ACCOUNT_DISABLED |
users.is_active = FALSE |
AUTH_MFA_REQUIRED |
Password OK but MFA challenge pending |
AUTH_MFA_INVALID_CODE |
OTP / passkey assertion failed |
AUTH_PASSWORD_EXPIRED |
Password age exceeded policy max |
AUTH_PASSWORD_BREACHED |
New password matches known-breached set |
AUTH_PAT_REVOKED |
Token has been revoked |
AUTH_PAT_EXPIRED |
Token past expires_at |
AUTH_PAT_IP_DENIED |
Caller IP not in PAT allow-list |
AUTH_SESSION_EXPIRED |
Session past idle or absolute timeout |
AUTH_PARTNER_SUSPENDED |
Partner account is suspended |
AUTH_RATE_LIMITED |
Too many requests from this principal/IP |
10. Error handling
The login API never distinguishes between "user does not exist" and "password incorrect" — both return AUTH_INVALID_CREDENTIALS. The MFA challenge response never reveals whether MFA is enrolled until the password phase succeeds. Lockout messages do not disclose lockout duration to the requester (they are logged for support, not shown to attacker).
For PAT failures, the response is 401 Unauthorized with no body discriminator — observability is internal.
11. Real-world examples
Example A — Accountant logs in from new device
- Accountant
[email protected](partnerbeta-travel) logs in. - Password validates.
- Geo-IP shows session originating from Singapore; last 30 days of logins were from Bangladesh.
- Risk score elevated → MFA challenge issued plus email notification "New login from Singapore".
- Ria enters TOTP → session established.
auth.login.successaudit event recorded withrisk_score=0.62, mfa_method=totp.
Example B — API key for booking automation
- Beta Corp dev creates a PAT
tvb_live_a1b2...with scopebookings.read.partner, bookings.create.partnerand IP allow-list198.51.100.0/24. - Dev integrates into their corporate booking bot.
- Bot calls
POST /api/v1/bookingsfrom198.51.100.45with the bearer token. - Auth layer verifies token, IP, scope → request proceeds.
- Usage metrics are visible in the partner UI; rate-limit headers tell the bot when to back off.
Example C — Session hijack attempt detected
- Attacker steals session cookie via XSS in third-party widget (hypothetical — travoBooks' CSP prevents it).
- Attacker replays cookie from a different IP / User-Agent.
- Session-binding mismatch raises
risk_scoreto0.95. - Session is force-terminated; original user notified by email.
auth.session.suspicious_replayaudit event recorded.
12. Step-by-step workflow
13. Database tables touched
| Table | Role |
|---|---|
users |
Identity record, password_hash, mfa_required |
user_mfa_methods |
Enrolled MFA factors per user |
user_mfa_backup_codes |
Hashed single-use backup codes |
user_login_attempts |
Failed-attempt counter + lockout state |
personal_access_tokens |
Hashed PATs with scope, expiry, IP allow-list |
sessions (Redis-backed; persistent log in session_history) |
Active sessions |
audit_logs |
Authentication events |
password_history |
Last 12 hashes |
14. Future scalability
- WebAuthn / Passkeys — Phase 2, becomes the preferred MFA factor; passwords retained as a recovery mechanism only.
- Risk-based adaptive auth — Phase 2, ML-scored risk that adjusts MFA frequency.
- Step-up auth API — Phase 2, exposes
require_mfa(action)to controllers so individual high-risk actions (large refund, payout) can demand fresh MFA regardless of session state. - Hardware-token (FIDO2) support for enterprise partners — Phase 3.
- SCIM 2.0 for SSO partner lifecycle — Phase 3.
15. Common pitfalls
- ⚠️ Don't expose user-existence via differing error messages. Always return the same generic message on credential failure.
- ⚠️ Don't make MFA bypass-able via "remember this device". travoBooks' policy is per-session — no persistent MFA-skip cookie for financial roles.
- 🔒 PATs are bearer tokens — treat them like cash. UI must mask, transport must be TLS, storage must be hashed.
- 🔒 Never log Authorization headers. Sanitisation must happen at the structured-logging layer before serialisation.
- ⚠️ Don't tie session lifetime to "Remember Me" without re-checking permission cache. Permission changes must invalidate stale sessions via
role_set_version(see Ch 2.1).