I am building my first React Native/Expo app and have implemented a custom JWT implementation on my PHP server. The JWTs work as expected on iOS, but they fail on Android. This is my code so far:
import axios from 'axios';
import * as SecureStore from 'expo-secure-store';
const API_BASE_URL = 'https://serverdomain.url/backend';
// Add a request interceptor to log the actual URL being requested
axios.interceptors.request.use(request => {
console.log(`Intercepted Request: ${request.method.toUpperCase()} ${request.url}`);
if (request.data) {
console.log(`Payload: ${JSON.stringify(request.data)}`);
}
return request;
}, error => {
console.error('Request error:', error);
return Promise.reject(error);
});
// Function to store and retrieve tokens securely
export async function setAccessToken(token) {
await SecureStore.setItemAsync('access_token', token);
}
export async function getAccessToken() {
return await SecureStore.getItemAsync('access_token');
}
export async function clearAccessToken() {
await SecureStore.deleteItemAsync('access_token');
}
// API request wrapper with automatic token handling
export async function apiRequest(endpoint, method = 'GET', data = null) {
const token = await getAccessToken();
const url = `${API_BASE_URL}/${endpoint}`;
try {
const response = await axios({
url,
method,
data,
headers: token ? { Authorization: `Bearer ${token}` } : {},
withCredentials: true, // Ensures HTTP-only cookies are included
});
return response.data;
} catch (error) {
console.error(`Error requesting: ${method} ${url}`, error);
if (error.response && error.response.status === 401) {
// Attempt to refresh the token
const newToken = await refreshAccessToken();
if (newToken) {
return apiRequest(endpoint, method, data);
}
}
throw error;
}
}
// Refresh the access token
export async function refreshAccessToken() {
try {
const response = await axios.post(`${API_BASE_URL}/refresh.php`, {}, { withCredentials: true });
const newToken = response.data.access_token;
await setAccessToken(newToken);
return newToken;
} catch (error) {
await clearAccessToken();
return null;
}
}
// Fetch logged-in user's data
export async function fetchUserData() {
console.log(`${API_BASE_URL}/api/user.php`);
const token = await getAccessToken();
console.log("Retrieved token:", token);
try {
const data = await apiRequest('api/user.php?id=1', 'POST', null);
console.log("User data response:", data);
return data;
} catch (error) {
console.error("Error fetching user data:", error.response ? error.response.data : error.message);
return null;
}
}
When I try the fetchUserData()
function, it seems like the token is not stored on Android so the app tries to refresh it, which fails, prompting the app to refresh the token again, and then it enters into a loop trying to refresh the tokens.
I appreciate it if you please look into this issue and guide me so I can find a solution. Thanks a lot.
I already tried switching from Axios to Fetch, but the problem persists on Android. I've also tried using AsyncStorage but it didn't fix it.
This is the logcat:
2025-02-12 10:55:36.806 1663-1676 GnssLocationProvider system_server I WakeLock released by handleMessage(REPORT_SV_STATUS, 0, com.android.server.location.GnssLocationProvider$SvStatusInfo@6a9178e)
2025-02-12 10:55:36.903 32421-3100 ReactNativeJS host.exp.exponent I Requesting new token
2025-02-12 10:55:37.014 32421-3100 ReactNativeJS host.exp.exponent I Setting token
2025-02-12 10:55:37.016 1508-1508 keystore keystore I 1 0
2025-02-12 10:55:37.020 1663-1677 BroadcastQueue system_server W Background execution not allowed: receiving Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) } to com.google.android.gms/.stats.service.DropBoxEntryAddedReceiver
2025-02-12 10:55:37.020 1663-1677 BroadcastQueue system_server W Background execution not allowed: receiving Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) } to com.google.android.gms/.chimera.GmsIntentOperationService$PersistentTrustedReceiver
2025-02-12 10:55:37.020 28684-28684 GmsReceiverSupport com.google.android.gms.persistent W intent throttled: Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) }
2025-02-12 10:55:37.025 32421-3100 ReactNativeJS host.exp.exponent I Saving new token
2025-02-12 10:55:37.025 32421-3100 ReactNativeJS host.exp.exponent I New token obtained
2025-02-12 10:55:37.025 32421-3100 ReactNativeJS host.exp.exponent I Getting token
2025-02-12 10:55:37.026 1508-1508 keystore keystore I 1 0
2025-02-12 10:55:37.029 1663-1677 BroadcastQueue system_server W Background execution not allowed: receiving Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) } to com.google.android.gms/.stats.service.DropBoxEntryAddedReceiver
2025-02-12 10:55:37.029 1663-1677 BroadcastQueue system_server W Background execution not allowed: receiving Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) } to com.google.android.gms/.chimera.GmsIntentOperationService$PersistentTrustedReceiver
2025-02-12 10:55:37.029 28684-28684 GmsReceiverSupport com.google.android.gms.persistent W intent throttled: Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) }
2025-02-12 10:55:37.030 32421-3100 ReactNativeJS host.exp.exponent I Calling API
2025-02-12 10:55:37.148 32421-3100 ReactNativeJS host.exp.exponent I Requesting new token
2025-02-12 10:55:37.266 32421-3100 ReactNativeJS host.exp.exponent I Setting token
2025-02-12 10:55:37.267 1508-1508 keystore keystore I 1 0
2025-02-12 10:55:37```
Turns out that some headers are sent in lowercase format on Android, whereas the backend, which is built on PHP, is case sensitive when dealing with headers. Thus, the Axios request sent the 'authorization' header but the server expected an 'Authorization' header (notice the difference in case.) I adjusted the server to convert the headers into lowercase before doing the authentication process using JWT.