mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 12:19:06 +00:00
protect from zombie auth
This commit is contained in:
@@ -26,6 +26,7 @@ const TranscriptCreate = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const isAuthenticated = auth.status === "authenticated";
|
const isAuthenticated = auth.status === "authenticated";
|
||||||
|
const isAuthRefreshing = auth.status === "refreshing";
|
||||||
const isLoading = auth.status === "loading";
|
const isLoading = auth.status === "loading";
|
||||||
const requireLogin = featureEnabled("requireLogin");
|
const requireLogin = featureEnabled("requireLogin");
|
||||||
|
|
||||||
@@ -133,7 +134,7 @@ const TranscriptCreate = () => {
|
|||||||
<Center>
|
<Center>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : requireLogin && !isAuthenticated ? (
|
) : requireLogin && !isAuthenticated && !isAuthRefreshing ? (
|
||||||
<Button onClick={() => auth.signIn("authentik")}>Log in</Button>
|
<Button onClick={() => auth.signIn("authentik")}>Log in</Button>
|
||||||
) : (
|
) : (
|
||||||
<Flex
|
<Flex
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import { authOptions } from "../../../lib/auth";
|
import { authOptions } from "../../../lib/authBackend";
|
||||||
|
|
||||||
const handler = NextAuth(authOptions);
|
const handler = NextAuth(authOptions);
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { createContext, useContext } from "react";
|
|||||||
import { useSession as useNextAuthSession } from "next-auth/react";
|
import { useSession as useNextAuthSession } from "next-auth/react";
|
||||||
import { signOut, signIn } from "next-auth/react";
|
import { signOut, signIn } from "next-auth/react";
|
||||||
import { configureApiAuth } from "./apiClient";
|
import { configureApiAuth } from "./apiClient";
|
||||||
import { assertExtendedTokenAndUserId, CustomSession } from "./types";
|
import { assertCustomSession, CustomSession } from "./types";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
import { SessionAutoRefresh } from "./SessionAutoRefresh";
|
import { SessionAutoRefresh } from "./SessionAutoRefresh";
|
||||||
|
import { REFRESH_ACCESS_TOKEN_ERROR } from "./auth";
|
||||||
|
|
||||||
type AuthContextType = (
|
type AuthContextType = (
|
||||||
| { status: "loading" }
|
| { status: "loading" }
|
||||||
@@ -28,29 +29,31 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { data: session, status, update } = useNextAuthSession();
|
const { data: session, status, update } = useNextAuthSession();
|
||||||
const customSession = session ? assertExtendedTokenAndUserId(session) : null;
|
const customSession = session ? assertCustomSession(session) : null;
|
||||||
console.log("customSessioncustomSession", customSession);
|
|
||||||
|
|
||||||
const contextValue: AuthContextType = {
|
const contextValue: AuthContextType = {
|
||||||
...(status === "loading" && !customSession
|
...(status === "loading" && !customSession
|
||||||
? { status }
|
? { status }
|
||||||
: status === "loading" && customSession
|
: status === "loading" && customSession
|
||||||
? { status: "refreshing" as const }
|
? { status: "refreshing" as const }
|
||||||
: status === "authenticated" && customSession?.accessToken
|
: status === "authenticated" &&
|
||||||
? {
|
customSession?.error === REFRESH_ACCESS_TOKEN_ERROR
|
||||||
status,
|
? { status: "unauthenticated" }
|
||||||
accessToken: customSession.accessToken,
|
: status === "authenticated" && customSession?.accessToken
|
||||||
accessTokenExpires: customSession.accessTokenExpires,
|
? {
|
||||||
user: customSession.user,
|
status,
|
||||||
}
|
accessToken: customSession.accessToken,
|
||||||
: status === "authenticated" && !customSession?.accessToken
|
accessTokenExpires: customSession.accessTokenExpires,
|
||||||
? (() => {
|
user: customSession.user,
|
||||||
console.warn(
|
}
|
||||||
"illegal state: authenticated but have no session/or access token. ignoring",
|
: status === "authenticated" && !customSession?.accessToken
|
||||||
);
|
? (() => {
|
||||||
return { status: "unauthenticated" as const };
|
console.warn(
|
||||||
})()
|
"illegal state: authenticated but have no session/or access token. ignoring",
|
||||||
: { status: "unauthenticated" as const }),
|
);
|
||||||
|
return { status: "unauthenticated" as const };
|
||||||
|
})()
|
||||||
|
: { status: "unauthenticated" as const }),
|
||||||
update,
|
update,
|
||||||
signIn,
|
signIn,
|
||||||
signOut,
|
signOut,
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import { useAuth } from "./AuthProvider";
|
|||||||
|
|
||||||
export function SessionAutoRefresh({
|
export function SessionAutoRefresh({
|
||||||
children,
|
children,
|
||||||
refreshInterval = 20 /* seconds */,
|
refreshInterval = 5 /* seconds */,
|
||||||
}) {
|
}) {
|
||||||
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;
|
const refreshIntervalMs = refreshInterval * 1000;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -25,7 +25,13 @@ export function SessionAutoRefresh({
|
|||||||
if (accessTokenExpires !== null) {
|
if (accessTokenExpires !== null) {
|
||||||
const timeLeft = accessTokenExpires - Date.now();
|
const timeLeft = accessTokenExpires - Date.now();
|
||||||
if (timeLeft < refreshIntervalMs) {
|
if (timeLeft < refreshIntervalMs) {
|
||||||
auth.update();
|
auth
|
||||||
|
.update()
|
||||||
|
.then(() => {})
|
||||||
|
.catch((e) => {
|
||||||
|
// note: 401 won't be considered error here
|
||||||
|
console.error("error refreshing auth token", e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, refreshIntervalMs);
|
}, refreshIntervalMs);
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ export const client = createClient<paths>({
|
|||||||
export const $api = createFetchClient<paths>(client);
|
export const $api = createFetchClient<paths>(client);
|
||||||
|
|
||||||
let currentAuthToken: string | null | undefined = null;
|
let currentAuthToken: string | null | undefined = null;
|
||||||
let authConfigured = false;
|
|
||||||
|
|
||||||
export const isAuthConfigured = () => authConfigured;
|
|
||||||
|
|
||||||
client.use({
|
client.use({
|
||||||
onRequest({ request }) {
|
onRequest({ request }) {
|
||||||
@@ -44,9 +41,4 @@ client.use({
|
|||||||
// the function contract: lightweight, idempotent
|
// the function contract: lightweight, idempotent
|
||||||
export const configureApiAuth = (token: string | null | undefined) => {
|
export const configureApiAuth = (token: string | null | undefined) => {
|
||||||
currentAuthToken = token;
|
currentAuthToken = token;
|
||||||
authConfigured = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useApiQuery = $api.useQuery;
|
|
||||||
export const useApiMutation = $api.useMutation;
|
|
||||||
export const useApiSuspenseQuery = $api.useSuspenseQuery;
|
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ export function useTranscriptsSearch(
|
|||||||
source_kind?: SourceKind;
|
source_kind?: SourceKind;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const { isAuthenticated } = useAuthReady();
|
|
||||||
|
|
||||||
return $api.useQuery(
|
return $api.useQuery(
|
||||||
"get",
|
"get",
|
||||||
"/v1/transcripts/search",
|
"/v1/transcripts/search",
|
||||||
@@ -66,7 +64,7 @@ export function useTranscriptsSearch(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isAuthenticated,
|
enabled: true, // anonymous enabled
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,178 +1 @@
|
|||||||
import { AuthOptions } from "next-auth";
|
export const REFRESH_ACCESS_TOKEN_ERROR = "RefreshAccessTokenError" as const;
|
||||||
import AuthentikProvider from "next-auth/providers/authentik";
|
|
||||||
import type { JWT } from "next-auth/jwt";
|
|
||||||
import { JWTWithAccessToken, CustomSession } from "./types";
|
|
||||||
import {
|
|
||||||
assertExists,
|
|
||||||
assertExistsAndNonEmptyString,
|
|
||||||
parseMaybeNonEmptyString,
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
const PRETIMEOUT = 600;
|
|
||||||
|
|
||||||
const tokenCache = new Map<
|
|
||||||
string,
|
|
||||||
{ token: JWTWithAccessToken; timestamp: number }
|
|
||||||
>();
|
|
||||||
const TOKEN_CACHE_TTL = 60 * 60 * 24 * 30 * 1000; // 30 days in milliseconds
|
|
||||||
|
|
||||||
const refreshLocks = new Map<string, Promise<JWTWithAccessToken>>();
|
|
||||||
|
|
||||||
const CLIENT_ID = assertExistsAndNonEmptyString(
|
|
||||||
process.env.AUTHENTIK_CLIENT_ID,
|
|
||||||
);
|
|
||||||
const CLIENT_SECRET = assertExistsAndNonEmptyString(
|
|
||||||
process.env.AUTHENTIK_CLIENT_SECRET,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const authOptions: AuthOptions = {
|
|
||||||
providers: [
|
|
||||||
AuthentikProvider({
|
|
||||||
clientId: CLIENT_ID,
|
|
||||||
clientSecret: CLIENT_SECRET,
|
|
||||||
issuer: process.env.AUTHENTIK_ISSUER,
|
|
||||||
authorization: {
|
|
||||||
params: {
|
|
||||||
scope: "openid email profile offline_access",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
session: {
|
|
||||||
strategy: "jwt",
|
|
||||||
},
|
|
||||||
callbacks: {
|
|
||||||
async jwt({ token, account, user }) {
|
|
||||||
const KEY = `token:${token.sub}`;
|
|
||||||
|
|
||||||
if (account && user) {
|
|
||||||
// called only on first login
|
|
||||||
// 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 expiresAtMs = expiresAtS * 1000;
|
|
||||||
if (!account.access_token) {
|
|
||||||
tokenCache.delete(KEY);
|
|
||||||
} else {
|
|
||||||
const jwtToken: JWTWithAccessToken = {
|
|
||||||
...token,
|
|
||||||
accessToken: account.access_token,
|
|
||||||
accessTokenExpires: expiresAtMs,
|
|
||||||
refreshToken: account.refresh_token || "",
|
|
||||||
};
|
|
||||||
// Store in memory cache
|
|
||||||
tokenCache.set(KEY, {
|
|
||||||
token: jwtToken,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
return jwtToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentToken = tokenCache.get(KEY);
|
|
||||||
if (currentToken && Date.now() < currentToken.token.accessTokenExpires) {
|
|
||||||
return currentToken.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
// access token has expired, try to update it
|
|
||||||
return await lockedRefreshAccessToken(token);
|
|
||||||
},
|
|
||||||
async session({ session, token }) {
|
|
||||||
const extendedToken = token as JWTWithAccessToken;
|
|
||||||
return {
|
|
||||||
...session,
|
|
||||||
accessToken: extendedToken.accessToken,
|
|
||||||
accessTokenExpires: extendedToken.accessTokenExpires,
|
|
||||||
error: extendedToken.error,
|
|
||||||
user: {
|
|
||||||
id: assertExists(extendedToken.sub),
|
|
||||||
name: extendedToken.name,
|
|
||||||
email: extendedToken.email,
|
|
||||||
},
|
|
||||||
} satisfies CustomSession;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
async function lockedRefreshAccessToken(
|
|
||||||
token: JWT,
|
|
||||||
): Promise<JWTWithAccessToken> {
|
|
||||||
const lockKey = `${token.sub}-refresh`;
|
|
||||||
|
|
||||||
const existingRefresh = refreshLocks.get(lockKey);
|
|
||||||
if (existingRefresh) {
|
|
||||||
return await existingRefresh;
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshPromise = (async () => {
|
|
||||||
try {
|
|
||||||
const cached = tokenCache.get(`token:${token.sub}`);
|
|
||||||
if (cached) {
|
|
||||||
if (Date.now() - cached.timestamp > TOKEN_CACHE_TTL) {
|
|
||||||
tokenCache.delete(`token:${token.sub}`);
|
|
||||||
} else if (Date.now() < cached.token.accessTokenExpires) {
|
|
||||||
return cached.token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentToken = cached?.token || (token as JWTWithAccessToken);
|
|
||||||
const newToken = await refreshAccessToken(currentToken);
|
|
||||||
|
|
||||||
tokenCache.set(`token:${token.sub}`, {
|
|
||||||
token: newToken,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return newToken;
|
|
||||||
} finally {
|
|
||||||
setTimeout(() => refreshLocks.delete(lockKey), 100);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
refreshLocks.set(lockKey, refreshPromise);
|
|
||||||
return refreshPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshAccessToken(token: JWT): Promise<JWTWithAccessToken> {
|
|
||||||
try {
|
|
||||||
const url = `${process.env.AUTHENTIK_REFRESH_TOKEN_URL}`;
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
body: new URLSearchParams({
|
|
||||||
client_id: process.env.AUTHENTIK_CLIENT_ID as string,
|
|
||||||
client_secret: process.env.AUTHENTIK_CLIENT_SECRET as string,
|
|
||||||
grant_type: "refresh_token",
|
|
||||||
refresh_token: token.refreshToken as string,
|
|
||||||
}).toString(),
|
|
||||||
method: "POST",
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(url, options);
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(
|
|
||||||
new Date().toISOString(),
|
|
||||||
"Failed to refresh access token. Response status:",
|
|
||||||
response.status,
|
|
||||||
);
|
|
||||||
const responseBody = await response.text();
|
|
||||||
console.error(new Date().toISOString(), "Response body:", responseBody);
|
|
||||||
throw new Error(`Failed to refresh access token: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
const refreshedTokens = await response.json();
|
|
||||||
return {
|
|
||||||
...token,
|
|
||||||
accessToken: refreshedTokens.access_token,
|
|
||||||
accessTokenExpires:
|
|
||||||
Date.now() + (refreshedTokens.expires_in - PRETIMEOUT) * 1000,
|
|
||||||
refreshToken: refreshedTokens.refresh_token,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error refreshing access token", error);
|
|
||||||
return {
|
|
||||||
...token,
|
|
||||||
error: "RefreshAccessTokenError",
|
|
||||||
} as JWTWithAccessToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
179
www/app/lib/authBackend.ts
Normal file
179
www/app/lib/authBackend.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { AuthOptions } from "next-auth";
|
||||||
|
import AuthentikProvider from "next-auth/providers/authentik";
|
||||||
|
import type { JWT } from "next-auth/jwt";
|
||||||
|
import { JWTWithAccessToken, CustomSession } from "./types";
|
||||||
|
import {
|
||||||
|
assertExists,
|
||||||
|
assertExistsAndNonEmptyString,
|
||||||
|
parseMaybeNonEmptyString,
|
||||||
|
} from "./utils";
|
||||||
|
import { REFRESH_ACCESS_TOKEN_ERROR } from "./auth";
|
||||||
|
|
||||||
|
const PRETIMEOUT = 600;
|
||||||
|
|
||||||
|
const tokenCache = new Map<
|
||||||
|
string,
|
||||||
|
{ token: JWTWithAccessToken; timestamp: number }
|
||||||
|
>();
|
||||||
|
const TOKEN_CACHE_TTL = 60 * 60 * 24 * 30 * 1000; // 30 days in milliseconds
|
||||||
|
|
||||||
|
const refreshLocks = new Map<string, Promise<JWTWithAccessToken>>();
|
||||||
|
|
||||||
|
const CLIENT_ID = assertExistsAndNonEmptyString(
|
||||||
|
process.env.AUTHENTIK_CLIENT_ID,
|
||||||
|
);
|
||||||
|
const CLIENT_SECRET = assertExistsAndNonEmptyString(
|
||||||
|
process.env.AUTHENTIK_CLIENT_SECRET,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const authOptions: AuthOptions = {
|
||||||
|
providers: [
|
||||||
|
AuthentikProvider({
|
||||||
|
clientId: CLIENT_ID,
|
||||||
|
clientSecret: CLIENT_SECRET,
|
||||||
|
issuer: process.env.AUTHENTIK_ISSUER,
|
||||||
|
authorization: {
|
||||||
|
params: {
|
||||||
|
scope: "openid email profile offline_access",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
async jwt({ token, account, user }) {
|
||||||
|
const KEY = `token:${token.sub}`;
|
||||||
|
|
||||||
|
if (account && user) {
|
||||||
|
// called only on first login
|
||||||
|
// 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 expiresAtMs = expiresAtS * 1000;
|
||||||
|
if (!account.access_token) {
|
||||||
|
tokenCache.delete(KEY);
|
||||||
|
} else {
|
||||||
|
const jwtToken: JWTWithAccessToken = {
|
||||||
|
...token,
|
||||||
|
accessToken: account.access_token,
|
||||||
|
accessTokenExpires: expiresAtMs,
|
||||||
|
refreshToken: account.refresh_token,
|
||||||
|
};
|
||||||
|
// Store in memory cache
|
||||||
|
tokenCache.set(KEY, {
|
||||||
|
token: jwtToken,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
return jwtToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentToken = tokenCache.get(KEY);
|
||||||
|
if (currentToken && Date.now() < currentToken.token.accessTokenExpires) {
|
||||||
|
return currentToken.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// access token has expired, try to update it
|
||||||
|
return await lockedRefreshAccessToken(token);
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
const extendedToken = token as JWTWithAccessToken;
|
||||||
|
return {
|
||||||
|
...session,
|
||||||
|
accessToken: extendedToken.accessToken,
|
||||||
|
accessTokenExpires: extendedToken.accessTokenExpires,
|
||||||
|
error: extendedToken.error,
|
||||||
|
user: {
|
||||||
|
id: assertExists(extendedToken.sub),
|
||||||
|
name: extendedToken.name,
|
||||||
|
email: extendedToken.email,
|
||||||
|
},
|
||||||
|
} satisfies CustomSession;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function lockedRefreshAccessToken(
|
||||||
|
token: JWT,
|
||||||
|
): Promise<JWTWithAccessToken> {
|
||||||
|
const lockKey = `${token.sub}-refresh`;
|
||||||
|
|
||||||
|
const existingRefresh = refreshLocks.get(lockKey);
|
||||||
|
if (existingRefresh) {
|
||||||
|
return await existingRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const cached = tokenCache.get(`token:${token.sub}`);
|
||||||
|
if (cached) {
|
||||||
|
if (Date.now() - cached.timestamp > TOKEN_CACHE_TTL) {
|
||||||
|
tokenCache.delete(`token:${token.sub}`);
|
||||||
|
} else if (Date.now() < cached.token.accessTokenExpires) {
|
||||||
|
return cached.token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentToken = cached?.token || (token as JWTWithAccessToken);
|
||||||
|
const newToken = await refreshAccessToken(currentToken);
|
||||||
|
|
||||||
|
tokenCache.set(`token:${token.sub}`, {
|
||||||
|
token: newToken,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return newToken;
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => refreshLocks.delete(lockKey), 100);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
refreshLocks.set(lockKey, refreshPromise);
|
||||||
|
return refreshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAccessToken(token: JWT): Promise<JWTWithAccessToken> {
|
||||||
|
try {
|
||||||
|
const url = `${process.env.AUTHENTIK_REFRESH_TOKEN_URL}`;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: process.env.AUTHENTIK_CLIENT_ID as string,
|
||||||
|
client_secret: process.env.AUTHENTIK_CLIENT_SECRET as string,
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: token.refreshToken as string,
|
||||||
|
}).toString(),
|
||||||
|
method: "POST",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(
|
||||||
|
new Date().toISOString(),
|
||||||
|
"Failed to refresh access token. Response status:",
|
||||||
|
response.status,
|
||||||
|
);
|
||||||
|
const responseBody = await response.text();
|
||||||
|
console.error(new Date().toISOString(), "Response body:", responseBody);
|
||||||
|
throw new Error(`Failed to refresh access token: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const refreshedTokens = await response.json();
|
||||||
|
return {
|
||||||
|
...token,
|
||||||
|
accessToken: refreshedTokens.access_token,
|
||||||
|
accessTokenExpires:
|
||||||
|
Date.now() + (refreshedTokens.expires_in - PRETIMEOUT) * 1000,
|
||||||
|
refreshToken: refreshedTokens.refresh_token,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing access token", error);
|
||||||
|
return {
|
||||||
|
...token,
|
||||||
|
error: REFRESH_ACCESS_TOKEN_ERROR,
|
||||||
|
} as JWTWithAccessToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { parseMaybeNonEmptyString } from "./utils";
|
|||||||
export interface JWTWithAccessToken extends JWT {
|
export interface JWTWithAccessToken extends JWT {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessTokenExpires: number;
|
accessTokenExpires: number;
|
||||||
refreshToken: string;
|
refreshToken?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,3 +65,10 @@ export const assertExtendedTokenAndUserId = <U, T extends { user?: U }>(
|
|||||||
}
|
}
|
||||||
throw new Error("Token is not extended with user id");
|
throw new Error("Token is not extended with user id");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// best attempt to check the session is valid
|
||||||
|
export const assertCustomSession = <S extends Session>(s: S): CustomSession => {
|
||||||
|
const r = assertExtendedTokenAndUserId(s);
|
||||||
|
// no other checks for now
|
||||||
|
return r as CustomSession;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user