Guide - Authentication Flow & Sessions
Booki uses JWT-based authentication with Bearer tokens for secure, stateless session management. This guide explains the complete authentication lifecycle.
Authentication Architecture
The Booki API uses:
✅ JWT Tokens — Stateless credentials (no session database needed)
✅ Authorization Header — Bearer <token> sent by frontend plugin on every request
✅ Refresh Tokens — Long-lived tokens for session renewal
✅ Dual Token Pattern — Short-lived access + long-lived refresh returned in response body
Token Concepts
Access Token
Purpose: Proves user identity to the API
Lifetime: 15 minutes (900 seconds)
Payload contains:
{
"user": "507f1f77bcf86cd799439011",
"organizationId": "507f191e810c19729de860ea",
"role": "owner",
"jti": "uuid-for-revocation",
"iat": 1712332080,
"exp": 1712332980
}
Refresh Token
Purpose: Obtain new access token without re-entering credentials
Lifetime: 7 days (expires, then user must login again)
Payload contains:
{
"user": "507f1f77bcf86cd799439011",
"organizationId": "507f191e810c19729de860ea",
"role": "owner",
"iat": 1712332080,
"exp": 1712937680
}
Complete Login Flow
┌─────────────────────────────────────────────┐
│ 1. User enters credentials │
│ Email: jane@example.com │
│ Password: SecureP@ss123 │
└──────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────┐
│ 2. POST /api/auth/login │
│ Request body contains credentials │
└──────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────┐
│ 3. Server validates credentials │
│ - Lookup user by email │
│ - Hash incoming password │
│ - Compare with stored hash │
│ - If mismatch → 401 Unauthorized │
└──────────────┬──────────────────────────────┘
│
↓ (if valid)
┌──────────────────────────────────────────────┐
│ 4. Generate JWT tokens │
│ - accessToken (15 min lifetime) │
│ - refreshToken (7 day lifetime) │
└──────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────┐
│ 5. Set HttpOnly cookies in response │
│ Set-Cookie: access_token=jwt; HttpOnly │
│ Set-Cookie: refresh_token=jwt; HttpOnly │
│ Set-Cookie: Path=/; Secure │
└──────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────┐
│ 6. Browser automatically stores cookies │
│ (JavaScript cannot access them) │
└──────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────┐
│ 7. Response to client │
│ { │
│ "user": { profile data }, │
│ "expiresIn": 900 │
│ } │
└──────────────┴──────────────────────────────┘
Step-by-Step Login
1. POST /api/auth/login — Initial Login
Request:
curl -X POST https://api.booki.app/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"password": "SecureP@ss123"
}'
Response (200 OK):
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "abc123def456...",
"_id": "507f1f77bcf86cd799439011"
}
What happened:
- ✅ Frontend plugin received and stored both tokens
- ✅ All future requests include
Authorization: Bearer <accessToken> - ✅ Access token valid for configured expiry
Authenticated Requests
Using Authorization Header
After login, include the accessToken in the Authorization header:
curl -X GET https://api.booki.app/api/auth/user \
-H "Authorization: Bearer eyJhbGc..."
Server validates:
- Extract token from
Authorization: Bearer <token>header - Verify JWT signature
- Check expiration (not expired)
- Check JTI is not revoked in Redis
- Return user data or 401 if invalid
Token Refresh Flow
When access token expires (15 minutes), client doesn't re-login. Instead:
Automatic Token Refresh
Time: 0:00 Login
├─ Access token expires in 900 seconds (15 min)
├─ Refresh token valid for 7 days
│
Time: 14:50 (Before expiry)
├─ Browser detects token nearly expired
├─ Auto-calls POST /api/auth/refresh
│
Time: 14:51 POST /api/auth/refresh
├─ Server validates refresh_token
├─ Issues new access_token (fresh 15 min)
├─ Returns new tokens in JSON response body
│
Time: 14:52 User resumes using API
└─ New access_token in all requests
2. POST /api/auth/refresh — Get New Access Token
Request:
curl -X POST https://api.booki.app/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"token": "abc123def456..."}'
Response (200 OK):
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "newRefreshToken123..."
}
What happened:
- ✅ Old access token discarded
- ✅ New access token issued
- ✅ User seamlessly continues (no re-login needed)
- ✅ Refresh token stays valid for remaining days
Session Lifespan Timeline
LOGIN (0:00)
├─ access_token = 15 minutes
├─ refresh_token = 7 days
├─ User active
│
4:50 (access token expires)
├─ Auto-refresh triggers
├─ New access_token issued
├─ Session continues
│
Day 3 (refresh token expiring soon)
├─ Backgrnd refresh extends refresh_token
├─ User unaware of renewal
│
Day 7 (refresh token expires)
├─ User returns to app
├─ refresh_token no longer valid
├─ Request returns 401
├─ Browser redirects to /login
├─ User must re-enter credentials
└─ New 7-day session starts
Error Scenarios & Recovery
Scenario 1: Access Token Expired, Refresh Valid
// Frontend attempt
GET /api/bookings (with expired access_token)
// Server response
401 Unauthorized: "Token expired"
// Frontend action
POST /api/auth/refresh (with valid refresh_token)
// Server returns
200 OK with new access_token
// Frontend retry
GET /api/bookings (with new access_token)
// Success
200 OK: Bookings returned
Scenario 2: Both Tokens Expired or Invalid
// User hasn't used app for 8 days
// Both access_token AND refresh_token expired
GET /api/bookings (with expired access_token)
// Server response
401 Unauthorized: Session expired
// Frontend action (required)
Redirect to /login
// User must re-enter credentials
POST /api/auth/login
Scenario 3: No Token Present
If no Authorization header is present, the request will fail:
curl -X GET https://api.booki.app/api/bookings \
-H "Authorization: Bearer eyJhbGc..."
On token expiration:
# Frontend detects 401 response
POST /api/auth/refresh \
-H "Authorization: Bearer REFRESH_TOKEN"
# Get new access_token from response
# Use for next request
Security Best Practices
✅ DO
- ✅ Use HTTPS always (never HTTP)
- ✅ Send
accessTokenviaAuthorization: Bearerheader - ✅ Implement token refresh before expiry
- ✅ Blacklist tokens on logout (JTI revocation)
- ✅ Validate tokens server-side
- ✅ Log failed auth attempts
❌ DON'T
- ❌ Store tokens in localStorage (XSS vulnerable)
- ❌ Share tokens in URLs or query params
- ❌ Log tokens to console in production
- ❌ Send tokens over HTTP (unencrypted)
- ❌ Trust expired tokens
- ❌ Skip HTTPS validation
Logout Flow
1. DELETE /api/auth/logout — Invalidate Session
Request:
curl -X DELETE https://api.booki.app/api/auth/logout \
-H "Authorization: Bearer eyJhbGc..."
Response (200 OK):
{
"message": "Logged out successfully."
}
What happened:
- ✅ Access token JTI blacklisted in Redis
- ✅ All future requests with this token return 401
- ✅ Session ended
Owner Registration (Multi-Step) with Tokens
Owner registration is different — uses OTP flow:
Step 1: Request OTP
POST /api/auth/register/owner/otp
{
"email": "jane@example.com"
}
# Response
{ "message": "Owner email OTP sent successfully. Kindly check your email for the OTP." }
Step 2: Verify OTP
POST /api/auth/register/owner/otp/verify
{
"otp": "123456"
}
# Response
{ "message": "OTP verified successfully.", "valid": true }
Step 3: Create Account
POST /api/auth/register/owner
{
"otp": "123456",
"password": "SecureP@ss123",
"ownerDetails": {...},
"businessDetails": {...}
}
# Response (201 Created)
{ "message": "Business owner added successfully." }
JWT Payload Structure
Decoded access_token:
{
"iss": "booki-api",
"sub": "507f1f77bcf86cd799439011",
"userId": "507f1f77bcf86cd799439011",
"organizationId": "507f191e810c19729de860ea",
"userType": "OWNER",
"email": "jane@example.com",
"iat": 1712332080,
"exp": 1712332980,
"aud": "booki-customers"
}
Field meanings:
iss(issuer): API that created tokensub/userId: Who this token belongs toorganizationId: Tenant scopeuserType: Role (OWNER, CUSTOMER, etc.)iat(issued at): When token was createdexp(expiration): When token becomes invalid (Unix timestamp)
Summary
| Concept | Value | Notes |
|---|---|---|
| Access Token Lifetime | 15 minutes | Auto-refresh before expiry |
| Refresh Token Lifetime | 7 days | After expiry, user must re-login |
| Cookie Type | HttpOnly | Not accessible to JavaScript |
| Transport | HTTPS only | Encrypts tokens in transit |
| Validation | JWT signature + expiry | Server-side only |
| Session Database | None | Stateless (JWT) |
Authentication Flow:
- Login — POST /api/auth/login → Get tokens in cookies
- Authenticated Requests — Cookies auto-included → API validates JWT
- Before Expiry — POST /api/auth/refresh → Get new access token
- After 7 Days — Refresh token expires → User re-logs in
- Logout — GET /api/auth/logout → Cookies cleared
It just works:
- 🍪 Cookies handled automatically by browser
- 🔄 Token refresh happens in background
- 🔒 No tokens exposed to JavaScript
- 📱 Mobile apps manually manage tokens (Authorization header)
