Open 78raoul78 opened 7 months ago
There are a lot of bugs in version 5. It better try to create an auth flow from scratch or rollback to v4.
I am not an official maintainer, but I think you have mistaken callback url with redirect url. The callback is for auth, you need to do redirect after you are logged in. Here's my code:
import { signIn } from "next-auth/react"; import { useRouter } from "next13-progressbar"; import { toast } from "sonner"; export default function Login() { const router = useRouter(); const onSubmit = async (values: z.infer<typeof formSchema>) => { setIsLoading(true); const { email, password } = values; try { const resp = await signIn("credentials", { email, password, redirect: false, }); if (!resp || resp.error) { toast.error(`Login in failed: ${resp?.error}` || "unkonwn error"); return; } toast.success("woohoo"); const redirect = searchParams.get("redirect"); if (redirect) { router.push(redirect); } else { router.push("/home"); } } catch (error) { toast.error(`Login in failed: ${error}` || "unkonwn error"); return; } finally { setIsLoading(false); } }; ...
I may be wrong. But you can give this a try.
With this could you probably have /had a similar issue to mine:
After successful login, the app's session isn't updated. The session in my root layout.tsx is logging the correct session data in server side rendering, but useSession
on client side remains undefined.
A manual page refresh will fix this issue, client side useSession gets uptodate session data.
Anyone familiar with this issue? I do not want to perform a whole page refresh upon login / logout. This sucks.
I think first of all you shouldnt use
getSession()
in layout. And the client side session should be updated, I recently did a twitter login and it works:import { signIn, signOut, useSession } from "next-auth/react"; export default function ConnectAccount() { const { data, status } = useSession(); if (status === "authenticated") { return ( <> <Button className="w-full" variant={"outline"} onClick={() => signOut()} > xxx </>); } else { return ( <Button className="w-full" variant={"outline"} onClick={() => signIn("twitter")} > <Image src="/icons/X_logo.svg" width={20} height={20} alt="X" /> </Button> ); }
and this signIn works perfectly good. If you could share your code We can probably figure out something.
My session is initialized / fetched in the root layout with const session = await auth()
, using next auth beta v11 right now.
This session is passed to the SessionProvider.
In i.e. the AppBar / NavBar, where I have a user button or a login button based on session status, I retrieve the session with useSession()
.
When I log the session
in my root layout.tsx, it logs correctly the uptodate sssiondata on server side in my ide console.
But a log in the AppBar where I use useSession remains undefined, even if the root layout logs the correct values.
Upon manual page refresh (or window.location.refresh) the session is passed down correctly and my AppBar useSession returns the proper data.
Recently I discovered that the same issue exists for the auth logout. If redirect is set to false, the logout doesn't actually happen on client side and the session isn't updated until manual page refresh.
The whole situation is all about these annoying page refreshes. I do not wanna reload the entire app upon login/logout, i just want the ui to represent the actual session as anyone would expect.
I will share code later when I get home, unless you have any ideas based on this description already?
I actually finally made it work thanks to another very recent github issue (cannot find it anymore tho lol)
The underlying issue was that next-auth
's session provider doesn't update the session passed from auth() in client side, it only happens upon page refresh.
The solution was to build a custom SessionProvider
that utilizes getSession()
instead of auth()
and passing it as a state onto next-auth's
SessionProvider
:
"use client";
import { Session } from "next-auth";
import {
SessionProvider as NextSessionProvider,
getSession
} from "next-auth/react";
import { usePathname } from "next/navigation";
import {
ReactNode,
useCallback,
useEffect,
useState
} from "react";
// Retrieve user session for the app's session context
export default function SessionProvider({
children
}: {
children: ReactNode;
}) {
const [ session, setSession ] = useState<Session | null>(null);
const pathName = usePathname();
const fetchSession = useCallback(async () => {
try {
const sessionData = await getSession();
setSession(sessionData);
} catch (error) {
setSession(null);
if (process.env.NODE_ENV === "development") {
console.error(error);
}
}
}, []);
useEffect(() => {
fetchSession().finally();
}, [fetchSession, pathName]);
return (
<NextSessionProvider session={session}>
{children}
</NextSessionProvider>
);
}
Now I can use useSession()
from next-auth
just normally and I am returned the up-to-date session data after login. Now, I do not have to reload the entire app upon login / logout!!!
I am very happy as I finally figured this out, it's been plagueing me for almost 2 months by now.
I am facing something like you The code i have provided works perfectly in development, when i signup it redirects to welcome page and clicking on logo on the header where path is ' / ' it goes to home with no external reload but when i create build and start build, after signup/create new user it redirects to welcome page but clicking on logo with path ' / ' it redirects to login page instead of home untill i reload the page. And it just happens in the production after creating build. Please provide solution for it. Thanks
This is my root layout code
export default function RootLayout({ children, session }: IProps) { const pathname = usePathname()
const handleSignOut = async () => { await signOut() }
return (
) }
/////////////////////////////////////////////////////////////////////////////////////////////////////////////// and this is the middleware code where i am checking the session and accordingly redirecting to different pages
import { NextResponse } from 'next/server' import { getSession } from 'next-auth/react'
export async function middleware(request: any) { const path = request.nextUrl.pathname // const session = await getSession({ req: request }) const cookie = request.headers.get('cookie') const session: any = cookie ? await getSession({ req: { headers: { cookie } } as any }) : null
// Check if session exists or not
if (!session) {
// If not authenticated, check if the user is trying to access /login or /signup
if (path === '/login' || path.includes('/signup') || path === '/forgot') {
// Allow access to /login and /signup for unauthenticated users
return NextResponse.next()
}
// Redirect to the login page for any other unauthenticated requests
return NextResponse.redirect(new URL(/login
, request.url))
}
// If session exists and trying to access /login or /signup, redirect to the home page
if (path === '/login' || path.includes('/signup') || path === '/forgot') {
return NextResponse.redirect(new URL(/
, request.url))
}
// Allow access to any other routes during the session
return NextResponse.next()
}
export const config = { unstable_allowDynamic: ['/node_modules/@babel//*/.js'], matcher: [ '/', '/login', '/signup', '/forgot', '/signup/account', '/visit/:path', '/add-dependent', '/add-pharmacy', '/add-credit-card', '/account', '/account/:path', '/follow-up',
Well I think by their documentation:
You will likely not need useSession if you are using the Next.js App Router.
So we are not supposed to useSession
or getSession
at the first place, but to router.reload() after each signIn()
, to allow our const session = await auth()
doing its job properly, let alone confuse ourselves even more with useEffect + Context
approaches..
with server action I use
'use server';
import { signIn } from '@/lib/auth';
import { AuthError } from 'next-auth';
import { redirect } from 'next/navigation';
export async function authenticate(_: string | undefined, formData: FormData) {
try {
await signIn('credentials', formData);
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return 'Invalid credentials.';
default:
return 'Something went wrong.';
}
}
throw error;
} finally {
redirect('/'); // or your pathname
}
}
and client
...
const [errorMessage, dispatch] = useFormState(authenticate, undefined);
return (
<form action={dispatch} className="..">
...
with server action I use
'use server'; import { signIn } from '@/lib/auth'; import { AuthError } from 'next-auth'; import { redirect } from 'next/navigation'; export async function authenticate(_: string | undefined, formData: FormData) { try { await signIn('credentials', formData); } catch (error) { if (error instanceof AuthError) { switch (error.type) { case 'CredentialsSignin': return 'Invalid credentials.'; default: return 'Something went wrong.'; } } throw error; } finally { redirect('/'); // or your pathname } }
and client
... const [errorMessage, dispatch] = useFormState(authenticate, undefined); return ( <form action={dispatch} className=".."> ...
Placing redirect('/')
inside the finally
block causes a redirection in all cases, even when an error occurs, preventing the errorMessage
from being displayed on the form. To fix this, you can add a condition check:
export async function authenticate(
prevState: string | undefined,
formData: FormData,
) {
let errorOccurred = false;
try {
await signIn('credentials', formData);
} catch (error) {
if (error instanceof AuthError) {
errorOccurred = true;
switch (error.type) {
case 'CredentialsSignin':
return 'Invalid credentials.';
default:
return 'Something went wrong.';
}
}
throw error;
} finally {
if (!errorOccurred) {
redirect('/your-pathname');
}
}
}
Note that errorOccurred = true;
is placed inside the if
clause because only AuthError
is considered a valid error in this context.
no updates yet ?!?
forced to turn the page back to server component
I actually finally made it work thanks to another very recent github issue (cannot find it anymore tho lol)
The underlying issue was that
next-auth
's session provider doesn't update the session passed from auth() in client side, it only happens upon page refresh.The solution was to build a custom
SessionProvider
that utilizesgetSession()
instead ofauth()
and passing it as a state ontonext-auth's
SessionProvider
:"use client"; import { Session } from "next-auth"; import { SessionProvider as NextSessionProvider, getSession } from "next-auth/react"; import { usePathname } from "next/navigation"; import { ReactNode, useCallback, useEffect, useState } from "react"; // Retrieve user session for the app's session context export default function SessionProvider({ children }: { children: ReactNode; }) { const [ session, setSession ] = useState<Session | null>(null); const pathName = usePathname(); const fetchSession = useCallback(async () => { try { const sessionData = await getSession(); setSession(sessionData); } catch (error) { setSession(null); if (process.env.NODE_ENV === "development") { console.error(error); } } }, []); useEffect(() => { fetchSession().finally(); }, [fetchSession, pathName]); return ( <NextSessionProvider session={session}> {children} </NextSessionProvider> ); }
Now I can use
useSession()
fromnext-auth
just normally and I am returned the up-to-date session data after login. Now, I do not have to reload the entire app upon login / logout!!!I am very happy as I finally figured this out, it's been plagueing me for almost 2 months by now.
It works, thanks! Saved my life!
I found solution without modifying SessionProvider. You have to properly split codebase to server/client part. According Next 14 App Router guidelines.
I use
"next-auth": "^5.0.0-beta.19",
"next": "^14.2.3",
You have to properly split codebase to server/client part. According to Next 14 App Router guidelines.
So, guys, I got your back!
current-session.ts
in lib
folder..
βββ ...
βββ app
β βββ components # here you can store client side components
βββ lib
lib/current-session.ts
import { auth } from "@/auth";
/**
* Retrieves the current user from the authentication session.
*
* @return {Promise<User | undefined>} The current user if available, otherwise undefined.
*/
export const currentUser = async () => {
const session = await auth();
return session?.user;
};
This is our server side session user. Use in any component that supposed to work as server component (by default it is). For example layout.tsx or page.tsx
[!IMPORTANT]
For example, i use<Navbar/>
in mylayout.tsx
. I have In Navbar user buttonAuthButton
, so to actually update state of this button (load avatar, nickname, email) after sign-in or sign-up without refreshing page simply pass user to client component (in this caseAuthButton
).
app/_components/Navbar.tsx
. I prefer to store server-side components in suffix-like folders _components
import {currentUser } from "@/lib/current-session";
export const Navbar = async () => {
// Get user from server side in layout (for example navbar)
const user = await currentUser();
return (
<AuthButton user={user} />
);
};
AuthButton.tsx
client component and get data from parent server component (Navbar.tsx
).app/components/AuthButton.tsx
"use client";
interface AuthButtonProps {
user?: User
}
export const AuthButton = ({
user,
}: AuthButtonProps) => {
if (user) {
return (
<div>
<span>{user.image}</span>
<p>{user.email}</p>
</div>
);
}
return (
<p>Not logged in</p>
);
};
Where user is type of default User. You can extend this via database model
type User = {
id: string;
image: string;
email: string;
// etc...
}
Hope it helps, please write back if this works for you @weijyun9008 @lespons @78raoul78
I actually finally made it work thanks to another very recent github issue (cannot find it anymore tho lol)
The underlying issue was that
next-auth
's session provider doesn't update the session passed from auth() in client side, it only happens upon page refresh.The solution was to build a custom
SessionProvider
that utilizesgetSession()
instead ofauth()
and passing it as a state ontonext-auth's
SessionProvider
:"use client"; import { Session } from "next-auth"; import { SessionProvider as NextSessionProvider, getSession } from "next-auth/react"; import { usePathname } from "next/navigation"; import { ReactNode, useCallback, useEffect, useState } from "react"; // Retrieve user session for the app's session context export default function SessionProvider({ children }: { children: ReactNode; }) { const [ session, setSession ] = useState<Session | null>(null); const pathName = usePathname(); const fetchSession = useCallback(async () => { try { const sessionData = await getSession(); setSession(sessionData); } catch (error) { setSession(null); if (process.env.NODE_ENV === "development") { console.error(error); } } }, []); useEffect(() => { fetchSession().finally(); }, [fetchSession, pathName]); return ( <NextSessionProvider session={session}> {children} </NextSessionProvider> ); }
Now I can use
useSession()
fromnext-auth
just normally and I am returned the up-to-date session data after login. Now, I do not have to reload the entire app upon login / logout!!!I am very happy as I finally figured this out, it's been plagueing me for almost 2 months by now.
Using this component, the session is not even updated after reloading lol.
This cannot possibly work as it does the exact same thing useSession().
I found solution without modifying SessionProvider. You have to properly split codebase to server/client part. According Next 14 App Router guidelines.
I use
"next-auth": "^5.0.0-beta.19", "next": "^14.2.3",
You have to properly split codebase to server/client part. According to Next 14 App Router guidelines.
So, guys, I got your back!
Follow these steps
1. Create
current-session.ts
inlib
folder.. βββ ... βββ app β βββ components # here you can store client side components βββ lib
lib/current-session.ts
import { auth } from "@/auth"; /** * Retrieves the current user from the authentication session. * * @return {Promise<User | undefined>} The current user if available, otherwise undefined. */ export const currentUser = async () => { const session = await auth(); return session?.user; };
This is our server side session user. Use in any component that supposed to work as server component (by default it is). For example layout.tsx or page.tsx
2. Get user in any server component then pass to client component
Important
For example, i use
<Navbar/>
in mylayout.tsx
. I have In Navbar user buttonAuthButton
, so to actually update state of this button (load avatar, nickname, email) after sign-in or sign-up without refreshing page simply pass user to client component (in this caseAuthButton
).
app/_components/Navbar.tsx
. I prefer to store server-side components in suffix-like folders_components
import {currentUser } from "@/lib/current-session"; export const Navbar = async () => { // Get user from server side in layout (for example navbar) const user = await currentUser(); return ( <AuthButton user={user} /> ); };
3. Create
AuthButton.tsx
client component and get data from parent server component (Navbar.tsx
).
app/components/AuthButton.tsx
"use client"; interface AuthButtonProps { user?: User } export const AuthButton = ({ user, }: AuthButtonProps) => { if (user) { return ( <div> <span>{user.image}</span> <p>{user.email}</p> </div> ); } return ( <p>Not logged in</p> ); };
Where user is type of default User. You can extend this via database model
type User = { id: string; image: string; email: string; // etc... }
Hope it helps, please write back if this works for you @weijyun9008 @lespons @78raoul78
Doesn't this cause your whole app to be dynamically rendered, instead of having static & dynamic pages?
I had the same issue with custom login page, adding this to custom login page fixed the issue for me (or hid it)
const session = await auth();
if (session) {
redirect(searchParams?.callbackUrl ? decodeURIComponent(searchParams.callbackUrl) : '/');
}
I solved issue in server action by adding isRedirectError, in my actions/auth.ts file
export async function doLogin(formData: z.infer<typeof signInSchema>, callbackUrl = DEFAULT_LOGIN_REDIRECT) {
try {
....
await signIn('credentials', {
redirectTo: callbackUrl,
email: parsedCredentials.data.email,
password: parsedCredentials.data.password,
remember: parsedCredentials.data.remember,
});
} catch (e: unknown) {
if (isRedirectError(e)) throw e;
....
const nextError = e as AuthError;
const error = nextError.cause?.err as CustomError;
// Handle unexpected CustomError types
return {success: false, error: {message: 'An unexpected error occurred.'}};
}
}
Still facing this issue with the latest version of NextAuth and NextJs
Hereβs the workaround I implemented for handling the redirection along with the callback URL after login. I would appreciate feedback from the community and the developers of NextAuth.js to confirm whether this is a good approach.
import { useSearchParams } from 'next/navigation';
export default function LoginPage() {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl');
return (
<main className="flex items-center justify-center md:h-screen">
{/* Other components */}
<LoginForm callbackUrl={callbackUrl} />
{/* Other components */}
</main>
);
}
callbackUrl
to the Login Form (Component)export default function LoginForm({
callbackUrl,
}: {
callbackUrl: string | null;
}) {
const [errorMessage, formAction, isPending] = useActionState(
(prevState: string | undefined, formData: FormData) =>
myLogin(prevState, formData, callbackUrl),
undefined,
);
return (
<form action={formAction}>
{/* Form fields go here */}
</form>
);
}
export async function myLogin(
prevState: string | undefined,
formData: FormData,
callbackUrl: string | null,
) {
try {
await signIn('credentials', formData);
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return 'Invalid credentials.';
default:
return 'Something went wrong.';
}
}
throw error;
} finally {
const decodedCallbackUrl = callbackUrl
? decodeURIComponent(callbackUrl)
: '/';
redirect(decodedCallbackUrl);
}
}
With this code, after a successful login, the user will be redirected to the callbackUrl
:
const decodedCallbackUrl = callbackUrl
? decodeURIComponent(callbackUrl)
: '/';
redirect(decodedCallbackUrl);
Environment
System: OS: macOS 14.1.2 CPU: (8) arm64 Apple M2 Memory: 489.31 MB / 16.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 20.7.0 - /opt/homebrew/bin/node Yarn: 1.22.21 - /opt/homebrew/bin/yarn npm: 10.1.0 - /opt/homebrew/bin/npm pnpm: 8.14.0 - /opt/homebrew/bin/pnpm Browsers: Chrome: 121.0.6167.160 Safari: 17.1.2 npmPackages: next: 14.1.0 => 14.1.0 react: 18.2.0 => 18.2.0
Reproduction URL
https://github.com/78raoul78/ui
Describe the issue
After a successful signIn, I'm not getting redirected to my home page even though the callbackUrl is correctly set.
How to reproduce
Expected behavior
After a successful login, I'm expecting to be redirected to the initial url (the one before the redirection) which is localhost:3001