-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How do I access my access_token in a server component? #7913
Comments
I tried to get the token from cookies() in my server components and log them. I must've been doing something wrong in the callbacks to store 2 of them. However, both signatures are invalid according to jwt.io when i copy them there.
|
check out the docs https://next-auth.js.org/configuration/nextjs#in-app-directory |
The docs don't really cover this question (as far as I can see). They show how to get the session, sure, but not how to get the access token in a server component. |
Sorry to open this thread back up - I am in the same exact scenario currently and have not found a viable solution. I would prefer to not have to access the cookies to extract the JWT within the server component. Has anyone been able to find a way to get this done? On my client component it returns all of the properties i've defined in the session callback, but in the server component, getServerSession only returns the user object with |
I managed to get the access_token by creating a callback in the authOption object: // app/api/auth/[...nextauth]/route.ts
const authOptions: NextAuthOptions = {
providers: [
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID ?? '',
clientSecret: process.env.AZURE_AD_CLIENT_SECRET ?? '',
tenantId: process.env.AZURE_AD_TENANT_ID,
}),
],
session: {
strategy: 'jwt', // <-- make sure to use jwt here
maxAge: 30 * 24 * 60 * 60,
},
callbacks: {
jwt: async ({ token, user, account }) => {
if (account && account.access_token) {
token.accessToken = account.access_token // <-- adding the access_token here
}
return token
},
},
secret: process.env.NEXTAUTH_SECRET,
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST } Now you should be able to see the accessToken when calling getToken() // app/api/test/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getToken } from 'next-auth/jwt'
export async function GET(req: NextRequest) {
const token = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET ?? '',
})
console.log('token.accessToken: ', token?.accessToken)
return NextResponse.json({})
} |
did you manage anyway to solve this |
|
@Yazan-Ali-01 https://next-auth.js.org/getting-started/client#updating-the-session |
this would update the server session or the client session ?? |
I have made some changes, so my import NextAuth from 'next-auth/next'
import { authOptions } from '@/auth'
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST } the // /auth.ts
import {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
} from 'next'
import { NextAuthOptions, getServerSession } from 'next-auth'
import AzureADProvider from 'next-auth/providers/azure-ad'
const authOptions: NextAuthOptions = {
providers: [
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID ?? '',
clientSecret: process.env.AZURE_AD_CLIENT_SECRET ?? '',
tenantId: process.env.AZURE_AD_TENANT_ID,
}),
],
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
jwt: async ({ token, user, account }) => {
if (account && account.access_token) {
// set access_token to the token payload
token.accessToken = account.access_token
}
return token
},
redirect: async ({ url, baseUrl }) => {
return baseUrl
},
session: async ({ session, token, user }) => {
// If we want to make the accessToken available in components, then we have to explicitly forward it here.
return { ...session, token: token.accessToken }
},
},
pages: {
signIn: '/',
},
secret: process.env.NEXTAUTH_SECRET,
}
function auth( // <-- use this function to access the jwt from React components
...args:
| [GetServerSidePropsContext['req'], GetServerSidePropsContext['res']]
| [NextApiRequest, NextApiResponse]
| []
) {
return getServerSession(...args, authOptions)
}
export { authOptions, auth } You can now import the auth function from above, and use it inside components // /app/[locale]/(auth)/dashboard/overview/page.tsx
import { auth } from '@/auth'
export default async function Page() {
const session = await auth()
console.log('jwt: ', session?.token)
return <></>
} |
This works! But have you made use of refresh token rotation? My IdentityProvider puts an short expire time on the token so its useless after 5min, and next auth doesn't automatically refresh the access token when the it expires. Feels like its something that should be handle automatically as long as the session is still valid. |
@linus-jansson I had the same problem with the refresh token rotation and solved it by adapting the jwt callback. I used the code from the refresh token rotation guide. Additionally, since the callback is only called when the session is created or updated (described in the jwt callback documentation), I created a middleware that calls the I hope this helps with your problem. |
|
Do you have a snippet with the example? Thanks! |
@avillegastorres I used the following code for the jwt callback to refresh the token issued by a keycloak server: // ...
jwt: async ({ token, account }) => {
if (account) {
return {
accessToken: account.access_token || "",
expiresAt: account.expires_at || 0,
refreshToken: account.refresh_token || "",
};
}
if (Date.now() < token.expiresAt * 1000) {
return token;
}
return await refreshToken(token);
},
// ...
async function refreshToken(token: JWT): Promise<JWT> {
try {
const tokenEndpoint = `${
oauthHost + oauthIssuer
}/protocol/openid-connect/token`;
// https://nuxt.com/docs/getting-started/data-fetching#fetch
const response = await $fetch<TokenSet>(tokenEndpoint, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: oauthClient,
client_secret: oauthSecret,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
}),
method: "POST",
});
return {
...token,
accessToken: response.access_token || "",
expiresAt: response.expires_at || 0,
refreshToken: response.refresh_token ?? token.refreshToken,
};
} catch (error) {
console.error("Error refreshing access token", error);
return { ...token, error: "RefreshAccessTokenError" as const };
}
}
declare module "next-auth/jwt" {
interface JWT {
accessToken: string;
expiresAt: number;
refreshToken: string;
error?: "RefreshAccessTokenError";
}
} I also implemented a middleware to call the // https://nuxt.com/docs/guide/directory-structure/server#server-middleware
export default defineEventHandler(async (event) => {
// Trigger JWT callback to refresh token if needed
await getServerSession(event);
}); For my project I used Nuxt 3 with the Sidebase Auth integration which uses |
Be aware that including the access token in the session will also expose it to the client (getSession, useSession...). |
Hey guys, I did a proxy API route on my next.js API folder that brings me the access token. My code looks like this. //This file lives in src/app/api/proxy/route.ts
import { getToken } from "next-auth/jwt";
import { NextRequest, NextResponse } from "next/server";
export async function PATCH(req: NextRequest) {
const token = await getToken({
req,
});
if (!token) {
return NextResponse.json(
{
message: "Unauthorized",
},
{
status: 401,
}
);
}
//The "access_toke" name is up to you; you need to check how you pass it on the JWT callback
return Response.json({ accessToken: token["access_token"] });
} Now, on my server component, I'm calling it like this: // src/app/page.tsx
import { getServerSession } from "next-auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import Landing from "./components/Landing";
import { Locale } from "./lib/i18n/dictionaries/types";
async function IndexPage({ params: { lang } }: { params: { lang: Locale } }) {
const session = await getServerSession();
const token = await fetch("http://localhost:3000/api/proxy", {
method: "PATCH",
headers: headers(), // this is important without, the API will not be able to extract the token from the header
});
console.log(await token.json());
if (!session || !session.user) {
redirect("/login");
}
return <Landing lang={lang} />;
}
export default IndexPage; why I choose thisOk, I decided to do it like this because I want to keep the access token information private from the client; the @joonasjarvinen92 works great if you don't mind that, but in my case, it wasn't an option. I hope the Next Auth introduces a better way to do this; in the meantime, I'll use this. Another note is that you still need the JWT callback for this to work since it is the only way to add the access token into the JWT that NextAuth creates as part of the JWT strategy. References: |
@avillegastorres I really like this approach. As mentioned, it keeps the token on the server and means I have a central location to retrieve the token from my various server components. The issue I have however is although this works locally, I retrieve an error when running in Production (which is on Vercel): It seems that passing the NextJS headers() to the PATCH route handler seems to throw this error 🙄 I can't see anything that is actually modifying the headers, but unfortunately don't have any other ideas on this other than storing the token on the client side which I was hoping to avoid. |
I also got 2 session tokens and was confused by it. It looks like NextAuth is chunking the session cookie it it is too large. You can see it if you turn on debug mode |
So to summarise we have 2 options?
Is there really no better way to get it? |
It seems like the new universal |
In the new docs it says that all should be replaced with Maybe a stupid question, but why is exposing accessToken to the client bad? Isn't it usually stored in the cookies and is encrypted anyway? It seems adding a Route Handler just for getting the I am still waiting for the new docs to give an example or explain how to do it properly in v5. I have two problems:
|
next-auth is awful lol |
It's 2024 and the most popular JS framework and the most popular auth library to go with it does not return the access token, I thought this is trivial. |
I believe the problem got fixed in the newer next auth version with the gloabl auth() |
I faced the same issue, and started a thread on Discord: https://discord.com/channels/1200116961590399008/1257260179683283048 It might help if others provide additional input there. |
On nextjs 14, I got the async jwt({ token, user }): Promise<JWT | null> {
const cookie = cookies();
if (user) {
const accessDecode = jwtDecode(user.accessToken);
const userProfile: User = {
id: token.sub,
name: user.name,
email: user.email,
image: user.image,
accessToken: "",
refreshToken: "",
username: user.username,
};
cookie.set("access-token", user.accessToken, {
expires: accessDecode.exp! * 1000,
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
});
return {
access_token: user.accessToken,
expires_at: accessDecode.exp as number,
refresh_token: user.refreshToken,
user: userProfile,
};
} else if (Date.now() < token.expires_at * 1000) {
return token;
} else {
if (!token.refresh_token) {
cookie.delete("access-token");
throw new Error("Missing refresh token");
}
try {
const { data } = await axios.get(
process.env.API_URL + "/user/generate-new-refresh-token",
{
headers: {
Authorization: token.access_token as string,
refreshToken: token.refresh_token as string,
},
}
);
if (data.error) throw new Error("Bad request");
const accessDecode = jwtDecode(data.data.accessToken);
cookie.set("access-token", data.data.accessToken, {
expires: accessDecode.exp! * 1000,
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
});
return {
// Keep the previous token properties
...token,
access_token: data.data.accessToken,
expires_at: accessDecode.exp as number,
refresh_token: data.data.refreshToken,
};
} catch (error) {
cookie.delete("access-token");
return null;
}
}
},
async session({ session, token }) {
if (token.user) {
// @ts-ignore
session.user = token.user as User;
}
return session;
},
},
session: {
maxAge: 60 * 60,
}, I guess this is quite a messy approach but atleast for now it insures that my access_token is safe in the cookies and is not exposed to the client. Hope, next-auth fixes this soon. :D |
You can use this in Next.js App Router! import { Suspense } from "react"
import { getServerSession } from "next-auth/next"
import { Session } from "@/types/session"
import { authOptions } from "@/lib/auth"
import { cookies } from 'next/headers';
export default async function JobsPage() {
const session: Session | null = await getServerSession(authOptions)
if (!session) {
redirect("/profile") // should not happen due to the middleware
}
+ const cookieStore = cookies();
+ const sessionToken = cookieStore.get('next-auth.session-token');
... |
I made it working by passing nextAuthOptions as parameter in getServerSession function. This should work for you as well. }); |
i'm using this approach in one of my server Actions and it works fine:
|
unfortunately I got error for next 14.2.5 and next-auth 5.0.0-beta.20 when using cookie from next/headers, here's the server log:
so weird the console.log visible on server log but auth said this is not server action .... hmmmmm |
Well you can create a deleteCookie action in action.ts and then call the deleteCookie fn from the catch block. I actually dropped the idea of using next-auth cz i was getting into lot of problems with the beta version and i went ahead and used my own auth management using providers and interceptors. |
@avillegastorres Any idea why I get this error on calling const token = await getToken({
req,
secret
}) Error auth.ts jwt({ token, profile }) {
if (profile) {
token.id = profile.id
token.image = profile.avatar_url || profile.picture
token.accessToken = profile.accessToken
}
return token
},
session: ({ session, token }) => {
if (session?.user && token?.id) {
session.user.id = String(token.id)
}
return session
},
authorized({ auth }) {
return !!auth?.user // this ensures there is a logged in user for -every- request
}
},``` |
Bruv....its crazy we cant easily get the acesstoken after signing in, this package is basically useless |
I'm really shocked how complicated it is, only to get the access token to send it to my backend api in requests. Seems that Auth.js just isn't the right tool for that simple task. |
Solution here. Hope it helps! import { headers, cookies } from 'next/headers' // Obtener headers y cookies // Obtener el token usando getToken if (token == null) { // Firmar el token usando la clave secreta |
All I do was to pass authOptions to await getServerSession(authOptions); If in you session callback your are passing accesstoken from the token to the session then you can just pass authOptions to getServerSession(authOption) and you will get the user object and the accessToken. If this is what you got:
Then just pass:
|
Here's a working method as of October. Previous methods did not work because getToken no longer supports cookies: import { decode } from 'next-auth/jwt'
import { cookies } from "next/headers";
// --- in your function
const sessionTokenName = 'authjs.session-token'; // or 'next-auth.session-token' if using next-auth
const sessionToken = cookies().getAll().find(c => c.name === sessionTokenName);
const token = await decode({
token: sessionToken?.value,
secret: env.NEXTAUTH_SECRET!,
salt: sessionTokenName,
}); |
Be careful with this implementation. If your token is too long, it might be split into 2 parts : Perhaps something like this could be better ? const sessionToken = cookies()
.getAll()
.filter((c) => c.name?.includes(cookieName))
?.map((st) => st.value)
.join('') |
I just made a ssrFetch.ts function in @/lib: import { headers } from 'next/headers';
export async function ssrFetch(
path: string,
options: RequestInit = {}
): Promise<Response> {
const baseUrl = process.env.NEXT_PUBLIC_API_URL
const url = path.startsWith('/') ? `${baseUrl}${path}` : path;
// Retrieve and forward incoming request headers
const incomingHeaders = await headers();
const fetchHeaders = Object.fromEntries(incomingHeaders.entries());
// Merge incoming headers with user-provided options
const finalHeaders = {
...fetchHeaders, // Forward cookies and other incoming headers
'Content-Type': 'application/json', // Default content type
...(options.headers || {}), // Allow user to override or add headers
};
// Perform fetch with merged headers and options
return fetch(url, {
...options,
headers: finalHeaders,
credentials: 'include', // Include cookies for SSR
});
} You can call it the same way as fetch. Just makes SSR pages a bit more like CSR pages. import { ssrFetch } from '@/lib/ssr-fetch';
// Fetch profile data using ssrFetch
const response = await ssrFetch('/api/foo');
const result = await response.json(); I hope this helps. |
Question 💬
I'm logging in using NextAuth with Azure AD as the provider, which works fine.
In the NextAuth callbacks, I receive the access_token. In middleware.ts, I can use the getToken(req, secret) method and access the access_token (but what to do with it there?). In my internal API routes, the getToken(req, secret) always returns null.
I read in another question thread that this is the expected behaviour. These values are not passed to the API routes for security reasons.
NOTE: The only reasons i created the internal API routes was because there I got access to the request object which is needed to use the getToken() method
My question then becomes:
How do I access my JWT token in my server components? And if they are encrypted by next, how do I decode and use them?
My NEXTAUTH_SECRET is created with openssl rand -base64 32 (I dont know if you should say that in public )
How to reproduce ☕️
auth.ts
My server component:
I've also done some attempts to extend the types/interfaces to store the access_token in the callbacks.
Tbh, im not really sure how this works.
next-auth-d.ts:
Contributing 🙌🏽
Yes, I am willing to help answer this question in a PR
The text was updated successfully, but these errors were encountered: