Mobile App Authentication: JWT vs OAuth vs Custom Solutions

Compare different authentication approaches for mobile applications, understand their trade-offs, and choose the right solution for your mobile app.

Mobile Authentication Challenges

Mobile app authentication presents unique challenges compared to web applications. Users expect seamless experiences, but security requirements are just as critical. The choice of authentication method can significantly impact user experience, security, and development complexity.

Key Considerations for Mobile Auth

  • Offline Support: Apps need to work without internet connectivity
  • Biometric Integration: Fingerprint and face recognition support
  • Secure Storage: Protecting tokens and credentials on device
  • Session Management: Handling token refresh and expiration
  • Cross-Platform: Consistent experience across iOS and Android
  • User Experience: Minimal friction while maintaining security

JWT-Based Authentication

How JWT Works in Mobile Apps

JWT (JSON Web Tokens) are self-contained tokens that include user information and permissions. They're ideal for mobile apps due to their stateless nature and offline capabilities.

JWT Implementation (React Native)

// JWT Authentication Service
import AsyncStorage from '@react-native-async-storage/async-storage';
import jwtDecode from 'jwt-decode';

class AuthService {
  constructor() {
    this.baseURL = 'https://api.yourapp.com';
  }

  async login(email, password) {
    try {
      const response = await fetch(`${this.baseURL}/auth/login`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email, password }),
      });

      const data = await response.json();
      
      if (data.token) {
        await this.storeToken(data.token);
        return { success: true, user: data.user };
      } else {
        return { success: false, error: data.message };
      }
    } catch (error) {
      return { success: false, error: 'Network error' };
    }
  }

  async storeToken(token) {
    try {
      await AsyncStorage.setItem('authToken', token);
    } catch (error) {
      console.error('Error storing token:', error);
    }
  }

  async getToken() {
    try {
      return await AsyncStorage.getItem('authToken');
    } catch (error) {
      console.error('Error getting token:', error);
      return null;
    }
  }

  async isTokenValid() {
    const token = await this.getToken();
    if (!token) return false;

    try {
      const decoded = jwtDecode(token);
      const currentTime = Date.now() / 1000;
      return decoded.exp > currentTime;
    } catch (error) {
      return false;
    }
  }

  async logout() {
    try {
      await AsyncStorage.removeItem('authToken');
    } catch (error) {
      console.error('Error during logout:', error);
    }
  }
}

export default new AuthService();

JWT Pros and Cons

Pros:

  • Stateless - no server-side session storage
  • Self-contained - includes user information
  • Offline capable - works without internet
  • Cross-platform compatible
  • Scalable for microservices

Cons:

  • Cannot be revoked before expiration
  • Larger than session IDs
  • Security depends on proper implementation
  • Token refresh complexity

OAuth 2.0 & OpenID Connect

OAuth 2.0 for Mobile Apps

OAuth 2.0 provides a secure way for mobile apps to access user data from third-party services without sharing passwords.

OAuth Implementation (React Native)

// OAuth 2.0 with Google Sign-In
import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { LoginManager, AccessToken } from 'react-native-fbsdk-next';

class OAuthService {
  constructor() {
    this.configureGoogle();
  }

  configureGoogle() {
    GoogleSignin.configure({
      webClientId: 'your-web-client-id',
      offlineAccess: true,
    });
  }

  async signInWithGoogle() {
    try {
      await GoogleSignin.hasPlayServices();
      const userInfo = await GoogleSignin.signIn();
      
      // Exchange Google token for your app's JWT
      const response = await fetch('https://api.yourapp.com/auth/google', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          idToken: userInfo.idToken,
          accessToken: userInfo.accessToken,
        }),
      });

      const data = await response.json();
      return { success: true, token: data.token, user: data.user };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  async signInWithFacebook() {
    try {
      const result = await LoginManager.logInWithPermissions(['public_profile', 'email']);
      
      if (result.isCancelled) {
        return { success: false, error: 'User cancelled' };
      }

      const data = await AccessToken.getCurrentAccessToken();
      if (!data) {
        return { success: false, error: 'Something went wrong' };
      }

      // Exchange Facebook token for your app's JWT
      const response = await fetch('https://api.yourapp.com/auth/facebook', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          accessToken: data.accessToken,
        }),
      });

      const authData = await response.json();
      return { success: true, token: authData.token, user: authData.user };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
}

export default new OAuthService();

Biometric Authentication

Implementing Biometric Auth

// Biometric Authentication (React Native)
import TouchID from 'react-native-touch-id';
import { BiometricAuth } from 'react-native-biometrics';

class BiometricService {
  async isBiometricAvailable() {
    try {
      const biometryType = await TouchID.isSupported();
      return biometryType !== false;
    } catch (error) {
      return false;
    }
  }

  async authenticateWithBiometrics() {
    try {
      const config = {
        title: 'Authenticate',
        subTitle: 'Use your fingerprint to authenticate',
        description: 'Place your finger on the sensor',
        fallbackLabel: 'Use Passcode',
      };

      const result = await TouchID.authenticate('Authenticate', config);
      return { success: true };
    } catch (error) {
      return { 
        success: false, 
        error: error.message,
        code: error.code 
      };
    }
  }

  async storeBiometricCredentials(credentials) {
    try {
      const { BiometricAuth } = require('react-native-biometrics');
      const rnBiometrics = new BiometricAuth();
      
      const { success } = await rnBiometrics.createKeys();
      if (success) {
        const { signature } = await rnBiometrics.createSignature({
          promptMessage: 'Authenticate to store credentials',
          payload: credentials,
        });
        
        // Store encrypted credentials
        await this.encryptAndStore(credentials, signature);
        return { success: true };
      }
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  async retrieveBiometricCredentials() {
    try {
      const { BiometricAuth } = require('react-native-biometrics');
      const rnBiometrics = new BiometricAuth();
      
      const { success, signature } = await rnBiometrics.createSignature({
        promptMessage: 'Authenticate to retrieve credentials',
        payload: 'retrieve_credentials',
      });
      
      if (success) {
        const credentials = await this.decryptAndRetrieve(signature);
        return { success: true, credentials };
      }
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
}

export default new BiometricService();

Secure Token Storage

iOS Keychain Storage

// iOS Keychain (React Native)
import Keychain from 'react-native-keychain';

class SecureStorage {
  async storeToken(token) {
    try {
      await Keychain.setInternetCredentials(
        'yourapp.com',
        'authToken',
        token,
        {
          accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY,
          authenticationType: Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS,
        }
      );
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  async getToken() {
    try {
      const credentials = await Keychain.getInternetCredentials('yourapp.com');
      if (credentials) {
        return { success: true, token: credentials.password };
      }
      return { success: false, error: 'No token found' };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  async deleteToken() {
    try {
      await Keychain.resetInternetCredentials('yourapp.com');
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
}

export default new SecureStorage();

Android Keystore

// Android Keystore (React Native)
import { getItem, setItem, removeItem } from 'react-native-keychain';

class AndroidSecureStorage {
  async storeToken(token) {
    try {
      await setItem('authToken', token, {
        accessControl: 'BiometryAny',
        authenticationPrompt: 'Authenticate to access your data',
      });
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  async getToken() {
    try {
      const token = await getItem('authToken');
      if (token) {
        return { success: true, token };
      }
      return { success: false, error: 'No token found' };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
}

export default new AndroidSecureStorage();

Token Refresh Strategy

Automatic Token Refresh

// Token Refresh Service
class TokenRefreshService {
  constructor() {
    this.refreshPromise = null;
  }

  async refreshToken() {
    // Prevent multiple simultaneous refresh attempts
    if (this.refreshPromise) {
      return this.refreshPromise;
    }

    this.refreshPromise = this.performRefresh();
    const result = await this.refreshPromise;
    this.refreshPromise = null;
    return result;
  }

  async performRefresh() {
    try {
      const refreshToken = await SecureStorage.getRefreshToken();
      if (!refreshToken) {
        throw new Error('No refresh token available');
      }

      const response = await fetch('https://api.yourapp.com/auth/refresh', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ refreshToken }),
      });

      const data = await response.json();
      
      if (data.token) {
        await SecureStorage.storeToken(data.token);
        if (data.refreshToken) {
          await SecureStorage.storeRefreshToken(data.refreshToken);
        }
        return { success: true, token: data.token };
      } else {
        throw new Error('Invalid refresh response');
      }
    } catch (error) {
      // Refresh failed, user needs to login again
      await this.logout();
      return { success: false, error: error.message };
    }
  }

  async logout() {
    await SecureStorage.deleteToken();
    await SecureStorage.deleteRefreshToken();
    // Navigate to login screen
  }
}

export default new TokenRefreshService();

Custom Authentication Solutions

When to Build Custom Auth

  • Unique business requirements
  • Specific security needs
  • Integration with legacy systems
  • Full control over user experience

Custom Auth Implementation

// Custom Authentication with PIN
class CustomAuthService {
  async authenticateWithPIN(pin) {
    try {
      const hashedPIN = await this.hashPIN(pin);
      const storedPIN = await SecureStorage.getHashedPIN();
      
      if (hashedPIN === storedPIN) {
        const token = await this.generateCustomToken();
        await SecureStorage.storeToken(token);
        return { success: true, token };
      } else {
        return { success: false, error: 'Invalid PIN' };
      }
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  async setupPIN(pin) {
    try {
      const hashedPIN = await this.hashPIN(pin);
      await SecureStorage.storeHashedPIN(hashedPIN);
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  async hashPIN(pin) {
    // Use a secure hashing algorithm
    const crypto = require('crypto');
    const salt = await SecureStorage.getSalt() || crypto.randomBytes(32);
    const hash = crypto.pbkdf2Sync(pin, salt, 10000, 64, 'sha512');
    return hash.toString('hex');
  }
}

export default new CustomAuthService();

Choosing the Right Solution

Decision Matrix

Solution Best For Pros Cons
JWT Simple apps, offline support Stateless, self-contained Cannot revoke, larger size
OAuth 2.0 Social login, third-party integration Industry standard, secure Complex implementation
Biometric High security, convenience User-friendly, secure Device dependent
Custom Unique requirements Full control, tailored More development time

Security Best Practices

  • Use HTTPS: Always encrypt communication
  • Secure Storage: Use Keychain/Keystore for sensitive data
  • Token Expiration: Implement short-lived access tokens
  • Certificate Pinning: Prevent man-in-the-middle attacks
  • Input Validation: Validate all user inputs
  • Error Handling: Don't expose sensitive information
  • Regular Updates: Keep authentication libraries updated

Test Your Authentication

Use our JWT verification tool and OIDC playground to test and validate your mobile authentication implementations.

Use JWT Verifier Use OIDC Playground