Skip to content

fix(auth): handle NaN clockDrift to fix token auto-refresh#14689

Open
pgbezerra wants to merge 2 commits intoaws-amplify:mainfrom
pgbezerra:fix-custom_without_srp-token-storage
Open

fix(auth): handle NaN clockDrift to fix token auto-refresh#14689
pgbezerra wants to merge 2 commits intoaws-amplify:mainfrom
pgbezerra:fix-custom_without_srp-token-storage

Conversation

@pgbezerra
Copy link
Copy Markdown

@pgbezerra pgbezerra commented Jan 26, 2026

Tip

Review each commit separately

  • Make token expiration checks resilient to NaN clockDrift values
  • Add comprehensive test coverage for token refresh and clockDrift edge cases
  • Potentially addresses the issue where fetchAuthSession() does not auto-refresh expired tokens for CUSTOM_WITHOUT_SRP auth flow

Problem

If clockDrift were to become NaN, the token expiration check would silently fail:

// isTokenExpired.ts (before)
return currentTime + clockDrift + tolerance > expiresAt;
//     currentTime + NaN + tolerance = NaN
//     NaN > expiresAt = false (always)

This would cause expired tokens to never be detected as expired, preventing automatic refresh.

How clockDrift could become NaN

In TokenStore.loadTokens(), clockDrift is parsed from storage:

const clockDriftString = (await storage.getItem(key)) ?? '0';
const clockDrift = Number.parseInt(clockDriftString);

The nullish coalescing operator (??) only handles null/undefined, not empty strings. If clockDrift were stored as "" (empty string):

Stored Value ?? '0' result parseInt() result
null '0' 0
'123' '123' 123
'' '' (falsy!) NaN

This could happen with custom KeyValueStorage implementations or if certain auth flows don't properly initialize the clockDrift value.

Fix

Added defensive handling for NaN clockDrift at multiple layers:

  1. TokenStore.ts: Sanitize at load time
  2. TokenOrchestrator.ts: Use || instead of ?? for fallback (NaN || 0 = 0 vs NaN ?? 0 = NaN)
  3. isTokenExpired.ts: Final safety net with explicit NaN check

Related

🤖 Generated with Claude Code

@pgbezerra
Copy link
Copy Markdown
Author

Hello @soberm, I'm tagging you here, since you interacted with the ticket #14618. Appreciate your review 🙏🏻

pgbezerra and others added 2 commits March 11, 2026 17:31
…y#14618)

Add test cases for the getTokens() method in TokenOrchestrator to verify
token refresh behavior when tokens expire. These tests prove that:

- Expired access tokens trigger automatic refresh
- Expired ID tokens trigger automatic refresh
- forceRefresh option works correctly with valid tokens
- signInDetails are preserved after token refresh
- NotAuthorizedException returns null and clears tokens
- Network errors are thrown (not swallowed)
- clientMetadata is passed to the token refresher
- New tokens are stored after successful refresh

All 12 new tests pass, confirming the core token refresh logic works
as expected. This suggests issues reported in aws-amplify#14618 may be related
to specific user configurations rather than the refresh mechanism itself.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…fy#14618)

Fix automatic token refresh failing silently when clockDrift is NaN.
This addresses the issue where fetchAuthSession() does not auto-refresh
expired tokens for CUSTOM_WITHOUT_SRP auth flow.

## Root Cause

When clockDrift is NaN, the token expiration check always returns false:

```javascript
// isTokenExpired.ts (before)
return currentTime + clockDrift + tolerance > expiresAt;
//     currentTime + NaN + tolerance = NaN
//     NaN > expiresAt = false (always)
```

This causes expired tokens to never be detected as expired, preventing
automatic refresh.

## How clockDrift becomes NaN

In TokenStore.loadTokens(), clockDrift is parsed from storage:

```javascript
const clockDriftString = (await storage.getItem(key)) ?? '0';
const clockDrift = Number.parseInt(clockDriftString);
```

The nullish coalescing operator (??) only handles null/undefined, not
empty strings. If clockDrift is stored as "" (empty string):

| Stored Value | ?? '0' result | parseInt() result |
|--------------|---------------|-------------------|
| null         | '0'           | 0 ✓               |
| '123'        | '123'         | 123 ✓             |
| ''           | '' (falsy!)   | NaN ✗             |

This can happen with custom KeyValueStorage implementations or when
certain auth flows don't properly initialize the clockDrift value.

## Fix

Added three-layer defense against NaN clockDrift:

1. **TokenStore.ts**: Sanitize at load time
   ```javascript
   const parsedClockDrift = Number.parseInt(clockDriftString);
   const clockDrift = Number.isNaN(parsedClockDrift) ? 0 : parsedClockDrift;
   ```

2. **TokenOrchestrator.ts**: Use || instead of ?? for fallback
   ```javascript
   clockDrift: tokens.clockDrift || 0  // NaN || 0 = 0
   // vs
   clockDrift: tokens.clockDrift ?? 0  // NaN ?? 0 = NaN
   ```

3. **isTokenExpired.ts**: Final safety net
   ```javascript
   const safeClockDrift = Number.isNaN(clockDrift) ? 0 : clockDrift;
   ```

## Test Coverage

Added comprehensive tests for clockDrift edge cases:
- NaN clockDrift with expired tokens triggers refresh
- NaN clockDrift with valid tokens does not trigger refresh
- undefined clockDrift with expired tokens triggers refresh
- Positive/negative/zero clockDrift handled correctly

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@pgbezerra pgbezerra force-pushed the fix-custom_without_srp-token-storage branch from af45f26 to 45db6c3 Compare March 11, 2026 20:31
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 11, 2026

⚠️ No Changeset found

Latest commit: 45db6c3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pgbezerra
Copy link
Copy Markdown
Author

@pranavosu @ahmedhamouda78, I rebased the branch and noticed that there is a changeset-bot, but I can't add it since I'm not a maintainer.

Could you take a look at this PR? It's been more than a month now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Auth] fetchAuthSession() does not auto-refresh tokens in CUSTOM_WITHOUT_SRP flow

3 participants