Skip to main content

Authentication API

The Authentication API provides secure user authentication, email verification, password management, and token handling for your applications.
All endpoints use the base URL: https://jethings-backend.fly.dev (production) or http://localhost:3000 (development)

🔐 Authentication Flow

1

Sign Up

Create account with email verification
2

Verify Email

Enter OTP sent to email
3

Sign In

Get access and refresh tokens
4

Make Requests

Use access token in Authorization header
5

Refresh Token

Get new tokens when access token expires

📝 User Registration

Sign Up

Create a new user account with email verification.
curl -X POST https://jethings-backend.fly.dev/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "[email protected]",
    "phoneNumber": "+1234567890",
    "password": "strongPass123",
    "age": 28,
    "description": "Software developer"
  }'
Request Body:
FieldTypeRequiredDescription
firstNamestringUser’s first name
lastNamestringUser’s last name
emailstringValid email address (must be unique)
phoneNumberstringValid phone number (must be unique)
passwordstringMinimum 6 characters
agenumberInteger between 1-120
descriptionstringOptional user bio
Success Response (200):
{
  "message": "User created, check your email for verification code"
}
Error Responses:
  • 409 Conflict: Email already exists
  • 409 Conflict: Phone number already exists
  • 400 Bad Request: Validation errors

Verify Email

Verify user email with the OTP sent during signup.
curl -X POST https://jethings-backend.fly.dev/auth/verify-email \
  -H "Content-Type: application/json" \
  -d '{"otp": "123456"}'
Request Body:
FieldTypeRequiredDescription
otpstring6-digit verification code
Success Response (200):
{
  "message": "Email verified successfully"
}
Error Responses:
  • 400 Bad Request: Invalid or expired OTP
  • 400 Bad Request: Too many attempts (max 3)
  • OTP expires after 10 minutes
  • Maximum 3 attempts per OTP
  • OTP is automatically deleted after successful verification

🔑 User Authentication

Sign In

Authenticate user and receive access/refresh tokens.
curl -X POST https://jethings-backend.fly.dev/auth/signin \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "strongPass123"
  }'
Request Body:
FieldTypeRequiredDescription
emailstringUser’s email address
passwordstringUser’s password
Success Response (200):
{
  "message": "Signed in successfully",
  "user": {
    "id": "ck_123...",
    "email": "[email protected]",
    "firstName": "Jane",
    "lastName": "Doe",
    "roles": ["user"]
  },
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
  "expiresIn": 900
}
Error Responses:
  • 401 Unauthorized: Invalid credentials
  • 401 Unauthorized: Email not verified
  • Access token expires in 15 minutes
  • Refresh token expires in 7 days
  • Store refresh token securely (httpOnly cookie recommended)

Get Current User

Retrieve current authenticated user’s profile.
curl -X GET https://jethings-backend.fly.dev/auth/me \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Success Response (200):
{
  "id": "ck_123...",
  "email": "[email protected]",
  "firstName": "Jane",
  "lastName": "Doe",
  "age": 28,
  "phoneNumber": "+1234567890",
  "avatarUrl": "https://example.com/avatar.jpg",
  "description": "Software developer",
  "roles": ["user"],
  "isEmailVerified": true,
  "lastActivity": "2024-01-15T10:30:00.000Z",
  "createdAt": "2024-01-01T00:00:00.000Z",
  "updatedAt": "2024-01-15T10:30:00.000Z"
}
Error Responses:
  • 401 Unauthorized: Invalid or expired token
  • 401 Unauthorized: User not found
  • 401 Unauthorized: Account deactivated

🔄 Refresh Token Flow

Overview

Jethings Backend implements a secure refresh token authentication system that provides persistent, secure user sessions without compromising security. Token Types:
  • Access Token: Short-lived JWT (15 minutes) used to authenticate API requests
  • Refresh Token: Long-lived token (7 days) used to obtain new access tokens
Key Features:
  • Automatic token refresh via backend middleware
  • Manual token refresh option
  • Token rotation on each refresh
  • Secure token revocation

Authentication Flow Diagram

Storing Tokens

Store tokens securely on the client. Common approaches: Web Applications:
  • localStorage - Persists across browser sessions
  • sessionStorage - Persists only for current session
  • httpOnly cookies (recommended for refresh tokens)
  • React Context/State Management
Mobile Applications:
  • Secure storage (Keychain on iOS, Keystore on Android)
  • Encrypted shared preferences
Example: Storing tokens after login
const loginResponse = await fetch('https://jethings-backend.fly.dev/auth/signin', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password }),
});

const data = await loginResponse.json();

// Store tokens securely
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
localStorage.setItem('tokenExpiresAt', Date.now() + (data.expiresIn * 1000));
The backend middleware automatically refreshes expired access tokens when both tokens are included in the request headers. This is the simplest and recommended approach. How it works:
  1. Include both Authorization: Bearer <accessToken> and x-refresh-token: <refreshToken> headers
  2. If the access token is expired, the backend automatically refreshes it
  3. New tokens are returned in response headers
  4. Update your stored tokens with the new values
Implementation Example:
async function makeAuthenticatedRequest(url: string, options: RequestInit = {}) {
  const accessToken = localStorage.getItem('accessToken');
  const refreshToken = localStorage.getItem('refreshToken');

  const response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${accessToken}`,
      'x-refresh-token': refreshToken || '',
      'Content-Type': 'application/json',
    },
  });

  // Check if tokens were refreshed
  const newAccessToken = response.headers.get('x-new-access-token');
  const newRefreshToken = response.headers.get('x-new-refresh-token');
  const tokenRefreshed = response.headers.get('x-token-refreshed');

  if (tokenRefreshed === 'true' && newAccessToken && newRefreshToken) {
    // Update stored tokens
    localStorage.setItem('accessToken', newAccessToken);
    localStorage.setItem('refreshToken', newRefreshToken);
    
    // Update expires time if provided
    const expiresIn = response.headers.get('x-token-expires-in');
    if (expiresIn) {
      localStorage.setItem('tokenExpiresAt', Date.now() + (parseInt(expiresIn) * 1000));
    }
  }

  return response;
}

// Usage
const data = await makeAuthenticatedRequest('https://jethings-backend.fly.dev/api/protected-endpoint', {
  method: 'GET',
});
Response Headers (Automatic Refresh): When automatic token refresh occurs, the backend responds with:
HeaderDescription
x-new-access-tokenNew access token (if refreshed)
x-new-refresh-tokenNew refresh token (if refreshed)
x-token-refreshed"true" if tokens were refreshed
x-token-expires-inToken expiration time in seconds

Option 2: Manual Token Refresh

Implement your own refresh logic before making requests. This gives you more control over when tokens are refreshed. Implementation Example:
async function ensureValidToken(): Promise<string> {
  const accessToken = localStorage.getItem('accessToken');
  const refreshToken = localStorage.getItem('refreshToken');
  const expiresAt = localStorage.getItem('tokenExpiresAt');

  // Check if token is expired or about to expire (within 1 minute)
  if (!accessToken || !expiresAt || Date.now() >= parseInt(expiresAt) - 60000) {
    if (!refreshToken) {
      // Redirect to login
      window.location.href = '/login';
      throw new Error('No valid tokens');
    }

    // Refresh the token
    const response = await fetch('https://jethings-backend.fly.dev/auth/refresh-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken }),
    });

    if (!response.ok) {
      // Refresh token is invalid, redirect to login
      localStorage.removeItem('accessToken');
      localStorage.removeItem('refreshToken');
      localStorage.removeItem('tokenExpiresAt');
      window.location.href = '/login';
      throw new Error('Token refresh failed');
    }

    const data = await response.json();
    
    // Update stored tokens
    localStorage.setItem('accessToken', data.accessToken);
    localStorage.setItem('refreshToken', data.refreshToken);
    localStorage.setItem('tokenExpiresAt', Date.now() + (data.expiresIn * 1000));

    return data.accessToken;
  }

  return accessToken;
}

async function makeAuthenticatedRequest(url: string, options: RequestInit = {}) {
  const accessToken = await ensureValidToken();

  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
  });
}

React Hook Example

For React applications, here’s a custom hook for managing authentication:
import { useState, useEffect, useCallback } from 'react';

function useAuth() {
  const [accessToken, setAccessToken] = useState<string | null>(
    localStorage.getItem('accessToken')
  );
  const [refreshToken, setRefreshToken] = useState<string | null>(
    localStorage.getItem('refreshToken')
  );

  const refreshTokens = useCallback(async () => {
    if (!refreshToken) return false;

    try {
      const response = await fetch('https://jethings-backend.fly.dev/auth/refresh-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken }),
      });

      if (!response.ok) {
        // Clear tokens and redirect to login
        setAccessToken(null);
        setRefreshToken(null);
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
        return false;
      }

      const data = await response.json();
      setAccessToken(data.accessToken);
      setRefreshToken(data.refreshToken);
      localStorage.setItem('accessToken', data.accessToken);
      localStorage.setItem('refreshToken', data.refreshToken);
      return true;
    } catch (error) {
      console.error('Token refresh failed:', error);
      return false;
    }
  }, [refreshToken]);

  const makeRequest = useCallback(async (url: string, options: RequestInit = {}) => {
    const response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${accessToken}`,
        'x-refresh-token': refreshToken || '',
        'Content-Type': 'application/json',
      },
    });

    // Handle automatic token refresh
    const newAccessToken = response.headers.get('x-new-access-token');
    const newRefreshToken = response.headers.get('x-new-refresh-token');
    const tokenRefreshed = response.headers.get('x-token-refreshed');

    if (tokenRefreshed === 'true' && newAccessToken && newRefreshToken) {
      setAccessToken(newAccessToken);
      setRefreshToken(newRefreshToken);
      localStorage.setItem('accessToken', newAccessToken);
      localStorage.setItem('refreshToken', newRefreshToken);
    }

    return response;
  }, [accessToken, refreshToken]);

  return { accessToken, refreshToken, refreshTokens, makeRequest };
}

Axios Interceptor Example

For applications using Axios, you can use interceptors to handle token refresh automatically:
import axios from 'axios';

// Create axios instance
const apiClient = axios.create({
  baseURL: 'https://jethings-backend.fly.dev',
});

// Request interceptor to add tokens
apiClient.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem('accessToken');
    const refreshToken = localStorage.getItem('refreshToken');

    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    if (refreshToken) {
      config.headers['x-refresh-token'] = refreshToken;
    }

    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor to handle token refresh
apiClient.interceptors.response.use(
  (response) => {
    const newAccessToken = response.headers['x-new-access-token'];
    const newRefreshToken = response.headers['x-new-refresh-token'];
    const tokenRefreshed = response.headers['x-token-refreshed'];

    if (tokenRefreshed === 'true' && newAccessToken && newRefreshToken) {
      localStorage.setItem('accessToken', newAccessToken);
      localStorage.setItem('refreshToken', newRefreshToken);
    }

    return response;
  },
  async (error) => {
    const originalRequest = error.config;

    // If 401 and we haven't already tried to refresh
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      const refreshToken = localStorage.getItem('refreshToken');
      if (!refreshToken) {
        // Redirect to login
        window.location.href = '/login';
        return Promise.reject(error);
      }

      try {
        const response = await axios.post('https://jethings-backend.fly.dev/auth/refresh-token', {
          refreshToken,
        });

        const { accessToken, refreshToken: newRefreshToken } = response.data;
        localStorage.setItem('accessToken', accessToken);
        localStorage.setItem('refreshToken', newRefreshToken);

        // Retry original request with new token
        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
        return apiClient(originalRequest);
      } catch (refreshError) {
        // Refresh failed, redirect to login
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

Sign Out / Token Revocation

Always revoke refresh tokens when the user signs out:
async function signOut() {
  const refreshToken = localStorage.getItem('refreshToken');
  
  if (refreshToken) {
    try {
      await fetch('https://jethings-backend.fly.dev/auth/revoke-token', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ refreshToken }),
      });
    } catch (error) {
      console.error('Error revoking token:', error);
    }
  }

  // Clear local storage
  localStorage.removeItem('accessToken');
  localStorage.removeItem('refreshToken');
  localStorage.removeItem('tokenExpiresAt');
  
  // Redirect to login
  window.location.href = '/login';
}

Security Best Practices

Important Security Considerations:
  • Never expose refresh tokens in URLs, client-side logs, or error messages
  • Use HTTPS in production to protect tokens in transit
  • Consider httpOnly cookies for refresh tokens in web applications
  • Implement token rotation - always use new refresh tokens when received
  • Handle token expiration gracefully - redirect to login when refresh fails
  • Store tokens securely - use secure storage mechanisms appropriate for your platform
  • Revoke tokens on logout - always call the revoke endpoint when users sign out
  • Clear tokens on errors - remove stored tokens if refresh fails

🔄 Token Management

Refresh Token

Get new access and refresh tokens when the current access token expires.
curl -X POST https://jethings-backend.fly.dev/auth/refresh-token \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"}'
Request Body:
FieldTypeRequiredDescription
refreshTokenstringValid refresh token
Success Response (200):
{
  "message": "Tokens refreshed successfully",
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1",
  "expiresIn": 900
}
Error Responses:
  • 401 Unauthorized: Invalid or expired refresh token

Revoke Token

Revoke a specific refresh token (logout from one device).
curl -X POST https://jethings-backend.fly.dev/auth/revoke-token \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"}'
Success Response (200):
{
  "message": "Refresh token revoked successfully"
}

Revoke All Tokens

Revoke all refresh tokens for the current user (logout from all devices).
curl -X POST https://jethings-backend.fly.dev/auth/revoke-all-tokens \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Success Response (200):
{
  "message": "All refresh tokens revoked successfully"
}

Logout

Alias for revoke-all-tokens (logout from all devices).
curl -X POST https://jethings-backend.fly.dev/auth/logout \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Success Response (200):
{
  "message": "All refresh tokens revoked successfully"
}

🔒 Password Management

Request Password Reset

Request a password reset OTP to be sent to user’s email.
curl -X POST https://jethings-backend.fly.dev/auth/request-password-reset \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]"}'
Request Body:
FieldTypeRequiredDescription
emailstringUser’s email address
Success Response (200):
{
  "message": "If that email exists, a reset code was sent"
}
Always returns this message to prevent email enumeration attacks.

Verify Password Reset

Reset password using the OTP sent to user’s email.
curl -X POST https://jethings-backend.fly.dev/auth/verify-password-reset \
  -H "Content-Type: application/json" \
  -d '{
    "otp": "123456",
    "newPassword": "newStrongPass456"
  }'
Request Body:
FieldTypeRequiredDescription
otpstring6-digit reset code
newPasswordstringNew password (min 6 characters)
Success Response (200):
{
  "message": "Password reset successful"
}
Error Responses:
  • 400 Bad Request: Invalid or expired OTP
  • 400 Bad Request: Too many attempts (max 3)

🛠️ Development Endpoints

Create Super Admin (Development Only)

Create a super admin account for development purposes.
This endpoint is only available when NODE_ENV=development
curl -X POST https://jethings-backend.fly.dev/auth/dev/super-admin \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Super",
    "lastName": "Admin",
    "email": "[email protected]",
    "password": "superSecurePassword123",
    "phoneNumber": "+1234567890",
    "age": 35,
    "description": "Development super administrator"
  }'
Request Body:
FieldTypeRequiredDescription
firstNamestringSuper admin’s first name
lastNamestringSuper admin’s last name
emailstringValid email address (must be unique)
passwordstringMinimum 6 characters
phoneNumberstringValid phone number (must be unique)
agenumberInteger between 1-120
descriptionstringOptional description
Success Response (200):
{
  "message": "Super admin created successfully (Development Only)",
  "admin": {
    "id": "ck_789...",
    "email": "[email protected]",
    "firstName": "Super",
    "lastName": "Admin",
    "age": 35,
    "phoneNumber": "+1234567890",
    "avatarUrl": null,
    "description": "Development super administrator",
    "roles": ["super_admin"],
    "isEmailVerified": true,
    "isActive": true,
    "lastActivity": null,
    "createdAt": "2024-01-15T12:00:00.000Z",
    "updatedAt": "2024-01-15T12:00:00.000Z",
    "isAdmin": false,
    "isSuperAdmin": true
  }
}
Error Responses:
  • 403 Forbidden: Endpoint only available in development
  • 409 Conflict: Email already exists
  • 409 Conflict: Phone number already exists

🔧 Error Handling

Common Error Codes

Status CodeDescriptionCommon Causes
400 Bad RequestInvalid request dataValidation errors, malformed JSON
401 UnauthorizedAuthentication requiredMissing/invalid token, expired token
403 ForbiddenInsufficient permissionsWrong role, development-only endpoint
404 Not FoundResource not foundInvalid endpoint, missing resource
409 ConflictResource conflictEmail/phone already exists
500 Internal Server ErrorServer errorDatabase issues, unexpected errors

Error Response Format

{
  "message": "Error description",
  "statusCode": 400
}

Handling Errors in Your Code

try {
  const response = await fetch('https://jethings-backend.fly.dev/auth/signin', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Authentication failed');
  }

  const data = await response.json();
  // Handle success
} catch (error) {
  console.error('Auth error:', error.message);
  // Handle error
}

🚀 Next Steps

Now that you understand the Authentication API, explore the Users API for user management features, or check out our Flutter Integration Guide for mobile development.