nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.17k stars 3.35k forks source link

no redirection after successful login #10016

Open 78raoul78 opened 7 months ago

78raoul78 commented 7 months ago

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

  1. Checkout the repository
  2. Create an .env file in apps/www and paste inside the content from app/www/.env.example (put your own AUTH_SECRET and the url of your POSTGRES_URL)
  3. Go to apps/www and run pnpm install
  4. then pnpm run seed
  5. then pnpm build
  6. then pnpm dev
  7. Go to http://localhost:3001
  8. You should be redirected to the signIn page http://localhost:3001/signIn?callbackUrl=http%3A%2F%2Flocalhost%3A3001%2F
  9. submit a valid account credentials : raoulcheck@flexup.com / 123456
  10. here is the issue : instead of being redirected to http://localhost:3001, you're being redirected to http://localhost:3001/signIn?callbackUrl=http%3A%2F%2Flocalhost%3A3001%2F , whats the interest of the callbackUrl if I'm not redirected to it πŸ€”

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

nauvalyusufaddairy commented 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.

schilffarth commented 6 months ago

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.

schilffarth commented 6 months ago

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?

schilffarth commented 6 months ago

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.

umar-brackets commented 6 months ago

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 (

{children} {/* SignOut */} {pathname === '/account/profile' && (
)} {/* Delete Dependent */} {pathname?.includes('/account/dependents/bio') && (
)} {/*
*/}
{/*
*/}

) }

/////////////////////////////////////////////////////////////////////////////////////////////////////////////// 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',

18601673727 commented 6 months ago

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..

lespons commented 3 months ago

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="..">
...
weijyun9008 commented 3 months ago

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.

adityav477 commented 3 months ago

no updates yet ?!?

forced to turn the page back to server component

rhiskey commented 3 months ago

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.

It works, thanks! Saved my life!

rhiskey commented 3 months ago

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 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

2. Get user in any server component then pass to client component

[!IMPORTANT]
For example, i use <Navbar/> in my layout.tsx. I have In Navbar user button AuthButton , 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 case AuthButton).

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

wzrdx commented 1 month ago

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.

Using this component, the session is not even updated after reloading lol.

This cannot possibly work as it does the exact same thing useSession().

wzrdx commented 1 month ago

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 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

2. Get user in any server component then pass to client component

Important

For example, i use <Navbar/> in my layout.tsx. I have In Navbar user button AuthButton , 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 case AuthButton).

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?

bartoszhernas commented 1 week ago

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) : '/');
  }
ritik3236 commented 1 week ago

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.'}};
    }
}
Linger7 commented 4 days ago

Still facing this issue with the latest version of NextAuth and NextJs

raghav-kokotree commented 1 day ago

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.

On the Login Page (Page)

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>
  );
}

Passing the 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>
  );
}

Handling the Login Action (Server Component)

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);
  }
}

Redirect After Login

With this code, after a successful login, the user will be redirected to the callbackUrl:

const decodedCallbackUrl = callbackUrl
  ? decodeURIComponent(callbackUrl)
  : '/';
redirect(decodedCallbackUrl);