fix(client): handle missing access token + delayed autoRefresh timer
- getAccessToken(): attempt refreshToken() when access token is absent but refresh token exists (previously returned null immediately) - setupAutoRefresh(): when delay <= 0 (background tab woke up after timer window), refresh immediately instead of silently exiting - init(): when _initialized=true but session is expired, attempt silent refresh instead of returning false immediately - Add [IAMClient] debug logging to all key lifecycle points
This commit is contained in:
@@ -73,7 +73,19 @@ export class StuffleIAMClient {
|
||||
* Safe to call multiple times — subsequent calls return the same promise.
|
||||
*/
|
||||
async init(): Promise<boolean> {
|
||||
if (this._initialized) return this.isAuthenticated();
|
||||
// Allow re-initialization if tokens have expired (e.g. called from a 401 handler).
|
||||
// The guard only blocks duplicate concurrent inits, not recovery calls.
|
||||
if (this._initialized && this.isAuthenticated()) return true;
|
||||
if (this._initialized && !this.isAuthenticated()) {
|
||||
// Access token expired — attempt a silent refresh instead of returning false immediately
|
||||
console.debug('[IAMClient] init() called on expired session — attempting silent refresh');
|
||||
const tokens = await this.refreshToken();
|
||||
if (tokens) {
|
||||
if (this.config.autoRefresh) this.setupAutoRefresh();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (this._initPromise) return this._initPromise;
|
||||
|
||||
this._initPromise = this._doInit();
|
||||
@@ -85,14 +97,18 @@ export class StuffleIAMClient {
|
||||
const accessToken = this.storage.get(STORAGE_KEYS.ACCESS_TOKEN);
|
||||
const refreshTokenValue = this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
|
||||
|
||||
console.debug('[IAMClient] _doInit: accessToken present =', !!accessToken, '| refreshToken present =', !!refreshTokenValue);
|
||||
|
||||
if (!accessToken && !refreshTokenValue) {
|
||||
// No session to restore
|
||||
console.debug('[IAMClient] _doInit: no tokens found — unauthenticated');
|
||||
this._initialized = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (accessToken && !isTokenExpired(accessToken)) {
|
||||
// Access token still valid — just re-setup auto-refresh
|
||||
console.debug('[IAMClient] _doInit: access token valid — setting up auto-refresh');
|
||||
if (this.config.autoRefresh && refreshTokenValue) {
|
||||
this.setupAutoRefresh();
|
||||
}
|
||||
@@ -103,6 +119,7 @@ export class StuffleIAMClient {
|
||||
|
||||
// Access token expired (or missing) but refresh token exists — silent refresh
|
||||
if (refreshTokenValue) {
|
||||
console.debug('[IAMClient] _doInit: access token expired — attempting silent refresh');
|
||||
const tokens = await this.refreshToken();
|
||||
if (tokens) {
|
||||
if (this.config.autoRefresh) {
|
||||
@@ -111,13 +128,14 @@ export class StuffleIAMClient {
|
||||
this._initialized = true;
|
||||
return true;
|
||||
}
|
||||
console.warn('[IAMClient] _doInit: silent refresh failed — unauthenticated');
|
||||
}
|
||||
|
||||
// Refresh failed or no refresh token
|
||||
this._initialized = true;
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error('[StuffleIAMClient] init() failed:', err);
|
||||
console.error('[IAMClient] _doInit failed:', err);
|
||||
this._initialized = true;
|
||||
return false;
|
||||
}
|
||||
@@ -320,7 +338,12 @@ export class StuffleIAMClient {
|
||||
*/
|
||||
async refreshToken(): Promise<TokenResponse | null> {
|
||||
const refreshToken = this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
|
||||
if (!refreshToken) return null;
|
||||
if (!refreshToken) {
|
||||
console.debug('[IAMClient] refreshToken: no refresh token in storage');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.debug('[IAMClient] refreshToken: attempting token refresh');
|
||||
|
||||
const discovery = await this.getDiscovery();
|
||||
|
||||
@@ -337,6 +360,8 @@ export class StuffleIAMClient {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.json().catch(() => ({}));
|
||||
console.error('[IAMClient] refreshToken: FAILED', response.status, errorBody);
|
||||
// Refresh failed - clear tokens
|
||||
this.clearTokens();
|
||||
this.notifyListeners();
|
||||
@@ -344,6 +369,7 @@ export class StuffleIAMClient {
|
||||
}
|
||||
|
||||
const tokens: TokenResponse = await response.json();
|
||||
console.debug('[IAMClient] refreshToken: SUCCESS — new token expires_in =', tokens.expires_in, 's');
|
||||
this.storeTokens(tokens);
|
||||
this.notifyListeners();
|
||||
|
||||
@@ -379,11 +405,23 @@ export class StuffleIAMClient {
|
||||
*/
|
||||
async getAccessToken(): Promise<string | null> {
|
||||
const accessToken = this.storage.get(STORAGE_KEYS.ACCESS_TOKEN);
|
||||
|
||||
if (!accessToken) return null;
|
||||
|
||||
// Check if token needs refresh
|
||||
if (!accessToken) {
|
||||
// No access token — attempt silent refresh if refresh token exists
|
||||
if (this.storage.get(STORAGE_KEYS.REFRESH_TOKEN)) {
|
||||
console.debug('[IAMClient] getAccessToken: no access token — attempting refresh');
|
||||
const tokens = await this.refreshToken();
|
||||
if (tokens) {
|
||||
if (this.config.autoRefresh) this.setupAutoRefresh();
|
||||
}
|
||||
return tokens?.access_token ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if token needs refresh (within threshold window)
|
||||
if (isTokenExpired(accessToken, this.config.refreshThreshold)) {
|
||||
console.debug('[IAMClient] getAccessToken: token within refresh threshold — refreshing');
|
||||
const tokens = await this.refreshToken();
|
||||
return tokens?.access_token ?? null;
|
||||
}
|
||||
@@ -519,10 +557,16 @@ export class StuffleIAMClient {
|
||||
const delay = refreshAt - Date.now();
|
||||
|
||||
if (delay > 0) {
|
||||
console.debug('[IAMClient] setupAutoRefresh: timer set for', Math.round(delay / 1000), 's from now');
|
||||
this.refreshTimer = setTimeout(async () => {
|
||||
console.debug('[IAMClient] autoRefresh: timer fired — refreshing token');
|
||||
await this.refreshToken();
|
||||
this.setupAutoRefresh();
|
||||
}, delay);
|
||||
} else {
|
||||
// Token already within threshold or expired — refresh immediately
|
||||
console.debug('[IAMClient] setupAutoRefresh: delay <= 0 (', delay, 'ms) — refreshing immediately');
|
||||
this.refreshToken().then(() => this.setupAutoRefresh());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user