mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 04:09:06 +00:00
401 reauth experiments
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
import { createContext, useContext, useEffect } from "react";
|
||||
import { useSession as useNextAuthSession } from "next-auth/react";
|
||||
import { signOut, signIn } from "next-auth/react";
|
||||
import { configureApiAuth } from "./apiClient";
|
||||
import { configureApiAuth, configureApiAuthRefresh } from "./apiClient";
|
||||
import { assertCustomSession, CustomSession } from "./types";
|
||||
import { Session } from "next-auth";
|
||||
import { SessionAutoRefresh } from "./SessionAutoRefresh";
|
||||
@@ -88,6 +88,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
contextValue.status === "authenticated" ? contextValue.accessToken : null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
configureApiAuthRefresh(
|
||||
contextValue.status === "authenticated" ? contextValue.update : null,
|
||||
);
|
||||
}, [contextValue.status === "authenticated" && contextValue.update]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
<SessionAutoRefresh>{children}</SessionAutoRefresh>
|
||||
|
||||
@@ -25,15 +25,16 @@ export function SessionAutoRefresh({ children }) {
|
||||
const interval = setInterval(() => {
|
||||
if (accessTokenExpires !== null) {
|
||||
const timeLeft = accessTokenExpires - Date.now();
|
||||
if (timeLeft < REFRESH_BEFORE) {
|
||||
auth
|
||||
.update()
|
||||
.then(() => {})
|
||||
.catch((e) => {
|
||||
// note: 401 won't be considered error here
|
||||
console.error("error refreshing auth token", e);
|
||||
});
|
||||
}
|
||||
console.log("time left", timeLeft);
|
||||
// if (timeLeft < REFRESH_BEFORE) {
|
||||
// auth
|
||||
// .update()
|
||||
// .then(() => {})
|
||||
// .catch((e) => {
|
||||
// // note: 401 won't be considered error here
|
||||
// console.error("error refreshing auth token", e);
|
||||
// });
|
||||
// }
|
||||
}
|
||||
}, INTERVAL_REFRESH_MS);
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
import createFetchClient from "openapi-react-query";
|
||||
import { assertExistsAndNonEmptyString } from "./utils";
|
||||
import { isBuildPhase } from "./next";
|
||||
import { Session } from "next-auth";
|
||||
import { assertCustomSession } from "./types";
|
||||
import { HttpMethod, PathsWithMethod } from "openapi-typescript-helpers";
|
||||
|
||||
const API_URL = !isBuildPhase
|
||||
? assertExistsAndNonEmptyString(process.env.NEXT_PUBLIC_API_URL)
|
||||
@@ -25,12 +28,20 @@ export const client = createClient<paths>({
|
||||
export const $api = createFetchClient<paths>(client);
|
||||
|
||||
let currentAuthToken: string | null | undefined = null;
|
||||
let refreshAuthCallback: (() => Promise<Session | null>) | null = null;
|
||||
|
||||
const injectAuth = (request: Request, accessToken: string | null) => {
|
||||
if (accessToken) {
|
||||
request.headers.set("Authorization", `Bearer ${currentAuthToken}`);
|
||||
} else {
|
||||
request.headers.delete("Authorization");
|
||||
}
|
||||
return request;
|
||||
};
|
||||
|
||||
client.use({
|
||||
onRequest({ request }) {
|
||||
if (currentAuthToken) {
|
||||
request.headers.set("Authorization", `Bearer ${currentAuthToken}`);
|
||||
}
|
||||
request = injectAuth(request, currentAuthToken || null);
|
||||
// XXX Only set Content-Type if not already set (FormData will set its own boundary)
|
||||
// This is a work around for uploading file, we're passing a formdata
|
||||
// but the content type was still application/json
|
||||
@@ -44,7 +55,46 @@ client.use({
|
||||
},
|
||||
});
|
||||
|
||||
client.use({
|
||||
async onResponse({ response, request, params, schemaPath }) {
|
||||
if (response.status === 401) {
|
||||
console.log(
|
||||
"response.status is 401!",
|
||||
refreshAuthCallback,
|
||||
request,
|
||||
schemaPath,
|
||||
);
|
||||
}
|
||||
if (response.status === 401 && refreshAuthCallback) {
|
||||
try {
|
||||
const session = await refreshAuthCallback();
|
||||
if (!session) {
|
||||
console.warn("Token refresh failed, no session returned");
|
||||
return response;
|
||||
}
|
||||
const customSession = assertCustomSession(session);
|
||||
currentAuthToken = customSession.accessToken;
|
||||
const r = await client.request(
|
||||
request.method as HttpMethod,
|
||||
schemaPath as PathsWithMethod<paths, HttpMethod>,
|
||||
...params,
|
||||
);
|
||||
return r.response;
|
||||
} catch (error) {
|
||||
console.error("Token refresh failed during 401 retry:", error);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
// the function contract: lightweight, idempotent
|
||||
export const configureApiAuth = (token: string | null | undefined) => {
|
||||
currentAuthToken = token;
|
||||
};
|
||||
|
||||
export const configureApiAuthRefresh = (
|
||||
callback: (() => Promise<Session | null>) | null,
|
||||
) => {
|
||||
refreshAuthCallback = callback;
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ export const authOptions: AuthOptions = {
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, account, user }) {
|
||||
console.log("token.sub jwt callback", token.sub);
|
||||
const KEY = `token:${token.sub}`;
|
||||
|
||||
if (account && user) {
|
||||
@@ -70,6 +71,13 @@ export const authOptions: AuthOptions = {
|
||||
}
|
||||
|
||||
const currentToken = await getTokenCache(tokenCacheRedis, KEY);
|
||||
console.log(
|
||||
"currentToken.token.accessTokenExpires",
|
||||
currentToken?.token?.accessTokenExpires,
|
||||
currentToken?.token?.accessTokenExpires
|
||||
? Date.now() < currentToken?.token?.accessTokenExpires
|
||||
: "?",
|
||||
);
|
||||
if (currentToken && Date.now() < currentToken.token.accessTokenExpires) {
|
||||
return currentToken.token;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user