From d6b100108a552c788888e57c98d8e4fc81abac9c Mon Sep 17 00:00:00 2001 From: Scott Anderson Date: Fri, 2 May 2025 15:02:53 -0400 Subject: [PATCH] JWT refresh (#636) ## Description: Implement refresh for JWTs. Related to https://github.com/openfrontio/infra/pull/26 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com> --- src/client/ApiSchemas.ts | 5 +++++ src/client/jwt.ts | 46 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/client/ApiSchemas.ts b/src/client/ApiSchemas.ts index e296d6c70..61952d3d5 100644 --- a/src/client/ApiSchemas.ts +++ b/src/client/ApiSchemas.ts @@ -1,6 +1,11 @@ import { z } from "zod"; import { base64urlToUuid } from "./Base64"; +export const RefreshResponseSchema = z.object({ + token: z.string(), +}); +export type RefreshResponse = z.infer; + export const TokenPayloadSchema = z.object({ jti: z.string(), sub: z diff --git a/src/client/jwt.ts b/src/client/jwt.ts index c7510d433..aa4d7edf2 100644 --- a/src/client/jwt.ts +++ b/src/client/jwt.ts @@ -1,5 +1,6 @@ import { decodeJwt } from "jose"; import { + RefreshResponseSchema, TokenPayload, TokenPayloadSchema, UserMeResponse, @@ -97,10 +98,17 @@ export function _isLoggedIn(): TokenPayload | false { localStorage.removeItem("token"); return false; } - // const maxAge: number | undefined = undefined; - // if (iat !== undefined && maxAge !== undefined && now >= iat + maxAge) { - // // TODO: Refresh token... - // } + const refreshAge: number = 6 * 3600; // 6 hours + if (iat !== undefined && now >= iat + refreshAge) { + console.log("Refreshing access token..."); + postRefresh().then((success) => { + if (success) { + console.log("Refreshed access token successfully."); + } else { + console.error("Failed to refresh access token."); + } + }); + } const result = TokenPayloadSchema.safeParse(payload); if (!result.success) { @@ -120,6 +128,36 @@ export function _isLoggedIn(): TokenPayload | false { } } +export async function postRefresh(): Promise { + try { + const token = getToken(); + if (!token) return false; + + // Refresh the JWT + const response = await fetch(getApiBase() + "/refresh", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + }, + }); + if (response.status !== 200) return false; + const body = await response.json(); + const result = RefreshResponseSchema.safeParse(body); + if (!result.success) { + console.error( + "Invalid response", + JSON.stringify(body), + JSON.stringify(result.error), + ); + return false; + } + localStorage.setItem("token", result.data.token); + return true; + } catch (e) { + return false; + } +} + export async function getUserMe(): Promise { try { const token = getToken();