UNPKG

@joinmeow/cognito-passwordless-auth

Version:

Passwordless authentication with Amazon Cognito: FIDO2 (WebAuthn, support for Passkeys)

158 lines (157 loc) 8.16 kB
"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 }); }); });