Skip to main content

Authentication Overview

The JOIP API uses session-based authentication with HTTP-only cookies for secure, stateless user sessions.

Authentication Methods

JOIP supports two authentication strategies based on the deployment environment:

1. Local Development (Default)

For local development and testing, the API uses Passport.js with LocalStrategy.

Test Accounts

Three test accounts are available in development:
// Admin accounts (development only)
{ email: '[email protected]', password: 'admin123', role: 'admin' }
{ email: '[email protected]', password: 'dev123', role: 'admin' }
{ email: '[email protected]', password: 'test123', role: 'admin' }

Login Request

curl -X POST http://localhost:5000/api/login \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{
    "email": "[email protected]",
    "password": "test123"
  }'
Response: 302 Redirect with session cookie

2. Replit OIDC (Production)

When deployed on Replit, the API automatically uses Replit’s OIDC authentication. Detection based on environment variable:
const useReplitAuth = !!process.env.REPLIT_DOMAINS;

Session Configuration

Session Storage

Sessions are stored in PostgreSQL using connect-pg-simple:
// From server/localAuth.ts:67-94
const sessionTtl = 7 * 24 * 60 * 60 * 1000; // 1 week
const pgStore = connectPg(session);
const sessionStore = new pgStore({
  conString: process.env.DATABASE_URL,
  createTableIfMissing: true,
  ttl: sessionTtl,
  tableName: "sessions",
});

return session({
  secret: process.env.SESSION_SECRET,
  store: sessionStore,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: sessionTtl,
    sameSite: 'lax', // CSRF protection
  },
});
  • httpOnly: true - Prevents JavaScript access (XSS protection)
  • secure: true in production - HTTPS only
  • maxAge: 7 days (604,800,000 ms)
  • sameSite: lax - CSRF protection

Authentication Endpoints

POST /api/login

Authenticate and receive session cookie. Request:
{
  "email": "[email protected]",
  "password": "test123"
}
Success Response: 302 Redirect to / Error Response:
{
  "message": "Invalid credentials"
}
Rate Limit: 5 attempts per 15 minutes per IP (failed attempts only)

GET /api/login

Client-side redirect endpoint (auto-login in development).

GET /api/dev-login

Development-only endpoint for quick auto-login. Response: 302 Redirect to / with dev account session

GET /api/logout

Terminate current session. Response: 302 Redirect to /

POST /api/auth/logout

API-compatible logout endpoint. Response:
{
  "message": "Logged out successfully"
}

GET /api/auth/user

Get current authenticated user details. Request:
curl -X GET http://localhost:5000/api/auth/user \
  -b cookies.txt
Response:
{
  "id": "local-test",
  "email": "[email protected]",
  "firstName": "test",
  "lastName": "User",
  "role": "admin",
  "isActive": true,
  "ageVerified": false,
  "credits": {
    "balance": 1000,
    "tier": "free",
    "isLowBalance": false,
    "nextAllocationDate": "2026-03-09T10:30:00.000Z"
  },
  "createdAt": "2026-03-02T10:30:00.000Z"
}
Error Response (401):
{
  "message": "Unauthorized"
}

Authentication Middleware

Protected endpoints use the isAuthenticated middleware:
// From server/localAuth.ts:573-581
export const isAuthenticated: RequestHandler = async (req, res, next) => {
  if (!req.isAuthenticated()) {
    return res.status(401).json({ message: "Unauthorized" });
  }
  if (!getUserIdOrNull(req)) {
    return res.status(401).json({ message: "Unauthorized" });
  }
  next();
};

User ID Extraction

The API uses a consistent method to extract user IDs from requests:
// From server/localAuth.ts:39-56
function getUserId(req: any): string {
  // Local auth user with direct ID
  if (req.user?.id) {
    return req.user.id;
  }
  
  // Replit OIDC claims structure
  if (req.user?.claims?.sub) {
    return req.user.claims.sub;
  }
  
  // Direct sub on user object
  if (req.user?.sub) {
    return req.user.sub;
  }
  
  throw new Error('Unable to extract user ID from request');
}

Making Authenticated Requests

Using cURL

  1. Login and save cookies:
curl -X POST http://localhost:5000/api/login \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{"email":"[email protected]","password":"test123"}'
  1. Use cookies in subsequent requests:
curl -X GET http://localhost:5000/api/sessions \
  -b cookies.txt

Using JavaScript (fetch)

// Login
await fetch('/api/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include', // Important: Include cookies
  body: JSON.stringify({
    email: '[email protected]',
    password: 'test123'
  })
});

// Authenticated request
const response = await fetch('/api/sessions', {
  credentials: 'include' // Include cookies
});
const sessions = await response.json();

Using Python (requests)

import requests

# Create session to persist cookies
session = requests.Session()

# Login
login_data = {
    "email": "[email protected]",
    "password": "test123"
}
session.post('http://localhost:5000/api/login', json=login_data)

# Authenticated request
response = session.get('http://localhost:5000/api/sessions')
sessions = response.json()

Session Lifecycle

User Creation Flow

When a user logs in for the first time:
// From server/localAuth.ts:98-147
async function upsertLocalUser(userId, userData, pendingReferral) {
  const existingUser = await storage.getUser(userId);
  const isNewUser = !existingUser;

  const user = await storage.upsertUser({
    id: userId,
    email: userData.email,
    firstName: userData.firstName || null,
    lastName: userData.lastName || null,
    role: userData.role || existingUser?.role || "user",
    isActive: existingUser?.isActive ?? true,
  });

  // Initialize credits for new users
  if (isNewUser) {
    await creditService.initializeUserCredits(userId);
    
    // Process referral if code provided
    if (pendingReferral?.code) {
      await referralService.processReferral(
        userId,
        pendingReferral.code,
        pendingReferral.source,
        pendingReferral.sourceId
      );
    }
  }

  return user;
}

Activity Tracking

Successful logins are tracked:
// From server/localAuth.ts:228
await trackLogin(userId);

Security Best Practices

Password Security

In production, passwords should be hashed using bcrypt. The current implementation uses plaintext passwords for development testing only.
// Production implementation should use:
import * as bcrypt from 'bcrypt';

// Hash password on registration
const hashedPassword = await bcrypt.hash(password, 10);

// Verify password on login
const isValid = await bcrypt.compare(password, hashedPassword);

Environment Variables

Required for authentication:
# Required in production
SESSION_SECRET=your-random-secret-key-here

# Database connection
DATABASE_URL=postgresql://user:pass@host:port/db

# Replit-specific (automatic on Replit)
REPLIT_DOMAINS=your-app.repl.co
REPL_ID=your-repl-id
ISSUER_URL=https://replit.com

CSRF Protection

Sessions use sameSite: 'lax' cookie policy:
  • Prevents CSRF attacks from external sites
  • Allows cookies on top-level GET navigations
  • Blocks cookies on cross-site POST requests

XSS Protection

  • httpOnly cookies: Session cookies cannot be accessed via JavaScript
  • Input sanitization: All user inputs are sanitized before storage
  • Output encoding: Data is properly encoded in responses

Common Authentication Errors

401 Unauthorized

Cause: Missing or invalid session cookie Solution:
  1. Ensure you’re logged in
  2. Include credentials: 'include' in fetch requests
  3. Check cookie expiration (7 days)

403 Forbidden

Cause: Authenticated but lacking required permissions Solution: Check user role and endpoint access requirements

Invalid credentials

Cause: Wrong email or password Solution: Verify credentials match test accounts

Next Steps

Build docs developers (and LLMs) love