T
TanStack•7mo ago
harsh-harlequin

External Backend JWT Authentication

I am developing a React application with Tanstack Start that works with an external backend. This backend provides access/refresh token for authentication. I am looking for a way to implement this authentication structure with Tanstack Start. Is there an example on this topic? Or can you give me some ideas on how to proceed?
27 Replies
like-gold
like-gold•7mo ago
I am also looking for something similar
wise-white
wise-white•7mo ago
I'd imagine you'd check your tokens in the middleware on server functions
harsh-harlequin
harsh-harlequinOP•7mo ago
Yes, but I also want to be able to make requests to the external backend from a client component. And if the access token expires, the new access token should be updated on the server and client. Likewise, if the request is made from the server component and the token expires, the same flow should work again. I don't know, I'm really confused. I think we were happier when we only had CSR in our lives 🙂
metropolitan-bronze
metropolitan-bronze•7mo ago
I'm also looking for exactly what you described. Right now I've only figured out how to make token refresh work with server.
like-gold
like-gold•7mo ago
GitHub
GitHub - j-mcfarlane/tanstack-start-jwt
Contribute to j-mcfarlane/tanstack-start-jwt development by creating an account on GitHub.
like-gold
like-gold•7mo ago
Here is the repo I have made that has refresh login/logout/register using JWTs - one of the things I would like to improve is the fact that there is a query every route change/preload getting the user Getting the user every request is debatable as its something that should be cached but at the same time it validates the token and additionally if there are permissions/roles involved this would ensure that the user has the most updated object possible
harsh-harlequin
harsh-harlequinOP•7mo ago
@jmcfeever this is really cool. Thanks for sharing this. It's a great repo for those who want to start working with jwt acces/refresh token.
fascinating-indigo
fascinating-indigo•7mo ago
From a security point of view, its best to keep the tokens on the backend only. And then pass a session id in a cookie. The "client" calls that you want to do, couldn't that be routed through a server fn / api fn? You can use something like BetterAuth that handles some of the auth for you on the server side.
like-gold
like-gold•7mo ago
@lajtmaN it is in a serverfn? If you can make improvements I would be thrilled to see what you mean on a pr.
adverse-sapphire
adverse-sapphire•7mo ago
@jmcfeever Thank for sharing this! I was really struggling with JWT auth.
like-gold
like-gold•7mo ago
Did you get it working for yourself @Enkhjil ?
adverse-sapphire
adverse-sapphire•7mo ago
@jmcfeever I've started implementing it in my code. But somehow session data is empty even though I can see the cooke is set in the browser
variable-lime
variable-lime•7mo ago
If anyone needs help with this lmk and ill upload a repo It would help to know where everyone is getting confused though.
like-gold
like-gold•7mo ago
@Rykuno would love to see your strategy compared to the implementation listed above
variable-lime
variable-lime•7mo ago
import createClient, { type Middleware } from "openapi-fetch";
import type { paths } from "../openapi";
import { createServerFn } from "@tanstack/react-start";
import { useAppSession } from "./session";
import { z } from "zod";

export const getSessionData = createServerFn({ method: "GET" }).handler(
async () => {
const session = await useAppSession();
return session.data;
}
);

export const setSessionData = createServerFn({ method: "POST" })
.validator(z.object({ accessToken: z.string(), refreshToken: z.string() }))
.handler(async ({ data }) => {
const session = await useAppSession();
await session.update({
accessToken: data.accessToken,
refreshToken: data.refreshToken
});
});

const refreshSession = createServerFn({ method: "POST" }).handler(async () => {
const session = await useAppSession();
const { data } = await rpcWithoutMiddleware.POST("/auth/refresh-tokens", {
body: {
refreshToken: session.data.refreshToken
}
});
if (!data) throw new Error("Failed to refresh session");
await setSessionData({
data: {
accessToken: data.accessToken,
refreshToken: data.refreshToken
}
});
});

const shouldRefreshAccessToken = (accessToken: string) => {
const tokenParts = accessToken.split(".");
if (tokenParts.length === 3) {
const payload = JSON.parse(atob(tokenParts[1]));
const exp = payload.exp * 1000;
const now = Date.now();
const timeLeft = exp - now;
return timeLeft < 5 * 60 * 1000;
}
return false;
};

const authMiddleware: Middleware = {
async onRequest({ request }) {
let sessionData = await getSessionData();

const staleSession =
sessionData?.accessToken &&
shouldRefreshAccessToken(sessionData.accessToken);

if (staleSession) {
await refreshSession();
sessionData = await getSessionData();
}

if (sessionData.accessToken)
request.headers.set("Authorization", `Bearer ${sessionData.accessToken}`);
return request;
}
};

export const rpc = createClient<paths>({ baseUrl: "http://localhost:8080" });
const rpcWithoutMiddleware = createClient<paths>({
baseUrl: "http://localhost:8080"
});

rpc.use(authMiddleware);
import createClient, { type Middleware } from "openapi-fetch";
import type { paths } from "../openapi";
import { createServerFn } from "@tanstack/react-start";
import { useAppSession } from "./session";
import { z } from "zod";

export const getSessionData = createServerFn({ method: "GET" }).handler(
async () => {
const session = await useAppSession();
return session.data;
}
);

export const setSessionData = createServerFn({ method: "POST" })
.validator(z.object({ accessToken: z.string(), refreshToken: z.string() }))
.handler(async ({ data }) => {
const session = await useAppSession();
await session.update({
accessToken: data.accessToken,
refreshToken: data.refreshToken
});
});

const refreshSession = createServerFn({ method: "POST" }).handler(async () => {
const session = await useAppSession();
const { data } = await rpcWithoutMiddleware.POST("/auth/refresh-tokens", {
body: {
refreshToken: session.data.refreshToken
}
});
if (!data) throw new Error("Failed to refresh session");
await setSessionData({
data: {
accessToken: data.accessToken,
refreshToken: data.refreshToken
}
});
});

const shouldRefreshAccessToken = (accessToken: string) => {
const tokenParts = accessToken.split(".");
if (tokenParts.length === 3) {
const payload = JSON.parse(atob(tokenParts[1]));
const exp = payload.exp * 1000;
const now = Date.now();
const timeLeft = exp - now;
return timeLeft < 5 * 60 * 1000;
}
return false;
};

const authMiddleware: Middleware = {
async onRequest({ request }) {
let sessionData = await getSessionData();

const staleSession =
sessionData?.accessToken &&
shouldRefreshAccessToken(sessionData.accessToken);

if (staleSession) {
await refreshSession();
sessionData = await getSessionData();
}

if (sessionData.accessToken)
request.headers.set("Authorization", `Bearer ${sessionData.accessToken}`);
return request;
}
};

export const rpc = createClient<paths>({ baseUrl: "http://localhost:8080" });
const rpcWithoutMiddleware = createClient<paths>({
baseUrl: "http://localhost:8080"
});

rpc.use(authMiddleware);
I just have server functions and keep all auth logic server side and proxy calls though it I just threw this together this morning while playing with tanstack start - so eviction is not there but
harsh-harlequin
harsh-harlequinOP•7mo ago
Hi @Rykuno, thank you for sharing this. One question - Here you are doing a manual check with shouldRefreshAccessToken function. What if an access token is revoked on the external backend? I mean, when an access token is revoked in the external backend and the access token stored in the client has not expired, I think you cannot renew the tokens.
variable-lime
variable-lime•7mo ago
Usually I believe refresh tokens are revoked, not access tokens - right? At least thats how I've always done it. I cleaned this up...a lot... So if it attempts to refresh the token and the server errors, it will clear the session and log the user out. This would happen if the auth server couldn't respond or if the reresh-token session wasn't renewed (session revocation)
adverse-sapphire
adverse-sapphire•7mo ago
do you have any idea why the access token is undefined verifyAuth function?
import { createServerFn } from "@tanstack/start";
import { authenticateUser } from "~/libs/cognito";
import { getSessionData, setSessionData } from "./session.server";

export const loginFn = createServerFn({ method: "POST" })
.validator((data: { username: string; password: string }) => data)
.handler(async ({ data }) => {
const authResult = await authenticateUser({ ...data });
const { AccessToken, RefreshToken } = authResult;
if (!AccessToken || !RefreshToken) {
throw Error("AccessToken or RefreshToken is undefined");
}

await setSessionData({
data: { accessToken: AccessToken, refreshToken: RefreshToken },
});

return { accessToken: AccessToken, refreshToken: RefreshToken };
});

export const verifyAuth = createServerFn({ method: "GET" }).handler(
async () => {
const { accessToken } = await getSessionData();
// SOMEHOW ACCESS TOKEN IS UNDEFINED
console.log("accessToken: ", accessToken);

return { authenticated: false };
},
);
import { createServerFn } from "@tanstack/start";
import { authenticateUser } from "~/libs/cognito";
import { getSessionData, setSessionData } from "./session.server";

export const loginFn = createServerFn({ method: "POST" })
.validator((data: { username: string; password: string }) => data)
.handler(async ({ data }) => {
const authResult = await authenticateUser({ ...data });
const { AccessToken, RefreshToken } = authResult;
if (!AccessToken || !RefreshToken) {
throw Error("AccessToken or RefreshToken is undefined");
}

await setSessionData({
data: { accessToken: AccessToken, refreshToken: RefreshToken },
});

return { accessToken: AccessToken, refreshToken: RefreshToken };
});

export const verifyAuth = createServerFn({ method: "GET" }).handler(
async () => {
const { accessToken } = await getSessionData();
// SOMEHOW ACCESS TOKEN IS UNDEFINED
console.log("accessToken: ", accessToken);

return { authenticated: false };
},
);
I can see the session cookie is set in my browser storage. I'm calling verifyAuth in beforeLoad but when i refresh my browser the accessToken is undefined
stormy-gold
stormy-gold•7mo ago
do you have a full repo somewhere?
harsh-harlequin
harsh-harlequinOP•7mo ago
Makes sense. Could you share a working repo?
adverse-sapphire
adverse-sapphire•6mo ago
@Manuel Schiller It seems like I'm hitting the cookie size limit here, even though its under 4KB. Access and Refresh tokens have 1078, 1797 bytes length respectively. When I set only one of them it works. Is there any way to increase this size?
stormy-gold
stormy-gold•6mo ago
please provide a complete minimal example and create a github issue for this
adverse-sapphire
adverse-sapphire•6mo ago
@Manuel Schiller Here is the example code. I've hardcoded mock JWT tokens. Please go to /login route. https://github.com/enkhjile/tanstack-start-jwt-auth
GitHub
GitHub - enkhjile/tanstack-start-jwt-auth
Contribute to enkhjile/tanstack-start-jwt-auth development by creating an account on GitHub.
stormy-gold
stormy-gold•6mo ago
please create a GitHub issue for this
adverse-sapphire
adverse-sapphire•6mo ago
Do you mean in tanstack start repo?
stormy-gold
stormy-gold•6mo ago
router but yeah
adverse-sapphire
adverse-sapphire•6mo ago
okay sure

Did you find this page helpful?