Skip to main contentSkip to navigation
Auth Product Reference

AuthFlow™ integrationfor organization owners

This Get Started documents our public auth API for your organization's users. AuthFlow™ provides 20 total /auth routes: 7 session routes for authentication and user management, 3 passwordManagement routes for password resets, 2 emailVerification routes for email verification, and 8 twoFactor routes for two-factor authentication. The downloadable Insomnia collection below contains all endpoint definitions, parameters, and example requests matching the exact API contracts.

Integration rules

Organization owners call AuthFlow™ from server-rendered code only. Your browser applications should store secrets as server-side environment variables, for calling the AuthFlow™ API. This is because: unlike the majority of authentication services, AuthFlow™ is designed to be used server-to-server, for general NIST-standards compliance.

The organization owner (your) API key is required for core session-management and other routes.

The fooBar token is a short-lived anti-abuse token and should be fetched immediately before each sign-up or login attempt, such that it is always fresh and valid.

If login returns requires2FA--meaning the user has enabled their 2FA--then the user must complete one of the documented /auth/2fa verification exchanges to obtain a session token.

Owner assets

Use the exact collection your implementation team can import into Insomnia.

The collection includes:

7 public /auth session routes
3 public /auth passwordManagement routes
2 public /auth emailVerification routes
8 public /auth/2fa twoFactor routes
Environment variables for API key, role/RBAC, session token, TOTP, temp token, and recovery code flows

Implementation checklist

This is the recommended organization owner workflow for wiring your organization to its authentication via AuthFlow™.

1. Configure server-side secrets
AUTHFLOW_BASE_URL=https://api.authflow.net

Public API origin.

AUTHFLOW_API_KEY=af_<32 lowercase hex chars>

Owner dashboard API key. Stored server-side only.

AUTHFLOW_DEFAULT_ROLE=detective

Role/RBAC value you assign to the auth user.

2. Implement server-side auth

This is the recommended organization owner workflow for wiring your organization to its authentication via AuthFlow™.

Fetch /auth/fooBar
Call /auth/signUp or /auth/login
Store session tokens securely
Use /auth/validate, /auth/me, /auth/changePassword, and /auth/logout from protected routes
3. Connect your frontend

Your browser code should never store AuthFlow™ API secrets, such as the owner API key.

Submit login and signup forms
Handle 2FA prompts when the API reports requires2FA
Store app sessions in secure cookies
Load the current user through the session endpoints' Responses

Session endpoints

These seven routes match authProduct.session from the backend root endpoint.

GET
/auth/fooBar

Generate a short-lived anti-abuse token before sign-up or login.

Parameters

identityqueryrequired

Email address for the auth user.

api_keyqueryrequired

Organization API key from the AuthFlow™ owner dashboard.

rolequeryoptional

RBAC role to encode into the temporary token. Defaults to user.

Response

{
  "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6InVzZXJAZXhhbXBsZS5jb20iLCJvcmdfaWQiOiJvcmctdXVpZCIsInJvbGUiOiJkZXRlY3RpdmUiLCJpYXQiOjE3NzUzNDQ4MDAsImV4cCI6MTc3NTM0NDg0NX0.Rm9vQmFyU2lnbmF0dXJlRXhhbXBsZVN0cmluZ0ZvckF1dGhGbG93"
}

Example

curl --get "$AUTHFLOW_BASE_URL/auth/fooBar" \
  --data-urlencode "identity=user@example.com" \
  --data-urlencode "api_key=$AUTHFLOW_API_KEY" \
  --data-urlencode "role=detective"

Notes

This token expires in 45 seconds.

Fetch a fresh fooBar anti-abuse token immediately before sign-up or login.

GET
/auth/signUp

Create a new authentication user inside the owner organization.

Parameters

foobarqueryrequired

Temporary anti-abuse token returned by /auth/fooBar.

emailqueryrequired

User email address.

passwordqueryrequired

Password matching ^(?:(?=(.*[A-Z]))|(?:.*[a-z]))(?=(.*[0-9]))(?=(.*[!@#$%^&*-])).{8,32}$.

fullnamequeryrequired

Full name for the auth user.

rolequeryrequired

RBAC role for the user inside your application.

api_keyqueryrequired

Organization API key.

disclaimedqueryrequired

Boolean-like value stored with the user record. The collection uses true.

Response

{
  "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJvcmdfaWQiOiJvcmctdXVpZCIsInJvbGUiOiJkZXRlY3RpdmUiLCJpYXQiOjE3NzUzNDQ4MDAsImV4cCI6MTc3NTM4ODAwMH0.U2Vzc2lvblRva2VuU2lnbmF0dXJlRXhhbXBsZVN0cmluZ0ZvckF1dGhGbG93"
}

Example

curl --get "$AUTHFLOW_BASE_URL/auth/signUp" \
  --data-urlencode "foobar=$FOOBAR_TOKEN" \
  --data-urlencode "api_key=$AUTHFLOW_API_KEY" \
  --data-urlencode "role=customer" \
  --data-urlencode "email=user@example.com" \
  --data-urlencode "password=TestPass123!" \
  --data-urlencode "fullname=John Smith" \
  --data-urlencode "disclaimed=true"

Notes

Successful sign-up returns a session token immediately.

If the same email and role already exist in your organization, the API returns a 409 payload.

GET
/auth/login

Authenticate an auth user and return either a session token or a 2FA challenge.

Parameters

foobarqueryrequired

Temporary anti-abuse token returned by /auth/fooBar.

emailqueryrequired

User email address.

passwordqueryrequired

User password.

api_keyqueryrequired

Organization API key.

rolequeryrequired

User role for RBAC login.

Response

{
  "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJvcmdfaWQiOiJvcmctdXVpZCIsInJvbGUiOiJkZXRlY3RpdmUiLCJpYXQiOjE3NzUzNDQ4MDAsImV4cCI6MTc3NTM4ODAwMH0.U2Vzc2lvblRva2VuU2lnbmF0dXJlRXhhbXBsZVN0cmluZ0ZvckF1dGhGbG93"
}
or
{
  "requires2FA": true,
  "tempToken": "d2e6c30aa5f74af4a81f98e8830a6e49e9bb2f31e1a7406a8d2f2eb3ef3028a1",
  "email": "user@example.com",
  "org_id": "org-uuid",
  "role": "detective"
}

Example

curl --get "$AUTHFLOW_BASE_URL/auth/login" \
  --data-urlencode "foobar=$FOOBAR_TOKEN" \
  --data-urlencode "email=user@example.com" \
  --data-urlencode "password=$USER_PASSWORD" \
  --data-urlencode "api_key=$AUTHFLOW_API_KEY" \
  --data-urlencode "role=detective"

Notes

When 2FA is enabled, login returns tempToken instead of token.

tempToken is then exchanged through one of the /auth/2fa verification endpoints.

GET
/auth/validate

Validate a session token for an auth user.

Parameters

sessionTokenqueryrequired

RS512 JWT session token.

emailqueryrequired

User email address.

Response

HTTP 200 when valid
HTTP 401 when invalid or expired

Example

curl --get "$AUTHFLOW_BASE_URL/auth/validate" \
  --data-urlencode "sessionToken=$SESSION_TOKEN" \
  --data-urlencode "email=user@example.com"
GET
/auth/me

Return the current auth-user profile for a valid session.

Parameters

sessionTokenqueryrequired

RS512 JWT session token.

Response

{
  "email": "user@example.com",
  "fullname": "John Smith",
  "role": "detective",
  "org_id": "org-uuid"
}

Example

curl --get "$AUTHFLOW_BASE_URL/auth/me" \
  --data-urlencode "sessionToken=$SESSION_TOKEN"
GET
/auth/logout

Log out the current auth user.

Parameters

sessionTokenqueryrequired

RS512 JWT session token.

emailqueryrequired

User email address.

Response

{
  "success": true
}

Example

curl --get "$AUTHFLOW_BASE_URL/auth/logout" \
  --data-urlencode "sessionToken=$SESSION_TOKEN" \
  --data-urlencode "email=user@example.com"
POST
/auth/deleteUser

Delete a non-owner auth user in your organization (organization owner only).

Parameters

Authorization: Bearer <sessionToken>headerrequired

Session token for the authenticated organization owner.

email_ownerbodyrequired

Owner email. Must match the token identity.

org_idbodyrequired

Organization UUID. Must match the token organization.

user_emailbodyrequired

Email of the user to delete.

Response

{
  "success": true,
  "message": "User deleted successfully"
}
or HTTP 403 when attempting to delete owner or without owner permissions
or HTTP 404 when user does not exist

Example

curl -X POST "$AUTHFLOW_BASE_URL/auth/deleteUser" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -d '{"email_owner":"owner@example.com","org_id":"'$AUTHFLOW_ORG_ID'","user_email":"member@example.com"}'

Notes

This endpoint is owner-only and cannot delete the organization owner account.

The token identity and org_id must match the request body.

Password management endpoints

These three routes match authProduct.passwordManagement from the backend root endpoint.

POST
/auth/forgot-password

Request a password-reset email for an auth user (enumeration-safe response).

Parameters

emailbodyrequired

Auth-user email address.

api_keybodyrequired

Organization API key.

rolebodyoptional

Auth-user role. Defaults to user.

Response

{
  "success": true,
  "message": "If an account exists for this email, a reset link has been sent."
}

Example

curl -X POST "$AUTHFLOW_BASE_URL/auth/forgot-password" \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","api_key":"'$AUTHFLOW_API_KEY'","role":"detective"}'
POST
/auth/reset-password

Reset an auth-user password with a valid reset token.

Parameters

tokenbodyrequired

Password-reset token from the email link.

newPasswordbodyrequired

New password matching ^(?:(?=(.*[A-Z]))|(?:.*[a-z]))(?=(.*[0-9]))(?=(.*[!@#$%^&*-])).{8,32}$.

Response

{
  "success": true
}

Example

curl -X POST "$AUTHFLOW_BASE_URL/auth/reset-password" \
  -H "Content-Type: application/json" \
  -d '{"token":"'$AUTH_RESET_TOKEN'","newPassword":"NewPass123!"}'
GET
/auth/changePassword

Change the authenticated auth-user password.

Parameters

sessionTokenqueryrequired

Current RS512 JWT session token.

emailqueryrequired

Email encoded in the session token.

oldPasswordqueryrequired

Current password for confirmation.

newPasswordqueryrequired

Replacement password matching ^(?:(?=(.*[A-Z]))|(?:.*[a-z]))(?=(.*[0-9]))(?=(.*[!@#$%^&*-])).{8,32}$.

Response

HTTP 200 on success

Example

curl --get "$AUTHFLOW_BASE_URL/auth/changePassword" \
  --data-urlencode "sessionToken=$SESSION_TOKEN" \
  --data-urlencode "email=user@example.com" \
  --data-urlencode "oldPassword=$OLD_PASSWORD" \
  --data-urlencode "newPassword=NewPass123!"

Email verification endpoints

These two routes match authProduct.emailVerification from the backend root endpoint.

GET
/auth/verify-email

Verify an auth-user email using the token delivered by AuthFlow™ email.

Parameters

tokenqueryrequired

Verification token from the email verification link.

Response

HTML verification page
HTTP 200 when verified
HTTP 401 for invalid or expired token

Example

curl --get "$AUTHFLOW_BASE_URL/auth/verify-email" \
  --data-urlencode "token=$VERIFY_EMAIL_TOKEN"

Notes

This endpoint is usually opened by clicking the verification link in email.

AuthFlow™ also supports frontend-hosted verification via /verify-email that proxies this check.

GET
/auth/resend-verify-email

Resend the auth-user verification email when the account is not yet verified.

Parameters

foobarqueryrequired

Temporary token returned by /auth/fooBar.

emailqueryrequired

Auth-user email address.

api_keyqueryrequired

Organization API key.

rolequeryoptional

Auth-user role. Defaults to user.

Response

{
  "statusCode": 200,
  "message": "Verification email sent."
}

Example

curl --get "$AUTHFLOW_BASE_URL/auth/resend-verify-email" \
  --data-urlencode "foobar=$FOOBAR_TOKEN" \
  --data-urlencode "email=user@example.com" \
  --data-urlencode "api_key=$AUTHFLOW_API_KEY" \
  --data-urlencode "role=detective"

Two-factor endpoints

These eight routes match authProduct.twoFactor from the backend root endpoint.

POST
/auth/2fa/enable

Generate a TOTP secret and QR code for an auth user.

Parameters

sessionTokenbodyrequired

Current session token.

emailbodyrequired

Auth-user email.

org_idbodyrequired

Organization UUID.

rolebodyrequired

Auth-user role.

Response

{
  "secret": "BASE32SECRET",
  "qrCode": "data:image/png;base64,...",
  "otpauth_url": "otpauth://totp/..."
}

Example

curl -X POST "$AUTHFLOW_BASE_URL/auth/2fa/enable" \
  -H "Content-Type: application/json" \
  -d '{"sessionToken":"'$SESSION_TOKEN'","email":"user@example.com","org_id":"'$AUTHFLOW_ORG_ID'","role":"detective"}'
POST
/auth/2fa/verify-setup

Verify the initial TOTP code and enable 2FA.

Parameters

sessionTokenbodyrequired

Current session token.

emailbodyrequired

Auth-user email.

org_idbodyrequired

Organization UUID.

rolebodyrequired

Auth-user role.

totpCodebodyrequired

Six-digit code from the authenticator app.

secretbodyrequired

TOTP secret returned by /auth/2fa/enable.

Response

{
  "success": true,
  "recoveryCodes": ["AB12CD34", "..."]
}

Example

curl -X POST "$AUTHFLOW_BASE_URL/auth/2fa/verify-setup" \
  -H "Content-Type: application/json" \
  -d '{"sessionToken":"'$SESSION_TOKEN'","email":"user@example.com","org_id":"'$AUTHFLOW_ORG_ID'","role":"detective","totpCode":"123456","secret":"'$TOTP_SECRET'"}'
POST
/auth/2fa/disable

Disable TOTP 2FA for an auth user.

Parameters

sessionTokenbodyrequired

Current session token.

emailbodyrequired

Auth-user email.

org_idbodyrequired

Organization UUID.

rolebodyrequired

Auth-user role.

passwordbodyrequired

Current password.

Response

{
  "success": true
}

Example

curl -X POST "$AUTHFLOW_BASE_URL/auth/2fa/disable" \
  -H "Content-Type: application/json" \
  -d '{"sessionToken":"'$SESSION_TOKEN'","email":"user@example.com","org_id":"'$AUTHFLOW_ORG_ID'","role":"detective","password":"'$USER_PASSWORD'"}'
POST
/auth/2fa/verify-2fa

Exchange a login tempToken and TOTP code for a session token.

Parameters

tempTokenbodyrequired

tempToken returned by /auth/login when 2FA is enabled.

emailbodyrequired

Auth-user email.

org_idbodyrequired

Organization UUID.

rolebodyrequired

Auth-user role.

totpCodebodyrequired

Six-digit authenticator code.

Response

{
  "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJvcmdfaWQiOiJvcmctdXVpZCIsInJvbGUiOiJkZXRlY3RpdmUiLCJpYXQiOjE3NzUzNDQ4MDAsImV4cCI6MTc3NTM4ODAwMH0.U2Vzc2lvblRva2VuU2lnbmF0dXJlRXhhbXBsZVN0cmluZ0ZvckF1dGhGbG93"
}

Example

curl -X POST "$AUTHFLOW_BASE_URL/auth/2fa/verify-2fa" \
  -H "Content-Type: application/json" \
  -d '{"tempToken":"'$TEMP_TOKEN'","email":"user@example.com","org_id":"'$AUTHFLOW_ORG_ID'","role":"detective","totpCode":"123456"}'
POST
/auth/2fa/backup/email

Send a backup email OTP using a login tempToken.

Parameters

tempTokenbodyrequired

tempToken returned by /auth/login.

emailbodyrequired

Auth-user email.

org_idbodyrequired

Organization UUID.

rolebodyrequired

Auth-user role.

Response

{
  "success": true,
  "message": "OTP sent to your email"
}

Example

curl -X POST "$AUTHFLOW_BASE_URL/auth/2fa/backup/email" \
  -H "Content-Type: application/json" \
  -d '{"tempToken":"'$TEMP_TOKEN'","email":"user@example.com","org_id":"'$AUTHFLOW_ORG_ID'","role":"detective"}'
POST
/auth/2fa/verify-email-otp

Exchange an emailed OTP for a session token.

Parameters

emailbodyrequired

Auth-user email.

org_idbodyrequired

Organization UUID.

rolebodyrequired

Auth-user role.

emailOTPbodyrequired

Six-digit code delivered by email.

Response

{
  "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJvcmdfaWQiOiJvcmctdXVpZCIsInJvbGUiOiJkZXRlY3RpdmUiLCJpYXQiOjE3NzUzNDQ4MDAsImV4cCI6MTc3NTM4ODAwMH0.U2Vzc2lvblRva2VuU2lnbmF0dXJlRXhhbXBsZVN0cmluZ0ZvckF1dGhGbG93"
}

Example

curl -X POST "$AUTHFLOW_BASE_URL/auth/2fa/verify-email-otp" \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","org_id":"'$AUTHFLOW_ORG_ID'","role":"detective","emailOTP":"123456"}'
POST
/auth/2fa/recovery-codes

Regenerate recovery codes for an auth user.

Parameters

sessionTokenbodyrequired

Current session token.

emailbodyrequired

Auth-user email.

org_idbodyrequired

Organization UUID.

rolebodyrequired

Auth-user role.

passwordbodyrequired

Current password.

Response

{
  "recoveryCodes": ["AB12CD34", "..."]
}

Example

curl -X POST "$AUTHFLOW_BASE_URL/auth/2fa/recovery-codes" \
  -H "Content-Type: application/json" \
  -d '{"sessionToken":"'$SESSION_TOKEN'","email":"user@example.com","org_id":"'$AUTHFLOW_ORG_ID'","role":"detective","password":"'$USER_PASSWORD'"}'
POST
/auth/2fa/verify-recovery

Exchange a recovery code for a session token.

Parameters

emailbodyrequired

Auth-user email.

org_idbodyrequired

Organization UUID.

rolebodyrequired

Auth-user role.

recoveryCodebodyrequired

One of the 8-character recovery codes previously returned.

Response

{
  "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJvcmdfaWQiOiJvcmctdXVpZCIsInJvbGUiOiJkZXRlY3RpdmUiLCJpYXQiOjE3NzUzNDQ4MDAsImV4cCI6MTc3NTM4ODAwMH0.U2Vzc2lvblRva2VuU2lnbmF0dXJlRXhhbXBsZVN0cmluZ0ZvckF1dGhGbG93",
  "remainingCodes": 9
}

Example

curl -X POST "$AUTHFLOW_BASE_URL/auth/2fa/verify-recovery" \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","org_id":"'$AUTHFLOW_ORG_ID'","role":"detective","recoveryCode":"AB12CD34"}'
Service example
// lib/auth.ts using fetch with React statefulness
  
import { UserRole } from './types'
import { updateState } from './store'

// Cookie helpers — use Secure + SameSite=Strict for security.

function setCookie(name: string, value: string, days = 7): void {
  if (typeof document === 'undefined') return
  const expires = new Date(Date.now() + days * 864e5).toUTCString()
  document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Strict; Secure`
}

function getCookie(name: string): string | null {
  if (typeof document === 'undefined') return null
  const match = document.cookie.match(new RegExp('(?:^|;\s*)' + name + '=([^;]*)'))
  return match ? decodeURIComponent(match[1]) : null
}

function deleteCookie(name: string): void {
  if (typeof document === 'undefined') return
  const isLocalhost = window.location.hostname === 'localhost'
  document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Strict; Secure`
}

export interface AuthUser {
  email: string
  fullname: string
  org_id: string
  role: UserRole
  token: string
}

export interface AuthState {
  user: AuthUser | null
  isLoading: boolean
  isAuthenticated: boolean
}

class AuthFlowClient {
  private baseUrl: string
  private apiKey: string

  constructor() {
    const isLocalhost = typeof window !== 'undefined' && window.location.hostname === 'localhost'
    this.baseUrl = process.env.AUTHFLOW_API_URL
    this.apiKey = process.env.AUTHFLOW_API_KEY
  }

  private async makeRequest(endpoint: string, params: Record<string, string>): Promise<{ data: any; status: number }> {
    const url = new URL(`${this.baseUrl}${endpoint}`)
    Object.entries(params).forEach(([key, value]) => {
      url.searchParams.append(key, value)
    })

    const response = await fetch(url.toString())
    
    let data: any
    const contentType = response.headers.get('content-type')
    
    if (contentType && contentType.includes('application/json')) {
      data = await response.json()
    } else {
      // Handle non-JSON responses (like plain text error messages)
      const text = await response.text()
      data = { message: text, status: response.status }
    }

    return { data, status: response.status }
  }

  async getFooBarToken(email: string, role?: string): Promise<string> {
    const { data: response } = await this.makeRequest('/auth/fooBar', { identity: email, api_key: this.apiKey, ...(role && { role }) })
    return response.token
  }

  async signUp(email: string, password: string, fullname: string, role?: string): Promise<AuthUser> {
    const foobar = await this.getFooBarToken(email, role)

    const { data: response, status } = await this.makeRequest('/auth/signUp', {
      foobar,
      api_key: this.apiKey,
      email,
      password,
      fullname,
      disclaimed: 'true',
      ...(role && { role })
    })

    // Handle different error status codes
    if (status === 400) {
      if (response.message?.includes('Password does not meet complexity')) {
        throw new Error('Password must be at least 8 characters long and contain uppercase, lowercase, and numbers')
      } else if (response.message?.includes('Missing required fields')) {
        throw new Error('Please fill in all required fields')
      } else {
        throw new Error('Invalid signup information. Please check your details and try again.')
      }
    } else if (status === 401) {
      if (response.message?.includes('Invalid or expired token')) {
        throw new Error('Signup session expired. Please refresh the page and try again.')
      } else if (response.message?.includes('Invalid api_key')) {
        throw new Error('Service temporarily unavailable. Please try again later.')
      } else if (response.message?.includes('email verification')) {
        throw new Error('Email verification is required. Please check your email for verification instructions.')
      } else {
        throw new Error('Unable to create account. Please try again later.')
      }
    } else if (status === 409) {
      throw new Error('An account with this email and role already exists. Please sign in instead or choose a different role.')
    } else if (status === 500) {
      throw new Error('Server error. Please try again later.')
    } else if (status !== 200) {
      throw new Error('Unable to create account. Please try again.')
    }

    if (role === 'admin') {
      const adminUserData = {
        email: response?.email || email,
        fullname: response?.fullname || fullname,
        org_id: response?.org_id || '',
        role: 'admin' as UserRole,
        token: '',
      }
      // Update the app store with the admin user data
      updateState({ user: adminUserData })
      return adminUserData
    }

    // For non-admin roles, login after signup to get the token
    return this.signIn(email, password, role)
  }

  async signIn(email: string, password: string, role?: string): Promise<AuthUser> {
    const foobar = await this.getFooBarToken(email, role)

    const { data: response, status } = await this.makeRequest('/auth/login', {
      foobar,
      email,
      password,
      api_key: this.apiKey,
      ...(role && { role })
    })

    // Handle different error status codes
    if (status === 400) {
      throw new Error('Please provide both email and password')
    } else if (status === 401) {
      if (response.message?.includes('Incorrect password')) {
        throw new Error('Incorrect password. Please check your password and try again.')
      } else if (response.message?.includes('Invalid api_key')) {
        throw new Error('Service temporarily unavailable. Please try again later.')
      } else {
        throw new Error('Invalid login credentials. Please check your email and password.')
      }
    } else if (status === 404) {
      throw new Error('There is no user with that signin info, please use the signup link.')
    } else if (status === 500) {
      throw new Error('Server error. Please try again later.')
    } else if (status !== 200) {
      throw new Error('Unable to sign in. Please try again.')
    }

    if (response.requires2FA) {
      // TODO: 2FA implementation - this is a placeholder to indicate where 2FA logic would go
      throw new Error('2FA required - not implemented yet')
    }

    // Get user details using /auth/me
    const { data: userResponse, status: meStatus } = await this.makeRequest('/auth/me', {
      sessionToken: response.token
    })

    if (meStatus !== 200) {
      if (meStatus === 401) {
        throw new Error('Session expired. Please sign in again.')
      } else {
        throw new Error('Unable to retrieve user information. Please try again.')
      }
    }

    // Ensure we have valid user data
    if (!userResponse || typeof userResponse !== 'object' || !userResponse.email) {
      throw new Error('Invalid user data received. Please try again.')
    }

    const userData = {
      email: userResponse.email,
      fullname: userResponse.fullname,
      org_id: userResponse.org_id,
      role: userResponse.role as UserRole,
      token: response.token
    }

    // Update the app store with the authenticated user data
    updateState({ user: userData })

    return userData
  }

  async signOut(token: string, email: string): Promise<void> {
    await this.makeRequest('/auth/logout', {
      sessionToken: token,
      email
    })
    // Clear the user data from the app store
    updateState({ user: null })
  }

  async validateToken(token: string, email: string): Promise<boolean> {
    try {
      await this.makeRequest('/auth/validate', {
        sessionToken: token,
        email
      })
      return true
    } catch {
      return false
    }
  }

  async changePassword(sessionToken: string, email: string, oldPassword: string, newPassword: string): Promise<void> {
    const { status } = await this.makeRequest('/auth/changePassword', {
      sessionToken,
      email,
      oldPassword,
      newPassword
    })

    // Handle different error status codes
    if (status === 400) {
      if (newPassword.length < 8 || !/(?=.*[a-z])(?=.*[A-Z])(?=.*d)/.test(newPassword)) {
        throw new Error('Password must be at least 8 characters long and contain uppercase, lowercase, and numbers')
      } else {
        throw new Error('Missing required fields. Please provide email, old password, and new password.')
      }
    } else if (status === 401) {
      throw new Error('Invalid session or incorrect old password. Please check your credentials and try again.')
    } else if (status === 404) {
      throw new Error('User not found. Please check your email address.')
    } else if (status === 500) {
      throw new Error('Server error. Please try again later.')
    } else if (status !== 200) {
      throw new Error('Unable to change password. Please try again.')
    }
  }
}

export const authFlowClient = new AuthFlowClient()

// Auth state management
let authState: AuthState = {
  user: null,
  isLoading: false,
  isAuthenticated: false
}

const listeners: ((state: AuthState) => void)[] = []

export function subscribeToAuthState(listener: (state: AuthState) => void) {
  listeners.push(listener)
  return () => {
    const index = listeners.indexOf(listener)
    if (index > -1) {
      listeners.splice(index, 1)
    }
  }
}

function notifyListeners() {
  listeners.forEach(listener => listener(authState))
}

export function getAuthState(): AuthState {
  return { ...authState }
}

export async function signUp(email: string, password: string, fullname: string, role?: string): Promise<AuthUser> {
  authState.isLoading = true
  notifyListeners()

  try {
    const user = await authFlowClient.signUp(email, password, fullname, role)
    authState.user = user.token ? user : null
    authState.isAuthenticated = Boolean(user.token)
    authState.isLoading = false
    notifyListeners()

    if (user.token) {
      setCookie('auth_user', JSON.stringify(user))
    }

    return user
  } catch (error) {
    authState.isLoading = false
    notifyListeners()
    throw error
  }
}

export async function signIn(email: string, password: string, role?: string): Promise<AuthUser> {
  authState.isLoading = true
  notifyListeners()

  try {
    const user = await authFlowClient.signIn(email, password, role)
    authState.user = user
    authState.isAuthenticated = true
    authState.isLoading = false
    notifyListeners()

    // Store in secure cookie
    setCookie('auth_user', JSON.stringify(user))

    return user
  } catch (error) {
    authState.isLoading = false
    notifyListeners()
    throw error
  }
}

export async function signOut(): Promise<void> {
  if (authState.user) {
    try {
      await authFlowClient.signOut(authState.user.token, authState.user.email)
    } catch (error) {
      console.error('Error signing out:', error)
    }
  }

  authState.user = null
  authState.isAuthenticated = false
  authState.isLoading = false
  notifyListeners()

  // Clear auth cookie
  deleteCookie('auth_user')
}

export async function initializeAuth(): Promise<void> {
  const stored = getCookie('auth_user')
  if (stored) {
    try {
      const user = JSON.parse(stored)
      // Validate token
      const isValid = await authFlowClient.validateToken(user.token, user.email)
      if (isValid) {
        authState.user = user
        authState.isAuthenticated = true
        notifyListeners()
        // Update the app store with the restored user data
        updateState({ user })
      } else {
        deleteCookie('auth_user')
        // Clear the user data from the app store
        updateState({ user: null })
      }
    } catch (error) {
      console.error('Error initializing auth:', error)
      deleteCookie('auth_user')
      // Clear the user data from the app store
      updateState({ user: null })
    }
  }
}
Security requirements

Keep AUTHFLOW_API_KEY server-side and encrypted.

Prefer HTTP-only secure cookies for your application session.

Validate AuthFlow™ sessions before serving protected user data.

Handle 400, 401, 404, 409, and 500 responses explicitly in your integration.

Ready to wire your company authentication?

Start with the downloadable collection, implement secure integration of the service with safely-stored secrets, and use the assistant on this page if you need guidance.

ExpressJS AuthFlowTM Proxy

You may also choose to proxy the AuthFlow backend, from your own ExpressJS backend. This is arguably the most-secure way to go about consuming AuthFlowTM in your application stacks.

The proxy forwards all AuthFlow /auth/* and /auth/2fa/* routes, keeps your organization API key server-side, and helps prevent secret exposure in browser code.

The proxy is Vercel-ready, supports local development immediately, and includes practical examples for sign-up, login, password flows, and 2FA verification.

AuthFlow™ Assistant

I guide organization owners through the exact AuthFlow™ auth-user contract. Ask me about owner API key setup, /auth endpoints, 2FA flows, the Insomnia collection, frontend-backend boundaries, or Settings-page account deletion.