@labdigital/federated-token-react
Version:
Federate JWT tokens for React clients
232 lines (231 loc) • 6.41 kB
JavaScript
"use client";
// src/provider.tsx
import { decodeJwt } from "jose";
import Cookie from "js-cookie";
import {
createContext,
useCallback,
useContext,
useEffect,
useState
} from "react";
import { jsx } from "react/jsx-runtime";
var TOKEN_VALID_BUFFER = 5 * 60;
var DEFAULT_COOKIE_NAMES = {
userData: "userData",
guestData: "guestData",
userRefreshTokenExists: "userRefreshTokenExists",
guestRefreshTokenExists: "guestRefreshTokenExists"
};
var AuthContext = createContext(void 0);
function AuthProvider({
children,
options
}) {
const [authState, setAuthState] = useState({
isAuthenticated: false,
hasToken: false,
values: null,
loading: true
});
const cookieNames = {
...DEFAULT_COOKIE_NAMES,
...options.cookieNames
};
const updateAuthState = useCallback((token) => {
if (token?.isAuthenticated) {
setAuthState({
isAuthenticated: true,
hasToken: true,
values: token.values,
loading: false
});
} else {
setAuthState({
isAuthenticated: false,
hasToken: Boolean(token),
values: null,
loading: false
});
}
}, []);
const checkTokenValidity = useCallback((token) => {
const timeSec = Math.floor(Date.now() / 1e3);
return Boolean(token?.exp && token.exp - TOKEN_VALID_BUFFER > timeSec);
}, []);
const getJWT = useCallback(() => {
const userToken = Cookie.get(cookieNames.userData);
const extractValues = (tokenPayload) => {
const skipKeys = ["exp"];
return Object.keys(tokenPayload).reduce(
(acc, key) => (
// biome-ignore lint/performance/noAccumulatingSpread: fixme
skipKeys.includes(key) ? acc : { ...acc, [key]: tokenPayload[key] }
),
{}
);
};
if (userToken) {
const tokenPayload = decodeToken(userToken);
if (tokenPayload) {
if (tokenPayload) {
return {
exp: tokenPayload.exp,
isAuthenticated: true,
values: extractValues(tokenPayload)
};
}
}
}
const guestToken = Cookie.get(cookieNames.guestData);
if (guestToken) {
const tokenPayload = decodeToken(guestToken);
if (tokenPayload) {
return {
exp: tokenPayload.exp,
isAuthenticated: false,
values: extractValues(tokenPayload)
};
}
}
}, [cookieNames.userData, cookieNames.guestData]);
const checkLocalToken = useCallback(() => {
const token = getJWT();
if (!token) {
return void 0;
}
return checkTokenValidity(token) ? token : void 0;
}, [getJWT, checkTokenValidity]);
const validateLocalToken = useCallback(() => {
const token = checkLocalToken();
updateAuthState(token);
}, [updateAuthState, checkLocalToken]);
const loadToken = useCallback(() => {
const token = getJWT();
updateAuthState(token);
}, [updateAuthState, getJWT]);
const checkToken = useCallback(async () => {
const token = await getAccessToken();
updateAuthState(token);
}, [options.refreshTokenEndpoint, updateAuthState]);
useEffect(() => {
loadToken();
}, [loadToken]);
const logout = async () => {
setAuthState({
isAuthenticated: false,
hasToken: false,
values: null,
loading: false
});
await clearTokens();
validateLocalToken();
};
const getAccessToken = async () => {
const token = getJWT();
const timeSec = Math.floor(Date.now() / 1e3);
const buffer = 5 * 60;
if (token?.exp && token.exp - buffer > timeSec) {
return token;
}
const hasRefreshToken = Cookie.get(cookieNames.userRefreshTokenExists) ?? Cookie.get(cookieNames.guestRefreshTokenExists);
if (hasRefreshToken) {
const success = await refreshAccessToken();
if (success) {
return getJWT();
}
}
return void 0;
};
const refreshAccessToken = async (signal) => {
if (!signal) {
const timeout = options.refreshTimeoutMs ?? 1e4;
signal = AbortSignal.timeout(timeout);
}
try {
if (options.refreshHandler) {
return await options.refreshHandler(signal);
}
if (!options.refreshTokenEndpoint || !options.refreshTokenMutation) {
throw new Error("No refresh token endpoint or mutation provided");
}
const response = await fetch(options.refreshTokenEndpoint, {
method: "POST",
body: options.refreshTokenMutation,
headers: {
"Content-Type": "application/json"
},
credentials: "include",
signal
});
if (!response.ok) {
throw new Error("Failed to refresh token");
}
const data = await response.json();
if (!data) {
throw new Error("Failed to refresh token");
}
if (data.errors && data.errors.length > 0) {
throw new Error("Failed to refresh token");
}
return data;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
const timeout = options.refreshTimeoutMs ?? 1e4;
throw new Error(`Token refresh timed out after ${timeout}ms`);
}
throw error;
}
};
const clearTokens = async () => {
if (options.logoutHandler) {
return options.logoutHandler();
}
if (!options.logoutEndpoint || !options.logoutMutation) {
throw new Error("No logout endpoint or mutation provided");
}
const response = await fetch(options.logoutEndpoint, {
method: "POST",
body: options.logoutMutation,
headers: {
"Content-Type": "application/json"
},
credentials: "include"
});
if (!response.ok) {
throw new Error(`Failed to clear token: ${response.statusText}`);
}
};
return /* @__PURE__ */ jsx(
AuthContext.Provider,
{
value: {
...authState,
logout,
validateLocalToken,
checkToken,
refreshToken: refreshAccessToken
},
children
}
);
}
function useAuth() {
const context = useContext(AuthContext);
if (context === void 0) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
var decodeToken = (token) => {
const decodedToken = decodeJwt(token);
if (!decodedToken) {
return void 0;
}
return decodedToken;
};
export {
AuthProvider,
useAuth
};
//# sourceMappingURL=index.js.map