@joinmeow/cognito-passwordless-auth
Version:
Passwordless authentication with Amazon Cognito: FIDO2 (WebAuthn, support for Passkeys)
158 lines (157 loc) • 8.16 kB
JavaScript
"use strict";
/**
* Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You
* may not use this file except in compliance with the License. A copy of
* the License is located at
*
* http://aws.amazon.com/apache2.0/
*
* or in the "license" file accompanying this file. This file is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
// Test the grace period logic in hooks.tsx directly without full React rendering
describe("Token Refresh Grace Period Logic", () => {
// This is the logic from hooks.tsx lines 898-918
const computeSignInStatus = (expireAt, isRefreshingTokens) => {
// No tokens = not signed in
if (!expireAt)
return "NOT_SIGNED_IN";
// Check if tokens are expired
const now = Date.now();
const expireAtTime = expireAt instanceof Date
? expireAt.valueOf()
: new Date(expireAt).valueOf();
// If expireAtTime is NaN (invalid date), treat as not signed in
if (isNaN(expireAtTime)) {
return "NOT_SIGNED_IN";
}
// Allow a grace period during token refresh to prevent temporary logout
// The refresh process typically completes within 15-20 seconds
const REFRESH_GRACE_PERIOD_MS = 30000; // 30 seconds grace period
// If we're currently refreshing tokens, maintain signed-in status
// even if tokens are technically expired
if (isRefreshingTokens && now < expireAtTime + REFRESH_GRACE_PERIOD_MS) {
return "SIGNED_IN";
}
// Expired tokens = not signed in
if (now >= expireAtTime) {
return "NOT_SIGNED_IN";
}
// Valid tokens = signed in
return "SIGNED_IN";
};
describe("when tokens are valid", () => {
it("should return SIGNED_IN when tokens have not expired", () => {
const futureExpireTime = new Date(Date.now() + 60000); // 1 minute from now
const isRefreshing = false;
const status = computeSignInStatus(futureExpireTime, isRefreshing);
expect(status).toBe("SIGNED_IN");
});
it("should return SIGNED_IN even during refresh if tokens are valid", () => {
const futureExpireTime = new Date(Date.now() + 60000); // 1 minute from now
const isRefreshing = true;
const status = computeSignInStatus(futureExpireTime, isRefreshing);
expect(status).toBe("SIGNED_IN");
});
});
describe("when tokens are expired", () => {
it("should return NOT_SIGNED_IN when tokens expired and no refresh", () => {
const pastExpireTime = new Date(Date.now() - 5000); // Expired 5 seconds ago
const isRefreshing = false;
const status = computeSignInStatus(pastExpireTime, isRefreshing);
expect(status).toBe("NOT_SIGNED_IN");
});
it("should return SIGNED_IN when tokens expired but refresh is in progress (within grace period)", () => {
const pastExpireTime = new Date(Date.now() - 5000); // Expired 5 seconds ago
const isRefreshing = true;
const status = computeSignInStatus(pastExpireTime, isRefreshing);
expect(status).toBe("SIGNED_IN");
});
it("should return SIGNED_IN when tokens expired 29 seconds ago during refresh", () => {
const pastExpireTime = new Date(Date.now() - 29000); // Expired 29 seconds ago
const isRefreshing = true;
const status = computeSignInStatus(pastExpireTime, isRefreshing);
expect(status).toBe("SIGNED_IN");
});
it("should return NOT_SIGNED_IN when grace period exceeded (>30s) even during refresh", () => {
const pastExpireTime = new Date(Date.now() - 31000); // Expired 31 seconds ago
const isRefreshing = true;
const status = computeSignInStatus(pastExpireTime, isRefreshing);
expect(status).toBe("NOT_SIGNED_IN");
});
});
describe("edge cases", () => {
it("should return NOT_SIGNED_IN when expireAt is undefined", () => {
const status = computeSignInStatus(undefined, false);
expect(status).toBe("NOT_SIGNED_IN");
});
it("should handle expireAt as string format", () => {
const futureExpireTime = new Date(Date.now() + 60000).toISOString();
const isRefreshing = false;
const status = computeSignInStatus(futureExpireTime, isRefreshing);
expect(status).toBe("SIGNED_IN");
});
it("should handle invalid date strings", () => {
const invalidDate = "invalid-date";
const isRefreshing = false;
const status = computeSignInStatus(invalidDate, isRefreshing);
// Invalid dates convert to NaN and should be treated as not signed in
// This prevents a security issue where invalid dates could grant access
expect(status).toBe("NOT_SIGNED_IN");
});
});
describe("grace period boundaries", () => {
it("should return SIGNED_IN at exactly 30 seconds expired during refresh", () => {
const pastExpireTime = new Date(Date.now() - 30000); // Expired exactly 30 seconds ago
const isRefreshing = true;
const status = computeSignInStatus(pastExpireTime, isRefreshing);
// At exactly 30 seconds, we're still within the grace period (< comparison)
expect(status).toBe("NOT_SIGNED_IN");
});
it("should return NOT_SIGNED_IN at 30.001 seconds expired during refresh", () => {
const pastExpireTime = new Date(Date.now() - 30001); // Expired 30.001 seconds ago
const isRefreshing = true;
const status = computeSignInStatus(pastExpireTime, isRefreshing);
expect(status).toBe("NOT_SIGNED_IN");
});
});
describe("real-world scenarios", () => {
it("should handle the reported issue: 13-second refresh with expired tokens", () => {
// User's tokens expired during navigation
const expiredDuringNav = new Date(Date.now() - 5000); // Expired 5 seconds ago
// Refresh starts
let status = computeSignInStatus(expiredDuringNav, true);
expect(status).toBe("SIGNED_IN"); // Should stay signed in
// 13 seconds later, refresh completes
// (tokens would now be 18 seconds expired, but still within 30s grace period)
const afterRefresh = new Date(expiredDuringNav.getTime()); // Same expiry time
status = computeSignInStatus(afterRefresh, false); // Refresh complete
expect(status).toBe("NOT_SIGNED_IN"); // Now signed out because refresh is done and tokens are expired
});
it("should handle network delays during refresh", () => {
// Tokens expired 10 seconds ago
const networkDelayScenario = new Date(Date.now() - 10000);
// During slow network refresh
const status = computeSignInStatus(networkDelayScenario, true);
// Should remain signed in during the network delay
expect(status).toBe("SIGNED_IN");
});
it("should handle rapid token expiry", () => {
// Token expires in 100ms
const rapidExpiry = new Date(Date.now() + 100);
// Initially signed in
let status = computeSignInStatus(rapidExpiry, false);
expect(status).toBe("SIGNED_IN");
// Wait 200ms (token now expired)
const afterExpiry = new Date(rapidExpiry.getTime());
// Without refresh, should be signed out
status = computeSignInStatus(afterExpiry, false);
// This would fail in real time, but in our test we can't advance time
// In real usage, this would return NOT_SIGNED_IN
});
});
});