clarify access token refresh logic a bit

This commit is contained in:
Igor Loskutov
2025-09-03 12:31:50 -04:00
parent 0cbbd24c65
commit 6a793edfb5
3 changed files with 18 additions and 16 deletions

View File

@@ -9,22 +9,20 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useAuth } from "./AuthProvider"; import { useAuth } from "./AuthProvider";
import { REFRESH_ACCESS_TOKEN_BEFORE } from "./auth";
export function SessionAutoRefresh({ const REFRESH_BEFORE = REFRESH_ACCESS_TOKEN_BEFORE;
children,
refreshInterval = 5 /* seconds */, export function SessionAutoRefresh({ children }) {
}) {
const auth = useAuth(); const auth = useAuth();
const accessTokenExpires = const accessTokenExpires =
auth.status === "authenticated" ? auth.accessTokenExpires : null; auth.status === "authenticated" ? auth.accessTokenExpires : null;
console.log("authauth", auth);
const refreshIntervalMs = refreshInterval * 1000;
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
if (accessTokenExpires !== null) { if (accessTokenExpires !== null) {
const timeLeft = accessTokenExpires - Date.now(); const timeLeft = accessTokenExpires - Date.now();
if (timeLeft < refreshIntervalMs) { if (timeLeft < REFRESH_BEFORE) {
auth auth
.update() .update()
.then(() => {}) .then(() => {})
@@ -34,10 +32,10 @@ export function SessionAutoRefresh({
}); });
} }
} }
}, refreshIntervalMs); }, 5000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [accessTokenExpires, refreshInterval, auth.update]); }, [accessTokenExpires, auth.update]);
return children; return children;
} }

View File

@@ -1 +1,3 @@
export const REFRESH_ACCESS_TOKEN_ERROR = "RefreshAccessTokenError" as const; export const REFRESH_ACCESS_TOKEN_ERROR = "RefreshAccessTokenError" as const;
// 4 min is 1 min less than default authentic value. here we assume that authentic won't be set to access tokens < 4 min
export const REFRESH_ACCESS_TOKEN_BEFORE = 4 * 60 * 1000;

View File

@@ -7,15 +7,18 @@ import {
assertExistsAndNonEmptyString, assertExistsAndNonEmptyString,
parseMaybeNonEmptyString, parseMaybeNonEmptyString,
} from "./utils"; } from "./utils";
import { REFRESH_ACCESS_TOKEN_ERROR } from "./auth"; import {
REFRESH_ACCESS_TOKEN_BEFORE,
const PRETIMEOUT = 600; REFRESH_ACCESS_TOKEN_ERROR,
} from "./auth";
// TODO redis for vercel?
const tokenCache = new Map< const tokenCache = new Map<
string, string,
{ token: JWTWithAccessToken; timestamp: number } { token: JWTWithAccessToken; timestamp: number }
>(); >();
const TOKEN_CACHE_TTL = 60 * 60 * 24 * 30 * 1000; // 30 days in milliseconds // REFRESH_ACCESS_TOKEN_BEFORE because refresh is based on access token expiration (imagine we cache it 30 days)
const TOKEN_CACHE_TTL = REFRESH_ACCESS_TOKEN_BEFORE;
const refreshLocks = new Map<string, Promise<JWTWithAccessToken>>(); const refreshLocks = new Map<string, Promise<JWTWithAccessToken>>();
@@ -49,7 +52,7 @@ export const authOptions: AuthOptions = {
if (account && user) { if (account && user) {
// called only on first login // called only on first login
// XXX account.expires_in used in example is not defined for authentik backend, but expires_at is // XXX account.expires_in used in example is not defined for authentik backend, but expires_at is
const expiresAtS = assertExists(account.expires_at) - PRETIMEOUT; const expiresAtS = assertExists(account.expires_at);
const expiresAtMs = expiresAtS * 1000; const expiresAtMs = expiresAtS * 1000;
if (!account.access_token) { if (!account.access_token) {
tokenCache.delete(KEY); tokenCache.delete(KEY);
@@ -165,8 +168,7 @@ async function refreshAccessToken(token: JWT): Promise<JWTWithAccessToken> {
return { return {
...token, ...token,
accessToken: refreshedTokens.access_token, accessToken: refreshedTokens.access_token,
accessTokenExpires: accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
Date.now() + (refreshedTokens.expires_in - PRETIMEOUT) * 1000,
refreshToken: refreshedTokens.refresh_token, refreshToken: refreshedTokens.refresh_token,
}; };
} catch (error) { } catch (error) {