Skip to content

[Bug]: id_token used instead of access_token after refresh #2444

@blwinters

Description

@blwinters

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:

  1. Has different JWT claims than the access_token
  2. Is not valid for API authentication
  3. 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:

  1. Plugin detects 401 when access token expires → triggers internal token refresh
  2. Refresh succeeds (Cognito returns both access_token and id_token)
  3. Plugin incorrectly stores id_token as the access token for future authentication (first bug)
  4. Plugin immediately retries the failed HTTP request with the (wrong) id_token
  5. Server returns 401 again (because id_token is not valid for API auth)
  6. Plugin's internal retry mechanism triggers ANOTHER refresh (within ~100ms)
  7. But the refresh_token is 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions