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