Skip to main content

Overview

Ceboelha API uses a robust JWT-based authentication system with the following security features:
  • Dual-token system: Short-lived access tokens (15 minutes) and long-lived refresh tokens (7 days)
  • Token rotation: Refresh tokens are invalidated after use to prevent token theft
  • Account lockout: Automatic lockout after 5 failed login attempts
  • Secure storage: Tokens stored as SHA-256 hashes, never in plain text
  • Device tracking: Session management with device and IP tracking
  • HttpOnly cookies: Tokens sent via secure httpOnly cookies for XSS protection

Authentication Flow

Registration

Create a new user account:
POST /auth/register
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "SecurePass123!",
  "name": "John Doe"
}
Password Requirements:
  • Minimum 8 characters
  • At least 1 uppercase letter
  • At least 1 lowercase letter
  • At least 1 number
  • At least 1 special character (!@#$%^&*...)
See auth.service.ts:179-182 for password validation logic. Response:
{
  "success": true,
  "data": {
    "user": {
      "id": "507f1f77bcf86cd799439011",
      "email": "[email protected]",
      "name": "John Doe",
      "role": "user",
      "createdAt": "2024-01-15T10:30:00.000Z"
    },
    "expiresIn": 900
  },
  "message": "Conta criada com sucesso! 🐰"
}
Tokens are automatically set as httpOnly cookies in the Set-Cookie header. The expiresIn value (in seconds) helps clients know when to refresh.

Login

Authenticate with email and password:
POST /auth/login
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "SecurePass123!"
}
Response:
{
  "success": true,
  "data": {
    "user": {
      "id": "507f1f77bcf86cd799439011",
      "email": "[email protected]",
      "name": "John Doe",
      "role": "user"
    },
    "expiresIn": 900
  },
  "message": "Login realizado com sucesso!"
}
After 5 failed login attempts, the account is locked for 15 minutes. You’ll receive remaining attempt warnings after the 3rd failed attempt.

Refresh Token

Access tokens expire after 15 minutes. Use the refresh token to get a new access token:
POST /auth/refresh
Content-Type: application/json
Cookie: refresh_token=<token>

{
  "refreshToken": "<refresh-token-from-cookie>"
}
Token Rotation (Security Feature): When you refresh, the old refresh token is immediately revoked and a new one is issued. This prevents stolen tokens from being reused (see auth.service.ts:407-411).
// Old refresh token is revoked
await RefreshToken.revokeToken(
  RefreshToken.hashToken(refreshTokenString),
  'Token rotation'
)

// New token pair is generated
return this.createTokenPair(user, deviceInfo)
If a revoked token is reused, the system detects potential token theft and revokes ALL tokens for that user as a security measure (auth.service.ts:386-392).

Logout

Revoke tokens to logout:
POST /auth/logout
Authorization: Bearer <access-token>
Content-Type: application/json

{
  "refreshToken": "<token>",
  "allDevices": false
}
Parameters:
refreshToken
string
The refresh token to revoke. If omitted, only the current session is invalidated.
allDevices
boolean
default:"false"
Set to true to logout from all devices by revoking all refresh tokens.

Token Structure

Access Token (JWT)

Access tokens are signed JWTs with the following payload:
sub
string
required
User ID (MongoDB ObjectId)
email
string
required
User email address
role
string
required
User role: "user" or "admin"
type
string
required
Token type: always "access"
iat
number
Issued at timestamp (Unix epoch)
exp
number
Expiration timestamp (Unix epoch)
iss
string
Issuer: "ceboelha-api"
aud
string
Audience: "ceboelha-app"
Token Generation (auth.service.ts:109-124):
const payload: JWTPayload = {
  sub: user._id.toString(),
  email: user.email,
  role: user.role,
  type: 'access',
}

return new SignJWT(payload)
  .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
  .setIssuedAt()
  .setExpirationTime('15m') // from env.JWT_ACCESS_EXPIRES_IN
  .setIssuer('ceboelha-api')
  .setAudience('ceboelha-app')
  .sign(accessSecret)

Refresh Token

Refresh tokens are cryptographically secure random strings (64 bytes = 128 hex characters), not JWTs. They are stored in the database as SHA-256 hashes with:
  • User ID reference
  • Device information (User-Agent, IP)
  • Expiration date (7 days)
  • Revocation status and reason
See refresh-token.model.ts:139-158 for token generation.

Making Authenticated Requests

Using Authorization Header

Include the access token in the Authorization header:
GET /diary/entries
Authorization: Bearer <access-token>
If tokens are set via cookies, they’re automatically sent:
GET /diary/entries
Cookie: access_token=<token>
The auth middleware (auth.middleware.ts:48-58) checks both sources:
// Try Authorization header first
const tokenFromHeader = extractBearerTokenFromHeader(authHeader)

// Fallback to httpOnly cookie
const tokenFromCookie = getTokenFromCookies(cookieHeader, ACCESS_TOKEN_COOKIE)

const token = tokenFromHeader || tokenFromCookie

Session Management

List Active Sessions

Get all active sessions (logged-in devices):
GET /auth/sessions
Authorization: Bearer <access-token>
Response:
{
  "success": true,
  "data": [
    {
      "id": "65a1b2c3d4e5f6789abcdef0",
      "device": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
      "ip": "192.168.1.100",
      "createdAt": "2024-01-15T10:30:00.000Z",
      "expiresAt": "2024-01-22T10:30:00.000Z"
    }
  ],
  "message": "1 sessão(ões) ativa(s)"
}

Revoke a Session

Remotely logout a specific device:
DELETE /auth/sessions/:id
Authorization: Bearer <access-token>
This is useful for “I don’t recognize this device” scenarios.

Security Features

Account Lockout

Protection against brute force attacks (login-attempt.model.ts:130-162):
  1. After 5 failed login attempts, the account is locked for 15 minutes
  2. Failed attempts are tracked per email address
  3. Successful login resets the counter
  4. Users receive warnings after the 3rd failed attempt
Configuration (env.ts:59-60):
MAX_LOGIN_ATTEMPTS: 5,
LOCKOUT_DURATION: 900000, // 15 minutes in milliseconds

Token Storage Security

Refresh tokens are never stored in plain text:
// Token is hashed before storage (refresh-token.model.ts:125-127)
function hashToken(token: string): string {
  return crypto.createHash('sha256').update(token).digest('hex')
}
Only the hash is stored in MongoDB. Even if the database is compromised, attackers cannot use the tokens.

Device Tracking

Every login and token refresh records:
  • IP address (from X-Forwarded-For or X-Real-IP headers)
  • User-Agent string
  • Timestamp
This enables:
  • Suspicious activity detection
  • Session management
  • Security auditing
See auth.middleware.ts:152-159 for device info extraction.

Token Theft Detection

If a revoked refresh token is used, the system assumes token theft and:
  1. Revokes ALL refresh tokens for that user
  2. Forces logout on all devices
  3. User must re-authenticate everywhere
// auth.service.ts:386-392
if (storedToken.isRevoked) {
  await RefreshToken.revokeAllForUser(
    storedToken.userId,
    'Possible token theft detected - revoked token reuse'
  )
}

Environment Configuration

Configure JWT settings via environment variables:
# JWT Secrets (minimum 32 characters)
JWT_ACCESS_SECRET=your-secret-key-min-32-chars
JWT_REFRESH_SECRET=your-refresh-secret-key-min-32-chars

# Token Expiration
JWT_ACCESS_EXPIRES_IN=15m   # 15 minutes
JWT_REFRESH_EXPIRES_IN=7d   # 7 days

# Account Lockout
MAX_LOGIN_ATTEMPTS=5
LOCKOUT_DURATION=900000     # 15 minutes in ms
See env.ts:20-27 for secret validation (minimum 32 characters enforced).

Error Handling

Common authentication errors:
Error CodeStatusDescription
UNAUTHORIZED401Invalid or expired token
FORBIDDEN403Valid token but insufficient permissions
RATE_LIMIT429Account locked due to failed attempts
VALIDATION_ERROR400Weak password or invalid input
CONFLICT409Email already registered
Example Error Response:
{
  "success": false,
  "error": "UnauthorizedError",
  "code": "UNAUTHORIZED",
  "message": "Token expirado ou inválido"
}
See Error Handling for more details.

Build docs developers (and LLMs) love