H
Hono•2mo ago
Adesh

Get `auth` session when API endpoint is called from a server component

import { Hono } from 'hono'
import { verifyAuth } from '@hono/auth-js'
import { zValidator } from '@hono/zod-validator'
import { db } from '@/db'
import { insertPostSchema, posts } from '@/db/schema'
import { auth } from '@/auth'

const app = new Hono()
// Protect API for authenticated users only
.get('/', async (c) => {
// Get the session manually instead of using middleware
const session = await auth()

// Check authentication
if (!session?.user) {
return c.json({ error: 'Unauthorized' }, 401)
}

const posts = await db.query.posts.findMany({})
return c.json(posts)
})
import { Hono } from 'hono'
import { verifyAuth } from '@hono/auth-js'
import { zValidator } from '@hono/zod-validator'
import { db } from '@/db'
import { insertPostSchema, posts } from '@/db/schema'
import { auth } from '@/auth'

const app = new Hono()
// Protect API for authenticated users only
.get('/', async (c) => {
// Get the session manually instead of using middleware
const session = await auth()

// Check authentication
if (!session?.user) {
return c.json({ error: 'Unauthorized' }, 401)
}

const posts = await db.query.posts.findMany({})
return c.json(posts)
})
The session is undefined when the route is called from the server component, but I get the value when I call it from the client component
41 Replies
ambergristle
ambergristle•2mo ago
sounds like something to do w how (next.js?) handles cookies in server components but could also have to do w middleware order ig can you share more info about your setup?
Adesh
AdeshOP•2mo ago
Sure, I am using Next.js, auth.js for auth And Hono for my backend I don't have any middleware setup as of now Although I also tried using middleware.ts file
import { auth } from '@/auth'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
// Only apply to /api routes
if (request.nextUrl.pathname.startsWith('/api')) {
const session = await auth()

if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
}

return NextResponse.next()
}

export const config = {
matcher: '/api/:path*',
}
import { auth } from '@/auth'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
// Only apply to /api routes
if (request.nextUrl.pathname.startsWith('/api')) {
const session = await auth()

if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
}

return NextResponse.next()
}

export const config = {
matcher: '/api/:path*',
}
But still the result is same
ambergristle
ambergristle•2mo ago
The session is undefined when the route is called from the server component, but I get the value when I call it from the client component
well, when the call is made from the browser, it 100% has access to whatever cookies are on the browser and if a request is made from a server component during ssr, you'd expect the cookie to be set if there is one
Adesh
AdeshOP•2mo ago
Makes sense, I am trying to prefetch the data for the client to use, But since the cookie is not available it fails and the client has to fetch the actual data. Is there a way to tell if the request if coming from client or server component, that way I could skip the auth check incase of server component and add that to the server component itself, since I am getting the session data there Here's what I mean
import CreatePostForm from '@/components/create-post-form'
import PostList from '@/app/posts/postList'
import { auth } from '@/auth'
import Unauthorized from '@/components/unauthorized'
import { Toaster } from 'sonner'
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query'
import { getPostQueryOptions } from '@/hooks/post'

const Posts = async () => {
const session = await auth()

if (!session?.user) {
return <Unauthorized />
}

const queryClient = new QueryClient()
// this fails currently, due to the auth check
// If I can skip it in this case only, things would work
await queryClient.prefetchQuery(getPostQueryOptions)

return (
<>
<Toaster />
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
Posts
</h1>
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList />
</HydrationBoundary>
<div>
<CreatePostForm userId={session?.user.id || ''} />
</div>
</>
)
}

export default Posts
import CreatePostForm from '@/components/create-post-form'
import PostList from '@/app/posts/postList'
import { auth } from '@/auth'
import Unauthorized from '@/components/unauthorized'
import { Toaster } from 'sonner'
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query'
import { getPostQueryOptions } from '@/hooks/post'

const Posts = async () => {
const session = await auth()

if (!session?.user) {
return <Unauthorized />
}

const queryClient = new QueryClient()
// this fails currently, due to the auth check
// If I can skip it in this case only, things would work
await queryClient.prefetchQuery(getPostQueryOptions)

return (
<>
<Toaster />
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
Posts
</h1>
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList />
</HydrationBoundary>
<div>
<CreatePostForm userId={session?.user.id || ''} />
</div>
</>
)
}

export default Posts
ambergristle
ambergristle•2mo ago
what does auth look like?
Adesh
AdeshOP•2mo ago
import NextAuth from 'next-auth'
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import { db } from './db'
import GitHub from '@auth/core/providers/github'
import Google from '@auth/core/providers/google'

export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: DrizzleAdapter(db),
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
callbacks: {
session({ session, user }) {
session.user.id = user.id
return session
},
},
})
import NextAuth from 'next-auth'
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import { db } from './db'
import GitHub from '@auth/core/providers/github'
import Google from '@auth/core/providers/google'

export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: DrizzleAdapter(db),
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
callbacks: {
session({ session, user }) {
session.user.id = user.id
return session
},
},
})
ambergristle
ambergristle•2mo ago
ah, i see is this the same auth, because i wouldn't expect that to work
import { auth } from '@/auth'

const app = new Hono()
// Protect API for authenticated users only
.get('/', async (c) => {
// Get the session manually instead of using middleware
const session = await auth()
import { auth } from '@/auth'

const app = new Hono()
// Protect API for authenticated users only
.get('/', async (c) => {
// Get the session manually instead of using middleware
const session = await auth()
Adesh
AdeshOP•2mo ago
Yeah it is same auth, and it works for the client components, Even on using the verifyAuth middleware from @hono/auth-js I was getting the same behavior
ambergristle
ambergristle•2mo ago
huh some nextjs magic ig
Adesh
AdeshOP•2mo ago
Why is that I get the session on server component, const session = await auth() But not on the Hono route?
ambergristle
ambergristle•2mo ago
sorry, i'm confused - you've got a server-rendered page Posts, that's trying to prefetch some data by hitting a hono endpoint - and some client components *on the Posts page that are then trying to fetch the same data from the same hono endpoint right?
Adesh
AdeshOP•2mo ago
Yeah And I have a stale time of 10 mins, so if the server has done the fetching, client can just use it
ambergristle
ambergristle•2mo ago
- and the prefetch on Posts is working - but the fetches from the client components aren't
Adesh
AdeshOP•2mo ago
No prefetch is only working if I remove the session check logic. Fetches from the client components works regardless But client won't fetch if there's fresh data in the cache So if prefetch from the server works, (which is not since I'm not able to get the session info) Clients can just use that data leading to faster page loads
ambergristle
ambergristle•2mo ago
but you're saying that you get the session when SSRing Posts, but not on the hono route (when making fetches)?
Adesh
AdeshOP•2mo ago
Yeah I do get that in the server component, but when making the prefetch request to the hono route, It is not available then The session check in the server component to validate if the page should render or not. But the session check for the api is for when someone hits the backend endpoint from say postman, (It is here where it is not workgin) I get the session here
const Posts = async () => {
const session = await auth()

if (!session?.user) {
return <Unauthorized />
}

const queryClient = new QueryClient()
await queryClient.prefetchQuery(getPostQueryOptions)

return (
<>
<Toaster />
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
Posts
</h1>
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList />
</HydrationBoundary>
<div>
<CreatePostForm userId={session?.user.id || ''} />
</div>
</>
)
}
const Posts = async () => {
const session = await auth()

if (!session?.user) {
return <Unauthorized />
}

const queryClient = new QueryClient()
await queryClient.prefetchQuery(getPostQueryOptions)

return (
<>
<Toaster />
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
Posts
</h1>
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList />
</HydrationBoundary>
<div>
<CreatePostForm userId={session?.user.id || ''} />
</div>
</>
)
}
But not here
const app = new Hono()
// Protect API for authenticated users only
.get('/', async (c) => {
// Get the session manually instead of using middleware
const session = await auth()

// Check authentication
if (!session?.user) {
return c.json({ error: 'Unauthorized' }, 401)
}

const posts = await db.query.posts.findMany({})
return c.json(posts)
})
const app = new Hono()
// Protect API for authenticated users only
.get('/', async (c) => {
// Get the session manually instead of using middleware
const session = await auth()

// Check authentication
if (!session?.user) {
return c.json({ error: 'Unauthorized' }, 401)
}

const posts = await db.query.posts.findMany({})
return c.json(posts)
})
ambergristle
ambergristle•2mo ago
how is the hono app getting served? are you using next actions? also, what's going on here?
session({ session, user }) {
session.user.id = user.id
return session
},
session({ session, user }) {
session.user.id = user.id
return session
},
Adesh
AdeshOP•2mo ago
No I have a catch all api route
import provider from '@/auth.config'
import posts from '@/server/posts'
import { AuthConfig, initAuthConfig } from '@hono/auth-js'
import { Hono } from 'hono'
import { handle } from 'hono/vercel'

export const runtime = 'edge'

const app = new Hono().basePath('/api')

app.use('*', initAuthConfig(getAuthConfig))

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const route = app.route('/posts', posts)

export const GET = handle(app)
export const POST = handle(app)
export const PUT = handle(app)
export const PATCH = handle(app)
export const DELETE = handle(app)

function getAuthConfig(): AuthConfig {
return {
secret: process.env.AUTH_SECRET,
...provider,
}
}

// this export will be used by the RPC client
export type AppType = typeof route
import provider from '@/auth.config'
import posts from '@/server/posts'
import { AuthConfig, initAuthConfig } from '@hono/auth-js'
import { Hono } from 'hono'
import { handle } from 'hono/vercel'

export const runtime = 'edge'

const app = new Hono().basePath('/api')

app.use('*', initAuthConfig(getAuthConfig))

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const route = app.route('/posts', posts)

export const GET = handle(app)
export const POST = handle(app)
export const PUT = handle(app)
export const PATCH = handle(app)
export const DELETE = handle(app)

function getAuthConfig(): AuthConfig {
return {
secret: process.env.AUTH_SECRET,
...provider,
}
}

// this export will be used by the RPC client
export type AppType = typeof route
ambergristle
ambergristle•2mo ago
hows that getting served then?
Adesh
AdeshOP•2mo ago
This is telling next-auth to extend the session to include the user id as well, by default it doesn't https://authjs.dev/guides/extending-the-session
Auth.js | Extending The Session
Authentication for the Web
Adesh
AdeshOP•2mo ago
Next.js has api routes that runs on the same port, I am hijacking, that to use hono instead
Adesh
AdeshOP•2mo ago
Vercel - Hono
Web framework built on Web Standards for Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Node.js, and others. Fast, but not only fast.
ambergristle
ambergristle•2mo ago
gotcha have you tried something like this?
const app = new Hono()
.use('*'), async (c, next) => {
const session = await auth(c.req.raw)
})
const app = new Hono()
.use('*'), async (c, next) => {
const session = await auth(c.req.raw)
})
ig you're saying that the backend is getting the session when it's been set on client-side requests
Adesh
AdeshOP•2mo ago
Just tried it, but still the same
ambergristle
ambergristle•2mo ago
so the cookie is available on the request when it hits Posts, but maybe isn't getting attached to the prefetch request?
Adesh
AdeshOP•2mo ago
Yeah
ambergristle
ambergristle•2mo ago
one thing to call out is that you don't really need to be fetching at all unless you've got specific project requirements
Adesh
AdeshOP•2mo ago
Yeah it is just an optimization, for better UX. I'll keep looking for solutions though, I'll update here if something works
ambergristle
ambergristle•2mo ago
i mean you don't need to hit another backend you're already on the server, loading the page. instead of hitting another backend endpoint for the data, just call the same logic that the endpoint calls directly
Adesh
AdeshOP•2mo ago
Yeah, I would have, but the for the react query cache to used, by the client, We need to call the same queryFn and with same queryKey at both the places. There is a way to fetch the data directly and pass the result to the client component cache as initial data, But that only works for statically rendered pages
ambergristle
ambergristle•2mo ago
gotcha, interesting i'm surprised the queryFn needs to be the same. that's a shame what's your fetch function look like? oh. hold up
Adesh
AdeshOP•2mo ago
export async function getPosts(): Promise<
Array<Zod.infer<typeof selectPostSchema>>
> {
// const response = await fetch('/api/posts')
const response = await client.api.posts.$get()

if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`)
}

return response.json()
}
export async function getPosts(): Promise<
Array<Zod.infer<typeof selectPostSchema>>
> {
// const response = await fetch('/api/posts')
const response = await client.api.posts.$get()

if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`)
}

return response.json()
}
ambergristle
ambergristle•2mo ago
for science, can you log in your hono app
import { headers } from 'next/headers'

// in handler
console.log(headers())
import { headers } from 'next/headers'

// in handler
console.log(headers())
just thinking through how the cookies are get/set/passed around i don't really know how next manages requests contexts are you injecting the next.js fetch?
Adesh
AdeshOP•2mo ago
It is a really long trail of logs
ambergristle
ambergristle•2mo ago
thats ok but we're really just looking for the cookies, ig
Adesh
AdeshOP•2mo ago
hmm, we do have the cookies there authjs.session-token=7454b460-84ba-40fb-b42e-773e7ec14b71; authjs.csrf-token=3955a742945c7d4e5eba846e96d95ed7621e1b31bde38156591bf59d29e73888%7C7c07efc9cd83ae19ede691973019d08973f2b0ab079c68f0a2e53712db45445c; authjs.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fposts But for the first call which was from the server component, it is null
ambergristle
ambergristle•2mo ago
interesting seems like you might need to manually forward headers: https://nextjs.org/docs/app/api-reference/functions/headers#using-the-authorization-header
Adesh
AdeshOP•2mo ago
Ohk, let me try
ambergristle
ambergristle•2mo ago
i wonder if the authjs csrf is playing a role this might be relevant: https://github.com/nextauthjs/next-auth/discussions/7256
Adesh
AdeshOP•2mo ago
@ambergristle got it, Sorry for the misconception, queryFn can be different. This does the trick
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: async () => {
const posts = await db.query.posts.findMany({})
return posts
},
staleTime: 10 * 1000,
})
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: async () => {
const posts = await db.query.posts.findMany({})
return posts
},
staleTime: 10 * 1000,
})
I am directly querying the db Thank you so much for your time and efforts, really appreciate it 🤗
ambergristle
ambergristle•2mo ago
legoooooooo happy to help!

Did you find this page helpful?