Guides

Guide - Authentication Flow & Sessions

Understanding JWT tokens, refresh cycles, cookie management, and secure authentication patterns.

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 HeaderBearer <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:

  1. Extract token from Authorization: Bearer <token> header
  2. Verify JWT signature
  3. Check expiration (not expired)
  4. Check JTI is not revoked in Redis
  5. 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 accessToken via Authorization: Bearer header
  • ✅ 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 token
  • sub / userId: Who this token belongs to
  • organizationId: Tenant scope
  • userType: Role (OWNER, CUSTOMER, etc.)
  • iat (issued at): When token was created
  • exp (expiration): When token becomes invalid (Unix timestamp)

Summary

ConceptValueNotes
Access Token Lifetime15 minutesAuto-refresh before expiry
Refresh Token Lifetime7 daysAfter expiry, user must re-login
Cookie TypeHttpOnlyNot accessible to JavaScript
TransportHTTPS onlyEncrypts tokens in transit
ValidationJWT signature + expiryServer-side only
Session DatabaseNoneStateless (JWT)

Authentication Flow:

  1. Login — POST /api/auth/login → Get tokens in cookies
  2. Authenticated Requests — Cookies auto-included → API validates JWT
  3. Before Expiry — POST /api/auth/refresh → Get new access token
  4. After 7 Days — Refresh token expires → User re-logs in
  5. 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)