-
Notifications
You must be signed in to change notification settings - Fork 441
Description
Required Reading
- Confirmed
Plugin Version
4.19.0
Mobile operating-system(s)
- iOS
- Android
Device Manufacturer(s) and Model(s)
Likely all, but confirmed on iPhone 17 Pro and Pixel 8 simulators
Device operating-systems(s)
iOS 26.1, Android API 36
React Native / Expo version
RN 0.81.5, Expo 54.0.20
What happened?
This is a duplicate of #2376 which was closed without resolution.
When using AWS Cognito's OAuth2 /oauth2/token endpoint for token refresh, the plugin incorrectly uses
the id_token instead of the access_token from the refresh response. This causes all subsequent API
requests to fail with 401 Unauthorized.
Expected Behavior
When Cognito returns both access_token and id_token in the refresh response:
{
"access_token": "eyJraWQiOi...",
"id_token": "eyJraWQiOi...",
"token_type": "Bearer",
"expires_in": 300
}The plugin should use the access_token for subsequent HTTP requests.
Actual Behavior
The plugin uses the id_token as the access token, which:
- Has different JWT claims than the
access_token - Is not valid for API authentication
- Causes perpetual 401 errors after any token refresh
Evidence from Charles Proxy
1. Cognito refresh response contains both tokens:
{
"id_token": "eyJraWQiOiI5NG56R01cL2RzMUljM2ZNbTg5RlVZWG9HRTlFUkpaMlY0QU54bVFSaFZLdz0i...",
"access_token": "eyJraWQiOiJwcmpySEdsUW9RRXh2bUpIaWloTzhqZ1c4S2MzWmtmb1pmTkZpZ0ZHRWtBPSI...",
"expires_in": 300,
"token_type": "Bearer"
}2. Token prefix comparison proves wrong token is used:
| Token | kid (first part of JWT header) |
|---|---|
access_token from refresh |
eyJraWQiOiJwcmpySEdsUW9RRXh2bU... |
id_token from refresh |
eyJraWQiOiI5NG56R01cL2RzMUljM2... |
| Token sent in next API request | eyJraWQiOiI5NG56R01cL2RzMUljM2... ❌ Matches id_token |
3. Full Authorization header from subsequent /locations/log/ request:
Authorization: Bearer eyJraWQiOiI5NG56R01cL2RzMUljM2ZNbTg5RlVZWG9HRTlFUkpaMlY0QU54bVFSaFZLdz0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhM2JiYjkwNC0xZGUwLTRhODMtYmEyNi1jZjdmNzYzMDY2ZGYiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfM2hMelFnQTJ5IiwiY29nbml0bzp1c2VybmFtZSI6IjAxODU5ZWU4LTllZjItYTVkZS0yM2E1LTgwMDE1MzRiMDI2ZSIsIm9yaWdpbl9qdGkiOiJmNDM2NjZhZi0zODNkLTQ1ODUtYThmMi03MWVkYjFjMTAwNjUiLCJhdWQiOiI3M3V0cGxpOWJic2dwZjYxYmxnaDlwOW0zMyIsImV2ZW50X2lkIjoiMmRiOGQ2ZWMtMTI1NS00ZTU5LTljNjUtMGJmMTIwMDcxYmIxIiwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE3NjU0NjY1NjcsImV4cCI6MTc2NTQ3MDc5NiwiaWF0IjoxNzY1NDY3MTk2LCJqdGkiOiI4MWMyYTg3Yi1lNWIwLTQwMmQtYTBmZi0yZjA4ZjVkYmRiZjIiLCJlbWFpbCI6ImJlbisxQGJhbWJpLmhlYWx0aCJ9...
Decoding this JWT matches the id_token object below.
4. Decoded token claim comparison:
access_token claims (correct - has username claim required by backend):
{
"sub": "a3bbb904-1de0-4a83-ba26-cf7f763066df",
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_...",
"client_id": "73utpli9bbsgpf61blgh9p9m33",
"token_use": "access",
"scope": "aws.cognito.signin.user.admin",
"username": "01859ee8-9ef2-a5de-23a5-8001534b026e"
}id_token claims (wrong - has cognito:username instead of username, and token_use: "id"):
{
"sub": "a3bbb904-1de0-4a83-ba26-cf7f763066df",
"email_verified": true,
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_...",
"cognito:username": "01859ee8-9ef2-a5de-23a5-8001534b026e",
"aud": "73utpli9bbsgpf61blgh9p9m33",
"token_use": "id",
"email": "[email protected]"
}Secondary but Related Bug: Plugin's Internal Retry Loses Refresh Token
Observed in logs:
- Plugin detects 401 when access token expires → triggers internal token refresh
- Refresh succeeds (Cognito returns both
access_tokenandid_token) - Plugin incorrectly stores
id_tokenas the access token for future authentication (first bug) - Plugin immediately retries the failed HTTP request with the (wrong)
id_token - Server returns 401 again (because
id_tokenis not valid for API auth) - Plugin's internal retry mechanism triggers ANOTHER refresh (within ~100ms)
- But the
refresh_tokenis now empty → 400 error
So the new part here is why the second refresh was attempted with an empty string for the refresh token.
I'm not sure if this missing refresh token is somehow an expected outcome of using the wrong access token, but I wanted to call it out. If the cause is not obvious to you as to why I observed an empty string being passed for the refresh token on subsequent refresh attempts, I'm happy to re-examine this issue separately once the id_token/access_token fix is in.
Thank you for reviewing this and please let me know if I can provide any more information.
Plugin Code and/or Config
const config = {
// ... other config
url: 'https://ourdomain.com/locations/log/',
autoSync: true,
authorization: {
strategy: 'JWT',
accessToken: '<initial-access-token>',
refreshToken: '<refresh-token>',
expires: 300, // 5 minutes
refreshUrl: 'https://ourdomain.com/oauth2/token',
refreshPayload: {
refresh_token: '{refreshToken}',
client_id: '<cognito-client-id>',
grant_type: 'refresh_token',
},
refreshHeaders: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
};Relevant log output
# Before refresh - token expires
[geo] onHttp: Geolocation log sync failed with 401 {"detail": "Token is invalid or expired"}
# Refresh succeeds
[geo] Geolocation auth success {
"hasAccessToken": true,
"responseKeys": ["access_token", "expires_in", "token_type", "id_token"]
}
# But subsequent request STILL fails because id_token is being used
[geo] onHttp: Geolocation log sync failed with 401 {
"detail": "Token contained no recognizable user identification"
}
# Plugin thinks it needs to refresh again, but passes a blank string instead of the refresh token, hence 400
[geo] Geolocation auth error, status 400