Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Expensify/App/llms.txt

Use this file to discover all available pages before exploring further.

Overview

New Expensify uses token-based authentication with automatic session management and reauthentication.

Authentication Flow

Session Management

Session Storage

Session data is stored in Onyx:
import ONYXKEYS from '@src/ONYXKEYS';

// Session structure
type Session = {
  authToken: string;      // Authentication token
  accountID: number;      // User's account ID
  email: string;          // User's email
  encryptedAuthToken?: string; // For sensitive operations
};

// Stored at
ONYXKEYS.SESSION

Getting Session Data

import {useOnyx} from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';

function MyComponent() {
  const [session] = useOnyx(ONYXKEYS.SESSION);
  
  if (!session?.authToken) {
    return <LoginScreen />;
  }
  
  return <AppContent accountID={session.accountID} />;
}

Sign In

Basic Sign In

import * as Session from '@libs/actions/Session';

function signIn(email: string, password: string) {
  Session.signIn(email, password);
}

Sign In Implementation

From src/libs/actions/Session.ts:
function signIn(email: string, password: string) {
  const optimisticData: OnyxUpdate[] = [
    {
      onyxMethod: Onyx.METHOD.MERGE,
      key: ONYXKEYS.ACCOUNT,
      value: {
        isLoading: true,
      },
    },
  ];

  const successData: OnyxUpdate[] = [
    {
      onyxMethod: Onyx.METHOD.MERGE,
      key: ONYXKEYS.ACCOUNT,
      value: {
        isLoading: false,
      },
    },
  ];

  const failureData: OnyxUpdate[] = [
    {
      onyxMethod: Onyx.METHOD.MERGE,
      key: ONYXKEYS.ACCOUNT,
      value: {
        isLoading: false,
        errors: {
          [Date.now()]: 'Failed to sign in',
        },
      },
    },
  ];

  API.write(
    'SignIn',
    {email, password},
    {optimisticData, successData, failureData},
  );
}

Short-Lived Auth Token

For magic links and deep links:
function signInWithShortLivedAuthToken(shortLivedAuthToken: string) {
  API.read(
    'SignInWithShortLivedAuthToken',
    {authToken: shortLivedAuthToken},
  );
}

Automatic Reauthentication

How It Works

When the server returns jsonCode: 407 (expired auth token):
  1. Reauthentication middleware intercepts the response
  2. Automatically calls Reauthenticate API
  3. Gets new authToken
  4. Retries the original request
  5. User never sees an error

Reauthentication Middleware

From src/libs/Middleware/Reauthentication.ts:
function Reauthentication(response: Response, request: Request): Promise<Response> {
  if (response?.jsonCode !== 407) {
    return Promise.resolve(response);
  }

  // Get stored credentials
  const credentials = getCredentials();
  
  if (!credentials) {
    // No credentials, redirect to login
    redirectToSignIn();
    return Promise.reject(new Error('Unable to reauthenticate'));
  }

  // Call Reauthenticate API
  return API.write('Reauthenticate', credentials)
    .then(() => {
      // Retry original request
      return retryRequest(request);
    });
}

Credentials Storage

Credentials are stored separately from session:
import ONYXKEYS from '@src/ONYXKEYS';

type Credentials = {
  login: string;      // Email or phone
  password: string;   // Encrypted password
  autoGeneratedLogin?: string;
  autoGeneratedPassword?: string;
};

// Stored at
ONYXKEYS.CREDENTIALS

Sign Out

Basic Sign Out

import * as Session from '@libs/actions/Session';

function handleSignOut() {
  Session.signOut();
}

Sign Out Implementation

function signOut() {
  // Clear session first (optimistic)
  Onyx.set(ONYXKEYS.SESSION, null);
  Onyx.set(ONYXKEYS.CREDENTIALS, null);
  
  // Notify server
  API.write('SignOut', {});
  
  // Navigate to login
  Navigation.navigate(ROUTES.HOME);
}

Two-Factor Authentication (2FA)

Enabling 2FA

function enable2FA() {
  API.write('Account_TwoFactorAuthGenerate', {});
}

Validating 2FA Code

function validate2FACode(twoFactorAuthCode: string) {
  API.write('Account_TwoFactorAuthValidate', {twoFactorAuthCode});
}

Sign In with 2FA

When 2FA is enabled, sign-in is a two-step process:
// Step 1: Initial sign in
function signInWith2FA(email: string, password: string) {
  API.write('SignIn', {email, password});
  // Server returns requiresTwoFactorAuth: true
}

// Step 2: Validate 2FA code
function validate2FASignIn(twoFactorAuthCode: string) {
  API.write('SignIn_Validate2FA', {twoFactorAuthCode});
  // Server returns authToken on success
}

Magic Code / OTP

Request Magic Code

function requestMagicCode(email: string) {
  API.write('RequestMagicCode', {email});
  // Code sent via email
}

Sign In with Magic Code

function signInWithMagicCode(email: string, magicCode: string) {
  API.write('SignInWithMagicCode', {email, magicCode});
}

Auth Token Management

Including Auth Token in Requests

Auth token is automatically included in all requests:
// Handled automatically by API client
const headers = {
  'Authorization': `Bearer ${session.authToken}`,
};

Token Expiration

Auth tokens expire after a period of inactivity:
  • Expiration: ~30 days of inactivity
  • Handling: Automatic reauthentication
  • User Impact: Seamless, no action required

Encrypted Auth Token

For sensitive operations (payments, bank accounts):
type Session = {
  authToken: string;           // Regular token
  encryptedAuthToken?: string; // For sensitive ops
};

// Automatically used for payment-related APIs

Account Validation

Email Validation

New accounts must validate their email:
function validateEmail(validateCode: string) {
  API.write('ValidateEmail', {validateCode});
}

Adding New Login Method

function addNewContactMethod(email: string) {
  API.write('AddNewContactMethod', {partnerUserID: email});
  // Validation email sent
}

function validateNewContactMethod(validateCode: string) {
  API.write('ValidateContactMethod', {validateCode});
}

Handling Deleted Accounts

When an account is deleted (jsonCode 408):
// Middleware handles this automatically
function handleDeletedAccount(response: Response): Promise<Response> {
  if (response?.jsonCode !== 408) {
    return Promise.resolve(response);
  }

  // Clear session
  Onyx.set(ONYXKEYS.SESSION, null);
  
  // Show message
  showAlertMessage('Account has been deleted');
  
  // Redirect to login
  Navigation.navigate(ROUTES.HOME);
  
  return Promise.reject(new Error('Account deleted'));
}

Testing Authentication

Mock Session

import Onyx from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';

beforeEach(async () => {
  await Onyx.merge(ONYXKEYS.SESSION, {
    authToken: 'test-auth-token',
    accountID: 1,
    email: 'test@example.com',
  });
});

test('requires authentication', () => {
  const {getByText} = render(<ProtectedComponent />);
  expect(getByText('Protected Content')).toBeTruthy();
});

Mock Sign In

import * as Session from '@libs/actions/Session';

jest.spyOn(Session, 'signIn').mockImplementation(() => {
  Onyx.merge(ONYXKEYS.SESSION, {
    authToken: 'mock-token',
    accountID: 1,
    email: 'test@example.com',
  });
});

Authentication Best Practices

1. Check Auth State

// ✅ Good: Check auth before protected actions
function MyComponent() {
  const [session] = useOnyx(ONYXKEYS.SESSION);
  
  if (!session?.authToken) {
    return <SignInPrompt />;
  }
  
  return <ProtectedContent />;
}

2. Handle Unauthenticated Users

import interceptAnonymousUser from '@libs/interceptAnonymousUser';

function handleProtectedAction() {
  interceptAnonymousUser(() => {
    // This only runs if user is authenticated
    performProtectedAction();
  });
  // Redirects to login if not authenticated
}

3. Never Store Passwords

// ❌ Bad: Storing plain text password
Onyx.merge(ONYXKEYS.CREDENTIALS, {
  password: 'user-password', // Don't do this
});

// ✅ Good: Let Session actions handle credentials
Session.signIn(email, password);
// Password is encrypted before storage

4. Trust Automatic Reauthentication

// ❌ Bad: Manual token refresh
if (isTokenExpired(authToken)) {
  refreshToken();
}

// ✅ Good: Let middleware handle it
API.write('SomeCommand', params);
// Middleware automatically reauthenticates if needed

Security Considerations

Secure Storage

  • Auth tokens stored in secure storage on native platforms
  • Web uses httpOnly cookies when possible
  • Credentials are encrypted before storage

HTTPS Only

All API calls use HTTPS:
const EXPENSIFY_URL = 'https://www.expensify.com';
const SECURE_EXPENSIFY_URL = 'https://www.expensify.com/api';

Token Rotation

Tokens are rotated on:
  • Sign in
  • Reauthentication
  • Password change
  • Security events

Troubleshooting

Stuck at Login

// Check session state
Onyx.connect({
  key: ONYXKEYS.SESSION,
  callback: (session) => {
    console.log('Session:', session);
  },
});

Reauthentication Loop

// Check credentials
Onyx.connect({
  key: ONYXKEYS.CREDENTIALS,
  callback: (credentials) => {
    console.log('Credentials:', credentials);
    // If null, user needs to sign in again
  },
});

407 Errors

  • Check that credentials are stored
  • Verify password hasn’t changed
  • Clear app data and sign in again

Next Steps

API Overview

Learn API fundamentals

API Endpoints

Explore available endpoints

State Management

Understand session in Onyx

Testing

Test authenticated flows