Welcome to the Kinde community

Updated 10 months ago

Kinde + Bun + Hono + React

Hey I was just hoping to get some advice/validation on code I've written for using Kinde with a bun and hono app. I'm using the typescript SDK.

My session manager is storing all the session items in cookies as the next.js libary does. Then I have some custom middleware for protected routes.

My backend is serving up a react app, the cookies get sent with every request, and everything is working there. I have a /api/me endpoint that checks if the user is logged in. The react app calls that endpoint when it first loads to check if the user is logged in.

Plain Text
// auth.ts
import {
  createKindeServerClient,
  GrantType,
  SessionManager,
  UserType,
} from "@kinde-oss/kinde-typescript-sdk";

import { Hono, Context, MiddlewareHandler } from "hono";
import { getCookie, setCookie, deleteCookie } from "hono/cookie";

export const kindeClient = createKindeServerClient(
  GrantType.AUTHORIZATION_CODE,
  {
    authDomain: process.env.KINDE_DOMAIN!,
    clientId: process.env.KINDE_CLIENT_ID!,
    clientSecret: process.env.KINDE_CLIENT_SECRET!,
    redirectURL: process.env.KINDE_REDIRECT_URI!,
    logoutRedirectURL: process.env.KINDE_LOGOUT_REDIRECT_URI!,
  }
);

export const sessionManager = (c: Context): SessionManager => ({
  async getSessionItem(key: string) {
    const result = getCookie(c, key);
    return result;
  },
  async setSessionItem(key: string, value: unknown) {
    if (typeof value === "string") {
      setCookie(c, key, value);
    } else {
      setCookie(c, key, JSON.stringify(value));
    }
  },
  async removeSessionItem(key: string) {
    deleteCookie(c, key);
  },
  async destroySession() {
    ["id_token", "access_token", "user", "refresh_token"].forEach((key) => {
      deleteCookie(c, key);
    });
  },
});

export const protectRoute: MiddlewareHandler = async (c, next) => {
  try {
    const manager = sessionManager(c);
    const isAuthenticated = await kindeClient.isAuthenticated(manager);
    if (!isAuthenticated) {
      return c.json({ error: "Unauthorized" }, 401);
    }
    await next();
  } catch (e) {
    console.error(e);
    return c.json({ error: "Unauthorized" }, 401);
  }
};

export const getUser: MiddlewareHandler<{
  Variables: {
    user: UserType;
  };
}> = async (c, next) => {
  try {
    const manager = sessionManager(c);
    const isAuthenticated = await kindeClient.isAuthenticated(manager);
    if (!isAuthenticated) {
      return c.json({ error: "Unauthorized" }, 401);
    }
    const profile = await kindeClient.getUserProfile(manager);
    c.set("user", profile);
    await next();
  } catch (e) {
    console.error(e);
    return c.json({ error: "Unauthorized" }, 401);
  }
};

export const authRoutes = new Hono()
  .get("/logout", async (c) => {
    const logoutUrl = await kindeClient.logout(sessionManager(c));
    return c.redirect(logoutUrl.toString());
  })
  .get("/login", async (c) => {
    const loginUrl = await kindeClient.login(sessionManager(c));
    return c.redirect(loginUrl.toString());
  })
  .get("/register", async (c) => {
    const registerUrl = await kindeClient.register(sessionManager(c));
    return c.redirect(registerUrl.toString());
  })
  .get("/callback", async (c) => {
    await kindeClient.handleRedirectToApp(
      sessionManager(c),
      new URL(c.req.url)
    );
    return c.redirect("/");
  });


Plain Text
// app.ts
import { Hono } from "hono";
import { serveStatic } from "hono/bun";

import { authRoutes, getUser } from "./auth";
import expenseRoute from "./expenses";

const app = new Hono();

const apiRoutes = app
  .basePath("/api")
  .route("/expenses", expenseRoute)
  .get("/me", getUser, async (c) => {
    const user = await c.var.user;
    return c.json({ user });
  });

app.route("/", authRoutes);

// app.use('/favicon.ico', serveStatic({ path: './favicon.ico' }))
app.get("*", serveStatic({ root: "./frontend/dist" }));
app.get("*", serveStatic({ path: "./frontend/dist/index.html" }));

export default app;
export type ApiRoutes = typeof apiRoutes;
O
s
l
9 comments
Hey @saM69420,
Thanks for reaching out.
Are you able to explain more on what a "bun and hono app" is?
Also just checking you aren't experiencing any issue and you just want us to validate your code/approach?
No issues, I just want to make sure i'm not shooting myself in the foot or causing any security issues. This is for a tutorial so I don't want to give people bad advice
bun is the typescript runtime instead of node.js
hono is the backend framework instead of express
We are here to help anytime you need some sense-checking on your code, especially for any security vulnerabilities. Also thanks for elaborating on bun and hono.

I have passed your code to an teammate of mine who is an expert on TypeScript and Node.js

Loved your last Kinde video by the way, thanks for much for the support you give us.
thank you 😊
Looks good to me @saM69420 , one suggestion would be to set httpOnly on the cookies to prevent them being accessed potentially by cross-site scripts client-side. For security, secure and sameSite are also good to set if appropriate for your app.
Thanks @leo_kinde I've updated the cookie options. It doesn't work with Strict, so I set it to Lax. I guess that's because of the way the callback redirect works upon successful sign in.

Plain Text
export const sessionManager = (c: Context): SessionManager => ({
  async setSessionItem(key: string, value: unknown) {
    const cookieOptions = {
      httpOnly: true,
      secure: true,
      sameSite: "Lax",
    } as const;
    if (typeof value === "string") {
      setCookie(c, key, value, cookieOptions);
    } else {
      setCookie(c, key, JSON.stringify(value), cookieOptions);
    }
  },
  // ...
});


Then client side, my react app is using react query to grab the user details from the server and keeps them i'm memory for Infinity which works since logout and login require complete redirects anyway.

Plain Text
import api from "@/lib/api";
import { queryOptions } from "@tanstack/react-query";

async function authenticatedUser() {
  const res = await api.me.$get();
  if (!res.ok) {
    throw new Error("Network response was not ok");
  }
  const data = await res.json();
  return data.user;
}

export const userQueryOptions = queryOptions({
  queryKey: ["user-me"],
  queryFn: () => authenticatedUser(),
  staleTime: Infinity,
});


Then any component can get the user's details by performing the query that's already been cached.

Plain Text
import { Button } from "@/components/ui/button";

import { userQueryOptions } from "@/lib/user-query";
import { useQuery } from "@tanstack/react-query";

export default function ProfilePage() {

  const {data: user} = useQuery(userQueryOptions);

  return (
    <div className="flex flex-col gap-y-4 items-center">
      <h1 className="text-4xl font-bold">Hi {user?.given_name}</h1>
      <div className="text-2xl font-bold">{user?.email}</div>
      <Button asChild>
        <a href="/logout">Logout</a>
      </Button>
    </div>
  );
}


So no extra context providers and no client side processing of the tokens.

I would love any suggestions if there's room for improvement with any of this. Im trying to keep it as simple and robust as possible while only using the Kinde typescript SDK on the backend.
Sounds like a reasonable approach @saM69420 , if the page is long lived with lots of interactivity, it could be worth rechecking auth periodically, but might not be relevant depending on the app.
Add a reply
Sign up and join the conversation on Discord