Skip to main content

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: '[email protected]',
  });
});

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: '[email protected]',
  });
});

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

Build docs developers (and LLMs) love