Search code examples
javascriptnext.jsnext-auth

When authenticating via credentials using NextAuth why does the address bar not update automatically to the correct path on success?


How am I able to have the url path refresh automatically on a successful credential log in?

I was trying to create credential authentication similar to what Next lays out in their tutorial here. I'm only using email as the authentication though.

I am able to log in successfully but the path in the address bar doesn't update. It should go from /login to /dashboard. It only updates after manually refreshing the page. Without an update I am unable to sign out after clicking "sign out". I am able to sign out only if I were to refresh the page manually and the address changes. You can see this in the gif below.

My folder structure can be seen below.

enter image description here

// ./auth.config.ts

import type { NextAuthConfig } from 'next-auth';

export const authConfig = {
  pages: {
    signIn: '/login',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      } else if (isLoggedIn) {
        return Response.redirect(new URL('/dashboard', nextUrl));
      }
      return true;
    },
  },
  providers: [],
} satisfies NextAuthConfig;
// ./middleware.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';

export default NextAuth(authConfig).auth;

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};
// ./auth.ts

import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { authConfig } from './auth.config';
import type { User } from '~/lib/definitions';

async function getUser(email: string): Promise<User | undefined> {
  try {
    const user = await sql<User>`SELECT * FROM users WHERE email=${email}`;
    return user.rows[0];
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw new Error('Failed to fetch user.');
  }
}

export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = z
          .object({ email: z.string().email() })
          .safeParse(credentials);

        if (parsedCredentials.success) {
          const { email } = parsedCredentials.data;
          const user = await getUser(email);
          if (user) return user;
          return null;
        }
        return null;
      },
    }),
  ],
});

// ./app/lib/actions.ts

'use server';

import { AuthError } from 'next-auth';
import { signIn } from '~/../auth';

export async function authenticate(
  prevState: 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;
  }
}
// ./app/login/page.tsx

'use client';

import { useFormState, useFormStatus } from 'react-dom';
import { authenticate } from '~/lib/actions';

export default function LoginPage() {
  const [errorMessage, dispatch] = useFormState(authenticate, undefined);

  return (
    <main>
      <form action={dispatch}>
        <div>
          <label htmlFor="email">
            Email
          </label>
          <div>
            <input
              id="email"
              type="email"
              name="email"
              placeholder="Enter your email address"
              required
            />
          </div>
        </div>
        <LoginButton />
      </form>
    </main>
  );
}

function LoginButton() {
  const { pending } = useFormStatus();

  return (
    <button aria-disabled={pending}>
      Sign in
    </button>
  );
}
// ./app/dashboard/page.tsx

import { signOut } from '~/../auth';

export default function Page() {
  return (
    <main>
      <h1>Dashboard</h1>
      <p>Dashboard content goes here</p>
      <form
        action={async () => {
          'use server';
          await signOut({ redirectTo: '/login' });
        }}
      >
        <button>
          <div>Sign Out</div>
        </button>
      </form>
    </main>
  );
}

enter image description here


Solution

  • After sleepless nights and getting more grays, the solution that I have found and works for me is to update the formData by adding in a redirect path.

    // ./app/lib/actions.ts
    
    'use server';
    
    import { AuthError } from 'next-auth';
    import { signIn } from '~/../auth';
    
    export async function authenticate(
      prevState: string | undefined,
      formData: FormData
    ) {
      formData.set('redirectTo', '/dashboard'); // <-- this solved the issue
      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;
      }
    }