Authentication Endpoints Index

Token-Based Authentication

This API uses secure opaque tokens for authentication. These tokens are cryptographically secure random strings that are validated against database sessions.

How Token Authentication Works

  1. When you successfully log in or register, the server generates a secure opaque access token.
  2. This token must be included in the Authorization header as a Bearer token for all protected endpoints.
  3. The token is validated against active sessions stored in the database.
  4. Access tokens have an expiration time of 30 minutes from issuance.
  5. Refresh tokens are valid for 6 months and can be used to obtain new access tokens.
  6. When an access token expires, you can use the refresh token endpoint to obtain a new valid token.
  7. Sessions persist across server restarts, ensuring uninterrupted authentication.

Example authorization header:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...

Token Types and Persistent Authentication

The authentication system uses opaque tokens with dual expiration times for enhanced security and user experience:

Token Type Purpose Lifetime Storage
Access Token API request authentication 30 minutes Client memory/secure storage
Refresh Token Obtain new access tokens 6 months (sliding window) Secure client storage only
CSRF Token Request validation Same as access token Client memory

Persistent Login Flow

For persistent authentication, clients should implement automatic token refresh:

  1. Initial Login: Receive access token (30min), refresh token (6mo), and CSRF token
  2. API Requests: Use access token in Authorization header for all API calls
  3. Token Expiry Detection: Monitor for 401 responses indicating expired access token
  4. Automatic Refresh: Use refresh token to obtain new access token via /api/auth/refresh
  5. Sliding Window: Each refresh extends the refresh token expiration (sliding window)
  6. Seamless Experience: Users stay logged in for up to 6 months without re-entering credentials

Token Refresh Example:

// Client detects 401 response
if (response.status === 401) {
    const refreshResponse = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${currentAccessToken}`, // Required for consistency
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            refreshToken: storedRefreshToken // Takes priority over header token
        })
    });
    
    if (refreshResponse.ok) {
        const newTokens = await refreshResponse.json();
        // Update stored tokens and retry original request
        updateStoredTokens(newTokens);
        return retryOriginalRequest();
    } else {
        // Refresh failed, redirect to login
        redirectToLogin();
    }
}

Client-Side Implementation

For a seamless user experience, implement automatic token refresh in your client application:

Token Manager Class:

class TokenManager {
    setTokens(authResponse) {
        this.accessToken = authResponse.token;  // API returns 'token'
        this.refreshToken = authResponse.refreshToken;
        this.csrfToken = authResponse.csrfToken;
        this.expiresAt = new Date(Date.now() + authResponse.expiresIn * 1000);
        
        // Store in secure storage
        localStorage.setItem('tokens', JSON.stringify({
            accessToken: this.accessToken,
            refreshToken: this.refreshToken,
            csrfToken: this.csrfToken,
            expiresAt: this.expiresAt.toISOString()
        }));
    }

    isAccessTokenExpired() {
        return !this.expiresAt || new Date() >= this.expiresAt;
    }
}

API Client with Auto-Refresh:

class ApiClient {
    async makeRequest(url, options = {}) {
        // Check if token needs refresh
        if (tokenManager.isAccessTokenExpired()) {
            await authService.refreshTokens();
        }

        // Add auth headers
        const headers = {
            ...options.headers,
            'Authorization': `Bearer ${tokenManager.getAccessToken()}`,
            'x-csrf-token': tokenManager.getCsrfToken()
        };

        const response = await fetch(url, { ...options, headers });
        
        // Handle 401 with retry
        if (response.status === 401) {
            const newToken = await authService.refreshTokens();
            if (newToken) {
                headers.Authorization = `Bearer ${newToken}`;
                return fetch(url, { ...options, headers });
            }
        }
        
        return response;
    }
}

Key Implementation Features:

CSRF Protection

This API implements CSRF (Cross-Site Request Forgery) protection for all mutation operations (POST, PUT, PATCH, DELETE).

How CSRF Protection Works

  1. When you log in or register, the server returns a CSRF token along with your authentication token.
  2. You must include this CSRF token in the x-csrf-token header for all subsequent mutation requests.
  3. Requests without a valid CSRF token will be rejected with a 403 Forbidden response.
  4. The CSRF token is validated against the token stored in your active session in the database.
  5. CSRF tokens are tied to your session validity and will become invalid when your session expires or you log out.
  6. Authentication endpoints themselves are exempt from CSRF protection.

Example authentication response with CSRF token:

{
    "token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...",
    "refreshToken": "XYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn...",
    "csrfToken": "a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3",
    "expiresIn": 1800,
    "user": {
        "id": 1,
        "username": "John Doe",
        "email": "john@example.com",
        "typeCode": "SUBS",
        "typeName": "Subscribed",
        "firstName": "John",
        "lastName": "Doe",
        "isActive": true,
        "createdAt": "2023-01-15T08:30:00Z",
        "verifiedAt": "2023-01-15T08:35:00Z",
        "updatedAt": "2023-01-15T08:30:00Z",
        "authTypeCode": "EMAI",
        "authTypeName": "Email",
        "subscriptionExemptionStartsAt": null,
        "subscriptionExemptionEndsAt": null,
        "legacyUserId": null,
        "lastLoginAt": "2023-01-15T08:35:00Z",
        "subscription": {
            "id": 1,
            "userId": 1,
            "email": "john@example.com",
            "username": "John Doe",
            "plan": {
                "id": 1,
                "stripePriceId": "price_...",
                "name": "Pro Plan",
                "interval": "month",
                "amount": 999,
                "currency": "usd",
                "trialPeriodDays": 14,
                "isActive": true,
                "createdAt": "2023-01-15T08:30:00Z",
                "updatedAt": null
            },
            "status": "active",
            "currentPeriodStart": "2023-06-01T00:00:00Z",
            "currentPeriodEnd": "2023-07-01T00:00:00Z",
            "trialStart": null,
            "trialEnd": null,
            "cancelAtPeriodEnd": false,
            "canceledAt": null,
            "createdAt": "2023-06-01T00:00:00Z",
            "updatedAt": null,
            "pauseCollectionResumesAt": "2026-04-15T00:00:00Z",
            "nextBillingDate": "2026-05-01T00:00:00Z",
            "promotion": {
                "id": 5,
                "name": "Summer Sale 2023",
                "coupon": {
                    "id": 3,
                    "type": "DISC",
                    "name": "Discount Percentage",
                    "percentOff": 20,
                    "trialDays": null
                }
            },
            "discount": {
                "percentOff": 20,
                "amountOff": 200,
                "discountedAmount": 799
            },
            "shopifyPromotion": {
                "id": 12,
                "shopifyOrderId": 99001,
                "name": "Shopify-99001",
                "durationDays": 30,
                "percentOff": null,
                "appliedAt": "2026-03-15T09:00:00Z"
            }
        },
        "promotion": {
            "id": 5,
            "name": "Summer Sale 2023",
            "code": "SUMMER23",
            "appliedAt": "2026-03-10T12:00:00Z",
            "validUntil": null
        }
    }
}

Note: The subscription field will be null if the user does not have an active subscription.

Example request with CSRF protection:

// Request headers
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
x-csrf-token: a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3
Content-Type: application/json

Legacy User Migration

When a user logs in or registers, the system automatically attempts to migrate any existing Stripe subscription linked to their email address. This is called legacy subscription migration and runs transparently in the background with a 5-second timeout — it never blocks the auth response. The depth of what gets migrated, particularly promotions, depends on whether the user has a corresponding record in the legacy_users table.

When Migration Is Triggered

Migration is triggered via migrateLegacySubscription(userId, email) automatically during login and registration, but only for SUBS type users.

Registration
Register request received
Email already in DB?
Yes → AUTH_EMAIL_EXISTS
Migration never runs
↓ No
Create user in local DB
typeCode = "SUBS"
✓ Migration runs
Login
Login request received
User exists in DB?
No → AUTH_INVALID_CREDENTIALS
Migration never runs
↓ Yes
Is SUBS type user?
No → Skip — migration does not run for NONS users on login
↓ Yes
✓ Migration runs
migrateLegacySubscription(userId, email)
Runs with a 5-second timeout — auth response is never blocked

What Migration Does (Step by Step)

Once triggered, migrateLegacySubscription performs the following steps in sequence. Any step that finds no data marks the audit as SKIP and returns early — all subsequent steps are skipped.

0
Create migration audit record in DB for tracking this attempt
1
Search Stripe by email: stripe.customers.list({ email, limit: 1 })
No match → audit SKIP, synced_at touched, return early
2
List all subscriptions for the Stripe customer (up to 100)
None found → audit SKIP, return early
3
Filter to migratable statuses: keeps only active, trialing, past_due. Canceled, incomplete, and unpaid subscriptions are discarded.
None qualify → audit SKIP, return early
4
Select primary subscription if multiple qualify: priority order is active > trialing > past_due, then latest current_period_end. Skipped subscription IDs are logged to a separate audit record for manual review.
5
Extract Stripe price ID from subscription.items.data[0].price.id
6
Sync plan locally: find or create a row in subscription_plans for this Stripe price. If new, fetches product name, interval, amount, and currency from Stripe.
7
Extract billing metadata: current period start/end, trial start/end, canceled_at. Checks both root-level and subscription item fields with fallbacks.
8
Upsert local subscription: INSERT or UPDATE a row in user_subscriptions with all Stripe data. Sets migration_status_code = 'MIGR' and stamps migrated_at and synced_at. Idempotent — safe to run on every login.
9
Update migration audit to MIGR status with the Stripe customer and subscription IDs
10
Sync promotion redemptions — queries promotion_redemptions for this user where user_subscription_id IS NULL or stripe_discount_id IS NULL and the promotion has a Stripe coupon. Re-fetches the subscription from Stripe with expand: ['discounts'] and backfills both fields. Failures here do not affect migration success — the subscription is already written.
Result: Returns { success: true, subscriptionId, stripeSubscriptionId, status }. The subscription is included in the auth response via a fresh getUserSubscription(userId, forceSync: true) call.

Promotion Migration: Legacy vs Non-Legacy Users

The legacy_users table contains records imported from the old system. The users.legacy_user_id column links a user to their legacy record and is set by a one-time admin bulk migration. It is NULL for every user who registers fresh on the new system.

Step 10 of the migration behaves very differently depending on this field — specifically whether promotion_redemptions rows were pre-seeded for the user before they ever logged in.

User IS in legacy_users
legacy_user_id IS NOT NULL
Before first login — admin bulk operation
migrateLegacyUserGroups() reads discount % and group data from legacy_users, creates the matching promotions, and inserts promotion_redemptions rows with user_id set but user_subscription_id = NULL
User logs in or registers → migration runs
Steps 1–9: Stripe subscription found by email and written to user_subscriptions
Step 10: Finds pre-seeded rows in promotion_redemptions. Re-fetches subscription from Stripe with expand: ['discounts']. Backfills user_subscription_id and stripe_discount_id on each row.
✓ Subscription migrated
✓ Promotion fully linked
User is NOT in legacy_users
legacy_user_id IS NULL
No pre-migration step runs. No promotion_redemptions rows exist for this user.
User logs in or registers → migration runs
Steps 1–9: Stripe subscription found by email and written to user_subscriptions
Step 10: Queries promotion_redemptions for this user — returns zero rows. Nothing to update. Any Stripe discount on the subscription has no corresponding local record.
✓ Subscription migrated
⚠ Stripe discount orphaned — no local promotion record.
User must apply a promo code manually, or an admin must create the redemption record.

Summary

Scenario Subscription Migrated? Promotion Linked? Notes
In legacy_users, has Stripe subscription ✓ Yes ✓ Yes Admin bulk op pre-seeds redemptions; Step 10 backfills subscription and Stripe discount IDs
In legacy_users, no Stripe subscription (or only cancelled/unpaid) ✗ No ✗ No Migration returns SKIP — no qualifying Stripe subscription found
NOT in legacy_users, has Stripe subscription with discount ✓ Yes ⚠ Partial Subscription fully migrated; Stripe discount exists in Stripe but has no local promotion_redemptions record
NOT in legacy_users, has Stripe subscription, no discount ✓ Yes N/A Subscription fully migrated; no promotion to link
No Stripe customer found for email ✗ No ✗ No Migration returns SKIP — user has no Stripe history. Auth response succeeds normally with no subscription.

Note on the one-discount assumption: When Step 10 backfills Stripe discount IDs, it reads the first discount from the subscription's discounts array and applies that same ID to all unsynced promotion_redemptions rows for that user. If a user has multiple Stripe discounts, only the first one is written across all rows.

Authentication Endpoints

POST /api/auth/apple/callback

Handle Apple OAuth callback and authenticate or register user

Request Body:

{
    "code": "c1234567890abcdef...",
    "idToken": "eyJraWQiOiJXNldjT0tC...",
    "nonce": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
    "redirectUri": "https://yourdomain.com/auth/apple/callback",
    "mode": "login",
    "platform": "ios"
}

Field Descriptions:

Response (200 OK for login, 201 Created for register):

{
    "token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...",
    "refreshToken": "XYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn...",
    "csrfToken": "a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3",
    "expiresIn": 1800,
    "user": {
        "id": 1,
        "username": "John Doe",
        "email": "john@privaterelay.appleid.com",
        "typeCode": "SUBS",
        "typeName": "Subscribed",
        "firstName": "John",
        "lastName": "Doe",
        "isActive": true,
        "createdAt": "2023-01-15T08:30:00Z",
        "verifiedAt": "2023-01-15T08:30:00Z",
        "updatedAt": null,
        "authTypeCode": "APPE",
        "authTypeName": "Apple OAuth",
        "subscriptionExemptionStartsAt": null,
        "subscriptionExemptionEndsAt": null,
        "legacyUserId": null,
        "lastLoginAt": "2023-01-15T08:30:00Z",
        "subscription": {
            "id": 1,
            "userId": 1,
            "email": "john@example.com",
            "username": "John Doe",
            "plan": {
                "id": 1,
                "stripePriceId": "price_...",
                "name": "Pro Plan",
                "interval": "month",
                "amount": 999,
                "currency": "usd",
                "trialPeriodDays": 14,
                "isActive": true,
                "createdAt": "2023-01-15T08:30:00Z",
                "updatedAt": null
            },
            "status": "active",
            "currentPeriodStart": "2023-06-01T00:00:00Z",
            "currentPeriodEnd": "2023-07-01T00:00:00Z",
            "trialStart": null,
            "trialEnd": null,
            "cancelAtPeriodEnd": false,
            "canceledAt": null,
            "createdAt": "2023-06-01T00:00:00Z",
            "updatedAt": null,
            "pauseCollectionResumesAt": "2026-04-15T00:00:00Z",
            "nextBillingDate": "2026-05-01T00:00:00Z",
            "promotion": {
                "id": 5,
                "name": "Summer Sale 2023",
                "coupon": {
                    "id": 3,
                    "type": "DISC",
                    "name": "Discount Percentage",
                    "percentOff": 20,
                    "trialDays": null
                }
            },
            "discount": {
                "percentOff": 20,
                "amountOff": 200,
                "discountedAmount": 799
            },
            "shopifyPromotion": {
                "id": 12,
                "shopifyOrderId": 99001,
                "name": "Shopify-99001",
                "durationDays": 30,
                "percentOff": null,
                "appliedAt": "2026-03-15T09:00:00Z"
            }
        },
        "promotion": {
            "id": 5,
            "name": "Summer Sale 2023",
            "code": "SUMMER23",
            "appliedAt": "2026-03-10T12:00:00Z",
            "validUntil": null
        }
    }
}

Error Responses:

{
    "error": "Missing required parameters",
    "message": "code, nonce, redirectUri, and mode are required",
    "code": "APPLE_MISSING_PARAMS"
}

{
    "error": "Invalid mode",
    "message": "mode must be 'login' or 'register'",
    "code": "APPLE_INVALID_MODE"
}

{
    "error": "Invalid Apple ID token",
    "message": "Failed to verify Apple ID token",
    "code": "APPLE_INVALID_ID_TOKEN"
}

{
    "error": "User not found",
    "message": "No account found with this Apple ID. Please register first.",
    "code": "AUTH_APPLE_USER_NOT_FOUND"
}

{
    "error": "Email already exists",
    "message": "An account with this Apple email already exists. Please sign in instead.",
    "code": "AUTH_EMAIL_EXISTS"
}

{
    "error": "Too many OAuth attempts",
    "message": "Too many OAuth attempts from this IP, please try again later.",
    "code": "RATE_LIMIT_EXCEEDED"
}

Notes:

Apple OAuth Flow Overview:

  1. Client generates random nonce and state values
  2. Client redirects to Apple with nonce and state
  3. User authenticates with Apple ID
  4. Apple redirects back with code, id_token, and state
  5. Client sends code, id_token, and nonce to this endpoint
  6. Server verifies ID token with Apple's public keys
  7. Server validates nonce to prevent replay attacks
  8. Server creates or authenticates user session
POST /api/auth/apple/form-post

Handle Apple OAuth form_post callback (receives data from Apple and redirects to frontend)

Request Body (sent by Apple):

{
    "code": "c1234567890abcdef...",
    "id_token": "eyJraWQiOiJXNldjT0tC...",
    "state": "{\"state\":\"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk\",\"mode\":\"login\",\"returnUrl\":\"http://localhost:5173\"}",
    "user": "{\"name\":{\"firstName\":\"John\",\"lastName\":\"Doe\"},\"email\":\"john@privaterelay.appleid.com\"}"
}

State Parameter Format:

The state parameter contains JSON-encoded data:

{
    "state": "random_string_for_csrf_protection",
    "mode": "login" | "register",
    "returnUrl": "https://yourdomain.com"  // Optional, frontend URL to redirect to
}

Response (200 OK - HTML redirect):

<!DOCTYPE html>
<html>
  <head>
    <title>Apple Sign In</title>
  </head>
  <body>
    <p>Completing Apple Sign-In...</p>
    <script>
      window.location.replace('https://yourdomain.com/auth/apple/callback#code=...&id_token=...&state=...');
    </script>
  </body>
</html>

Error Response (400 Bad Request):

<!DOCTYPE html>
<html>
  <body>
    <script>
      sessionStorage.setItem('appleAuthError', JSON.stringify({
        title: 'Apple Authentication Error',
        description: 'Missing required parameters from Apple',
        type: 'error'
      }));
      window.location.replace('/login');
    </script>
  </body>
</html>

Notes:

Apple Developer Configuration:

In your Apple Services ID, configure the Return URL WITHOUT query parameters:

https://yourdomain.com/api/auth/apple/form-post

Important: Do NOT include query parameters like ?returnUrl=... in the Apple Developer Console configuration. Apple requires exact URL matching.

POST /api/auth/google/callback-pkce

Handle Google OAuth callback with PKCE (Proof Key for Code Exchange) - recommended for mobile apps

Request Body:

{
    "code": "4/0AfJohXlxxx...",
    "codeVerifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
    "redirectUri": "com.googleusercontent.apps.123456789:redirect",
    "mode": "login",
    "platform": "android"
}

Field Descriptions:

Response (200 OK for login, 201 Created for register):

{
    "token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...",
    "refreshToken": "XYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn...",
    "csrfToken": "a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3",
    "expiresIn": 1800,
    "user": {
        "id": 1,
        "username": "John Doe",
        "email": "john@gmail.com",
        "typeCode": "SUBS",
        "typeName": "Subscribed",
        "firstName": "John",
        "lastName": "Doe",
        "isActive": true,
        "createdAt": "2023-01-15T08:30:00Z",
        "verifiedAt": "2023-01-15T08:30:00Z",
        "updatedAt": null,
        "authTypeCode": "GOOG",
        "authTypeName": "Google OAuth",
        "subscriptionExemptionStartsAt": null,
        "subscriptionExemptionEndsAt": null,
        "legacyUserId": null,
        "lastLoginAt": "2023-01-15T08:30:00Z",
        "subscription": {
            "id": 1,
            "userId": 1,
            "email": "john@example.com",
            "username": "John Doe",
            "plan": {
                "id": 1,
                "stripePriceId": "price_...",
                "name": "Pro Plan",
                "interval": "month",
                "amount": 999,
                "currency": "usd",
                "trialPeriodDays": 14,
                "isActive": true,
                "createdAt": "2023-01-15T08:30:00Z",
                "updatedAt": null
            },
            "status": "active",
            "currentPeriodStart": "2023-06-01T00:00:00Z",
            "currentPeriodEnd": "2023-07-01T00:00:00Z",
            "trialStart": null,
            "trialEnd": null,
            "cancelAtPeriodEnd": false,
            "canceledAt": null,
            "createdAt": "2023-06-01T00:00:00Z",
            "updatedAt": null,
            "pauseCollectionResumesAt": "2026-04-15T00:00:00Z",
            "nextBillingDate": "2026-05-01T00:00:00Z",
            "promotion": {
                "id": 5,
                "name": "Summer Sale 2023",
                "coupon": {
                    "id": 3,
                    "type": "DISC",
                    "name": "Discount Percentage",
                    "percentOff": 20,
                    "trialDays": null
                }
            },
            "discount": {
                "percentOff": 20,
                "amountOff": 200,
                "discountedAmount": 799
            },
            "shopifyPromotion": {
                "id": 12,
                "shopifyOrderId": 99001,
                "name": "Shopify-99001",
                "durationDays": 30,
                "percentOff": null,
                "appliedAt": "2026-03-15T09:00:00Z"
            }
        },
        "promotion": {
            "id": 5,
            "name": "Summer Sale 2023",
            "code": "SUMMER23",
            "appliedAt": "2026-03-10T12:00:00Z",
            "validUntil": null
        }
    }
}

Error Responses:

{
    "error": "Missing required parameters",
    "message": "code, codeVerifier, redirectUri, and mode are required",
    "code": "PKCE_MISSING_PARAMS"
}

{
    "error": "Invalid mode",
    "message": "mode must be 'login' or 'register'",
    "code": "PKCE_INVALID_MODE"
}

{
    "error": "Invalid redirect URI",
    "message": "Redirect URI does not match expected pattern for platform",
    "code": "PKCE_INVALID_REDIRECT_URI"
}

{
    "error": "Failed to exchange authorization code",
    "message": "Token exchange failed",
    "code": "PKCE_TOKEN_EXCHANGE_FAILED"
}

{
    "error": "User not found",
    "message": "No account found with this Google email. Please register first.",
    "code": "AUTH_GOOGLE_USER_NOT_FOUND"
}

{
    "error": "Email already exists",
    "message": "An account with this Google email already exists. Please sign in instead.",
    "code": "AUTH_EMAIL_EXISTS"
}

{
    "error": "Too many OAuth attempts",
    "message": "Too many OAuth attempts from this IP, please try again later.",
    "code": "RATE_LIMIT_EXCEEDED"
}

Notes:

PKCE Flow Overview:

  1. Client generates codeVerifier (random string)
  2. Client generates codeChallenge from verifier (SHA256 hash)
  3. Client redirects to Google with codeChallenge
  4. User authenticates with Google
  5. Google redirects back with authorization code
  6. Client sends code + codeVerifier to this endpoint
  7. Server exchanges code + verifier with Google for tokens
  8. Server validates and creates user session
POST /api/auth/google/callback

Handle Google OAuth callback and exchange authorization code for tokens

Request Body:

{
    "code": "4/0AfJohXlxxx...",
    "mode": "login",
    "redirectUri": "http://localhost:3000/auth/google/callback"
}

Field Descriptions:

Response (200 OK):

{
    "googleIdToken": "eyJzdWIiOiIxMjM0NTY3ODkw...",
    "message": "Google authentication successful"
}

Error Responses:

{
    "error": "Missing required parameters",
    "message": "code, mode, and redirectUri are required",
    "code": "GOOGLE_AUTH_MISSING_PARAMS"
}

{
    "error": "Invalid mode",
    "message": "mode must be 'login' or 'register'",
    "code": "GOOGLE_AUTH_INVALID_MODE"
}

{
    "error": "Failed to exchange authorization code",
    "message": "Google authentication failed",
    "code": "GOOGLE_AUTH_TOKEN_EXCHANGE_FAILED"
}

{
    "error": "Failed to retrieve user information",
    "message": "Could not get user profile from Google",
    "code": "GOOGLE_AUTH_USER_INFO_FAILED"
}

Notes:

POST /api/auth/google/login

Authenticate with Google ID token

Request Body:

{
    "googleIdToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
    "platform": "android"  // Optional: "web", "android", or "ios"
}

Field Descriptions:

Response (200 OK):

{
    "token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...", // Valid for 30 minutes
    "refreshToken": "XYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn...", // Valid for 6 months
    "csrfToken": "a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3",
    "expiresIn": 1800, // 30 minutes in seconds
    "user": {
        "id": 1,
        "username": "John Doe",
        "email": "john@gmail.com",
        "typeCode": "SUBS",
        "typeName": "Subscribed",
        "firstName": "John",
        "lastName": "Doe",
        "isActive": true,
        "createdAt": "2023-01-15T08:30:00Z",
        "verifiedAt": "2023-01-15T08:35:00Z",
        "updatedAt": "2023-01-15T08:30:00Z",
        "authTypeCode": "GOOG",
        "authTypeName": "Google OAuth",
        "subscriptionExemptionStartsAt": null,
        "subscriptionExemptionEndsAt": null,
        "legacyUserId": null,
        "lastLoginAt": "2023-01-15T08:35:00Z",
        "subscription": {
            "id": 1,
            "userId": 1,
            "email": "john@example.com",
            "username": "John Doe",
            "plan": {
                "id": 1,
                "stripePriceId": "price_...",
                "name": "Pro Plan",
                "interval": "month",
                "amount": 999,
                "currency": "usd",
                "trialPeriodDays": 14,
                "isActive": true,
                "createdAt": "2023-01-15T08:30:00Z",
                "updatedAt": null
            },
            "status": "active",
            "currentPeriodStart": "2023-06-01T00:00:00Z",
            "currentPeriodEnd": "2023-07-01T00:00:00Z",
            "trialStart": null,
            "trialEnd": null,
            "cancelAtPeriodEnd": false,
            "canceledAt": null,
            "createdAt": "2023-06-01T00:00:00Z",
            "updatedAt": null,
            "pauseCollectionResumesAt": "2026-04-15T00:00:00Z",
            "nextBillingDate": "2026-05-01T00:00:00Z",
            "promotion": {
                "id": 5,
                "name": "Summer Sale 2023",
                "coupon": {
                    "id": 3,
                    "type": "DISC",
                    "name": "Discount Percentage",
                    "percentOff": 20,
                    "trialDays": null
                }
            },
            "discount": {
                "percentOff": 20,
                "amountOff": 200,
                "discountedAmount": 799
            },
            "shopifyPromotion": {
                "id": 12,
                "shopifyOrderId": 99001,
                "name": "Shopify-99001",
                "durationDays": 30,
                "percentOff": null,
                "appliedAt": "2026-03-15T09:00:00Z"
            }
        },
        "promotion": {
            "id": 5,
            "name": "Summer Sale 2023",
            "code": "SUMMER23",
            "appliedAt": "2026-03-10T12:00:00Z",
            "validUntil": null
        }
    }
}

Error Responses:

{
    "message": "Invalid Google ID token",
    "code": "AUTH_INVALID_GOOGLE_TOKEN"
}

{
    "message": "No account found with this Google email. Please register first.",
    "code": "AUTH_GOOGLE_USER_NOT_FOUND"
}

Notes:

POST /api/auth/google/register

Register a new user account with Google ID token

Request Body:

{
    "googleIdToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
    "platform": "ios"  // Optional: "web", "android", or "ios"
}

Field Descriptions:

Response (201 Created):

{
    "token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...", // Valid for 30 minutes
    "refreshToken": "XYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn...", // Valid for 6 months
    "csrfToken": "f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3",
    "expiresIn": 1800, // 30 minutes in seconds
    "user": {
        "id": 2,
        "username": "John Doe",
        "email": "john@gmail.com",
        "typeCode": "SUBS",
        "typeName": "Subscribed",
        "firstName": "John",
        "lastName": "Doe",
        "isActive": true,
        "createdAt": "2023-01-15T08:30:00Z",
        "verifiedAt": "2023-01-15T08:30:00Z",
        "updatedAt": null,
        "authTypeCode": "GOOG",
        "authTypeName": "Google OAuth",
        "subscriptionExemptionStartsAt": null,
        "subscriptionExemptionEndsAt": null,
        "legacyUserId": null,
        "lastLoginAt": null,
        "subscription": {
            "id": 1,
            "userId": 1,
            "email": "john@example.com",
            "username": "John Doe",
            "plan": {
                "id": 1,
                "stripePriceId": "price_...",
                "name": "Pro Plan",
                "interval": "month",
                "amount": 999,
                "currency": "usd",
                "trialPeriodDays": 14,
                "isActive": true,
                "createdAt": "2023-01-15T08:30:00Z",
                "updatedAt": null
            },
            "status": "active",
            "currentPeriodStart": "2023-06-01T00:00:00Z",
            "currentPeriodEnd": "2023-07-01T00:00:00Z",
            "trialStart": null,
            "trialEnd": null,
            "cancelAtPeriodEnd": false,
            "canceledAt": null,
            "createdAt": "2023-06-01T00:00:00Z",
            "updatedAt": null,
            "pauseCollectionResumesAt": "2026-04-15T00:00:00Z",
            "nextBillingDate": "2026-05-01T00:00:00Z",
            "promotion": {
                "id": 5,
                "name": "Summer Sale 2023",
                "coupon": {
                    "id": 3,
                    "type": "DISC",
                    "name": "Discount Percentage",
                    "percentOff": 20,
                    "trialDays": null
                }
            },
            "discount": {
                "percentOff": 20,
                "amountOff": 200,
                "discountedAmount": 799
            },
            "shopifyPromotion": {
                "id": 12,
                "shopifyOrderId": 99001,
                "name": "Shopify-99001",
                "durationDays": 30,
                "percentOff": null,
                "appliedAt": "2026-03-15T09:00:00Z"
            }
        },
        "promotion": {
            "id": 5,
            "name": "Summer Sale 2023",
            "code": "SUMMER23",
            "appliedAt": "2026-03-10T12:00:00Z",
            "validUntil": null
        }
    }
}

Error Responses:

{
    "message": "Invalid Google ID token",
    "code": "AUTH_INVALID_GOOGLE_TOKEN"
}

{
    "message": "Email address is already registered. Please use login instead.",
    "code": "AUTH_EMAIL_EXISTS"
}

Notes:

POST /api/auth/login

Log in with email and password

Request Body:

{
    "email": "user@example.com",
    "password": "yourpassword"
}

Response (200 OK):

{
    "token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...", // Valid for 30 minutes
    "refreshToken": "XYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn...", // Valid for 6 months
    "csrfToken": "a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3",
    "expiresIn": 1800, // 30 minutes in seconds
    "user": {
        "id": 1,
        "username": "John Doe",
        "email": "john@example.com",
        "typeCode": "SUBS",
        "typeName": "Subscribed",
        "firstName": "John",
        "lastName": "Doe",
        "isActive": true,
        "createdAt": "2023-01-15T08:30:00Z",
        "verifiedAt": "2023-01-15T08:35:00Z",
        "updatedAt": "2023-01-15T08:30:00Z",
        "authTypeCode": "EMAI",
        "authTypeName": "Email",
        "subscriptionExemptionStartsAt": null,
        "subscriptionExemptionEndsAt": null,
        "legacyUserId": null,
        "lastLoginAt": "2023-01-15T08:35:00Z",
        "subscription": {
            "id": 1,
            "userId": 1,
            "email": "john@example.com",
            "username": "John Doe",
            "plan": {
                "id": 1,
                "stripePriceId": "price_...",
                "name": "Pro Plan",
                "interval": "month",
                "amount": 999,
                "currency": "usd",
                "trialPeriodDays": 14,
                "isActive": true,
                "createdAt": "2023-01-15T08:30:00Z",
                "updatedAt": null
            },
            "status": "active",
            "currentPeriodStart": "2023-06-01T00:00:00Z",
            "currentPeriodEnd": "2023-07-01T00:00:00Z",
            "trialStart": null,
            "trialEnd": null,
            "cancelAtPeriodEnd": false,
            "canceledAt": null,
            "createdAt": "2023-06-01T00:00:00Z",
            "updatedAt": null,
            "pauseCollectionResumesAt": "2026-04-15T00:00:00Z",
            "nextBillingDate": "2026-05-01T00:00:00Z",
            "promotion": {
                "id": 5,
                "name": "Summer Sale 2023",
                "coupon": {
                    "id": 3,
                    "type": "DISC",
                    "name": "Discount Percentage",
                    "percentOff": 20,
                    "trialDays": null
                }
            },
            "discount": {
                "percentOff": 20,
                "amountOff": 200,
                "discountedAmount": 799
            },
            "shopifyPromotion": {
                "id": 12,
                "shopifyOrderId": 99001,
                "name": "Shopify-99001",
                "durationDays": 30,
                "percentOff": null,
                "appliedAt": "2026-03-15T09:00:00Z"
            }
        },
        "promotion": {
            "id": 5,
            "name": "Summer Sale 2023",
            "code": "SUMMER23",
            "appliedAt": "2026-03-10T12:00:00Z",
            "validUntil": null
        }
    }
}

Error Responses:

{
    "message": "Invalid credentials",
    "code": "AUTH_INVALID_CREDENTIALS",
    "isLegacyUser": true,
    "hasLoggedInBefore": false
}

{
    "message": "Your email address has not been verified. A verification code has been sent to your email address.",
    "code": "AUTH_EMAIL_NOT_VERIFIED"
}

Notes:

POST /api/auth/logout

Log out the current user (invalidate token)

Headers (Optional):

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...

Note: Authorization header is optional. If provided, the specific session will be invalidated. If not provided, logout will still succeed.

Response (200 OK):

{
    "success": true,
    "message": "Logged out successfully"
}
POST /api/auth/refresh-token /api/auth/refresh

Refresh authentication token when the current one is about to expire or has expired. Both endpoints are supported for compatibility.

Headers (Optional):

Authorization: Bearer [current_access_token_or_refresh_token]

Request Body (Optional - takes priority if provided):

{
    "refreshToken": "XYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn..."
}

Notes:

Response (200 OK):

{
    "token": "DEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstu...", // New token valid for 30 minutes
    "refreshToken": "XYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn...", // New refresh token valid for 6 months
    "csrfToken": "f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3", // New CSRF token
    "expiresIn": 1800, // 30 minutes in seconds
    "user": {
        "id": 1,
        "username": "John Doe",
        "email": "john@example.com",
        "typeCode": "SUBS",
        "typeName": "Subscribed",
        "firstName": "John",
        "lastName": "Doe",
        "isActive": true,
        "createdAt": "2023-01-15T08:30:00Z",
        "verifiedAt": "2023-01-15T08:35:00Z",
        "updatedAt": "2023-01-15T08:30:00Z",
        "authTypeCode": "EMAI",
        "authTypeName": "Email",
        "subscriptionExemptionStartsAt": null,
        "subscriptionExemptionEndsAt": null,
        "legacyUserId": null,
        "lastLoginAt": "2023-01-15T08:35:00Z",
        "subscription": {
            "id": 1,
            "userId": 1,
            "email": "john@example.com",
            "username": "John Doe",
            "plan": {
                "id": 1,
                "stripePriceId": "price_...",
                "name": "Pro Plan",
                "interval": "month",
                "amount": 999,
                "currency": "usd",
                "trialPeriodDays": 14,
                "isActive": true,
                "createdAt": "2023-01-15T08:30:00Z",
                "updatedAt": null
            },
            "status": "active",
            "currentPeriodStart": "2023-06-01T00:00:00Z",
            "currentPeriodEnd": "2023-07-01T00:00:00Z",
            "trialStart": null,
            "trialEnd": null,
            "cancelAtPeriodEnd": false,
            "canceledAt": null,
            "createdAt": "2023-06-01T00:00:00Z",
            "updatedAt": null,
            "pauseCollectionResumesAt": "2026-04-15T00:00:00Z",
            "nextBillingDate": "2026-05-01T00:00:00Z",
            "promotion": {
                "id": 5,
                "name": "Summer Sale 2023",
                "coupon": {
                    "id": 3,
                    "type": "DISC",
                    "name": "Discount Percentage",
                    "percentOff": 20,
                    "trialDays": null
                }
            },
            "discount": {
                "percentOff": 20,
                "amountOff": 200,
                "discountedAmount": 799
            },
            "shopifyPromotion": {
                "id": 12,
                "shopifyOrderId": 99001,
                "name": "Shopify-99001",
                "durationDays": 30,
                "percentOff": null,
                "appliedAt": "2026-03-15T09:00:00Z"
            }
        },
        "promotion": {
            "id": 5,
            "name": "Summer Sale 2023",
            "code": "SUMMER23",
            "appliedAt": "2026-03-10T12:00:00Z",
            "validUntil": null
        }
    }
}

Error Responses:

{
    "message": "No token provided",
    "code": "AUTH_NO_TOKEN"
}

{
    "message": "Invalid token format",
    "code": "AUTH_INVALID_TOKEN_FORMAT"
}

{
    "message": "No active session found. Please log in again.",
    "code": "AUTH_SESSION_NOT_FOUND",
    "requiresLogout": true
}

{
    "message": "Your session has been revoked. Please log in again.",
    "code": "AUTH_SESSION_REVOKED",
    "requiresLogout": true
}

{
    "message": "Your session has expired. Please log in again.",
    "code": "AUTH_REFRESH_EXPIRED",
    "requiresLogout": true
}

{
    "message": "User account issue. Please log in again.",
    "code": "AUTH_USER_NOT_FOUND",
    "requiresLogout": true
}

{
    "message": "Authentication error. Please log in again.",
    "code": "AUTH_ERROR",
    "requiresLogout": true
}
POST /api/auth/register

Register a new user account

Request Body:

{
    "username": "johnsmith",
    "email": "john@example.com",
    "password": "securePassword123",
    "firstName": "John",
    "lastName": "Smith"
}

Field Descriptions:

Response (201 Created):

{
    "token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...", // Valid for 30 minutes
    "refreshToken": "XYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn...", // Valid for 6 months
    "csrfToken": "f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3",
    "expiresIn": 1800, // 30 minutes in seconds
    "user": {
        "id": 2,
        "username": "johnsmith",
        "email": "john@example.com",
        "typeCode": "SUBS",
        "typeName": "Subscribed",
        "firstName": "John",
        "lastName": "Smith",
        "isActive": true,
        "createdAt": "2023-01-15T08:30:00Z",
        "verifiedAt": null,
        "updatedAt": null,
        "authTypeCode": "EMAI",
        "authTypeName": "Email",
        "subscriptionExemptionStartsAt": null,
        "subscriptionExemptionEndsAt": null,
        "legacyUserId": null,
        "lastLoginAt": null,
        "subscription": {
            "id": 1,
            "userId": 1,
            "email": "john@example.com",
            "username": "John Doe",
            "plan": {
                "id": 1,
                "stripePriceId": "price_...",
                "name": "Pro Plan",
                "interval": "month",
                "amount": 999,
                "currency": "usd",
                "trialPeriodDays": 14,
                "isActive": true,
                "createdAt": "2023-01-15T08:30:00Z",
                "updatedAt": null
            },
            "status": "active",
            "currentPeriodStart": "2023-06-01T00:00:00Z",
            "currentPeriodEnd": "2023-07-01T00:00:00Z",
            "trialStart": null,
            "trialEnd": null,
            "cancelAtPeriodEnd": false,
            "canceledAt": null,
            "createdAt": "2023-06-01T00:00:00Z",
            "updatedAt": null,
            "pauseCollectionResumesAt": "2026-04-15T00:00:00Z",
            "nextBillingDate": "2026-05-01T00:00:00Z",
            "promotion": {
                "id": 5,
                "name": "Summer Sale 2023",
                "coupon": {
                    "id": 3,
                    "type": "DISC",
                    "name": "Discount Percentage",
                    "percentOff": 20,
                    "trialDays": null
                }
            },
            "discount": {
                "percentOff": 20,
                "amountOff": 200,
                "discountedAmount": 799
            },
            "shopifyPromotion": {
                "id": 12,
                "shopifyOrderId": 99001,
                "name": "Shopify-99001",
                "durationDays": 30,
                "percentOff": null,
                "appliedAt": "2026-03-15T09:00:00Z"
            }
        },
        "promotion": {
            "id": 5,
            "name": "Summer Sale 2023",
            "code": "SUMMER23",
            "appliedAt": "2026-03-10T12:00:00Z",
            "validUntil": null
        }
    }
}

Error Responses:

{
    "message": "Email address is already registered",
    "code": "AUTH_EMAIL_EXISTS"
}

{
    "message": "Validation error",
    "code": "VALIDATION_ERROR",
    "errors": [
        {
            "type": "field",
            "value": "",
            "msg": "Username is required",
            "path": "username",
            "location": "body"
        }
    ]
}

Notes:

POST /api/auth/reset-password

Request a password reset code

Request Body:

{
    "email": "user@example.com"
}

Response (200 OK):

{
    "message": "Password reset code has been sent to your email address"
}

Error Responses:

{
    "message": "Email address not found"
}

Notes:

POST /api/auth/verify-password

Verify user's current password (requires authentication and CSRF token)

Headers:

Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
x-csrf-token: a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3

Request Body:

{
    "password": "currentUserPassword"
}

Response (200 OK):

{
    "message": "Password verified successfully"
}

Error Responses:

{
    "message": "Authentication required"
}

{
    "message": "Password is required"
}

{
    "message": "Invalid password"
}

{
    "message": "Account has been deactivated"
}

{
    "message": "Password verification is not available for Google accounts"
}

Notes:

POST /api/auth/verify-registration

Verify a registration with the verification code

Request Body:

{
    "email": "user@example.com",
    "verificationCode": "123456"
}

Response (200 OK):

{
    "message": "Email verified successfully"
}

Error Responses:

{
    "message": "Invalid email address"
}

{
    "message": "No verification request found"
}

{
    "message": "Maximum attempts exceeded"
}

{
    "message": "Verification code has expired"
}

{
    "message": "Invalid verification code"
}

Notes:

POST /api/auth/verify-reset-password

Reset password using verification code

POST /api/auth/verify-reset-password

Reset password using verification code

Request Body:

{
    "email": "user@example.com",
    "verificationCode": "123456",
    "newPassword": "newSecurePassword"
}

Response (200 OK):

{
    "message": "Password has been successfully reset"
}

Error Responses:

{
    "message": "Password must be at least 8 characters long"
}

{
    "message": "Invalid email address"
}

{
    "message": "No password reset request found"
}

{
    "message": "Maximum attempts exceeded. Please request a new verification code"
}

{
    "message": "Verification code has expired. Please request a new one"
}

{
    "message": "Invalid verification code"
}

Notes: