Skip to content

Conversation

@pgbezerra
Copy link

@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 and others added 2 commits January 26, 2026 16:36
…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 <[email protected]>
…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 <[email protected]>
@pgbezerra
Copy link
Author

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

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

1 participant