Skip to content
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 to create a custom sign-in page in Auth.js? #9189

Closed
kjetilhartveit opened this issue Nov 19, 2023 · 23 comments
Closed

How to create a custom sign-in page in Auth.js? #9189

kjetilhartveit opened this issue Nov 19, 2023 · 23 comments
Labels
documentation Relates to documentation triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.

Comments

@kjetilhartveit
Copy link

What is the improvement or update you wish to see?

The examples on https://authjs.dev/guides/basics/pages depicting custom sign-in pages "OAuth Sign In" and "Email Sign In" are no longer working because getProviders and getCsrfToken are no longer exported from next-auth/react.

They seem to be marked with @internal, maybe that's why they're not exported?

A type I also have used on the page which is missing is SignInErrorTypes which I previously imported from next-auth/core/pages/signin. This was useful to show the error message if there was an error in the query parameter. Although I wouldn't need the type if I just got an object with error types mapping to the error strings.

Is there any context that might help us understand?

I'm on next auth version 5.0.0-beta.3.

Does the docs page already exist? Please link to it.

https://authjs.dev/guides/basics/pages

@kjetilhartveit kjetilhartveit added documentation Relates to documentation triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime. labels Nov 19, 2023
@rorn-prometeus
Copy link

rorn-prometeus commented Nov 20, 2023

hi. can you show me the code example of all configutation include handle error message using credential provider please
i already implement but i can not catch the error message throw from authorize function.

@kjetilhartveit
Copy link
Author

kjetilhartveit commented Nov 20, 2023

hi. can you show me the code example of all configutation include handle error message using credential provider please i already implement but i can not catch the error message throw from authorize function.

Note I'm posting directly to /api/auth/signin/email etc and aren't using the signIn() function.

The code for the error message looks like this (in getServerSideProps()):

const error = context.query["error"];
// the error messages are copied from somewhere in the NextAuth source code, don't remember where
const errors: Record<SignInErrorTypes, string> = {
	Signin: "Try signing in with a different account.",
	OAuthSignin: "Try signing in with a different account.",
	OAuthCallback: "Try signing in with a different account.",
	OAuthCreateAccount: "Try signing in with a different account.",
	EmailCreateAccount: "Try signing in with a different account.",
	Callback: "Try signing in with a different account.",
	OAuthAccountNotLinked:
		"To confirm your identity, sign in with the same account you used originally.",
	EmailSignin: "The e-mail could not be sent.",
	CredentialsSignin:
		"Sign in failed. Check the details you provided are correct.",
	SessionRequired: "Please sign in to access this page.",
	default: "Unable to sign in.",
};
const errorMessage =
	error && typeof error === "string" && errors[error as SignInErrorTypes];

@rorn-prometeus
Copy link

rorn-prometeus commented Nov 21, 2023

hi. can you show me the code example of all configutation include handle error message using credential provider please i already implement but i can not catch the error message throw from authorize function.

Note I'm posting directly to /api/auth/signin/email etc and aren't using the signIn() function.

The code for the error message looks like this (in getServerSideProps()):

const error = context.query["error"];
// the error messages are copied from somewhere in the NextAuth source code, don't remember where
const errors: Record<SignInErrorTypes, string> = {
	Signin: "Try signing in with a different account.",
	OAuthSignin: "Try signing in with a different account.",
	OAuthCallback: "Try signing in with a different account.",
	OAuthCreateAccount: "Try signing in with a different account.",
	EmailCreateAccount: "Try signing in with a different account.",
	Callback: "Try signing in with a different account.",
	OAuthAccountNotLinked:
		"To confirm your identity, sign in with the same account you used originally.",
	EmailSignin: "The e-mail could not be sent.",
	CredentialsSignin:
		"Sign in failed. Check the details you provided are correct.",
	SessionRequired: "Please sign in to access this page.",
	default: "Unable to sign in.",
};
const errorMessage =
	error && typeof error === "string" && errors[error as SignInErrorTypes];

did you have some experience on set up credentials login with custom page using beta version of authjs ? i set up it but it hard to catch error message when throw from authorize function. it throw error message in console when i throw error from it.

@daniel-farina
Copy link

I agree we need the docs here updated: https://authjs.dev/guides/basics/pages the serversideprops on inside app is not longer possible.

@SmileSydney
Copy link

So is it possible to have a custom signin page given getProviders is not exported in next-auth@5.0.0-beta.3? Does anyone know of the new sample code to use?

@srkuleo
Copy link

srkuleo commented Nov 24, 2023

So is it possible to have a custom signin page given getProviders is not exported in next-auth@5.0.0-beta.3? Does anyone know of the new sample code to use?

I was asking myself the same question. Docs are in really bad shape atm, especially since Next14 and v5 changed the way you can handle authentication.

@misrasaurabh1
Copy link

Would really like a quick documentation or pointers of building custom auth pages. I am building a new website using authjs and I am stuck at this step.

@wunderlichh
Copy link

Hi, I'm using v5 in combination with Next 14. My custom login page is using the signin function created by NextAuth(). It's really straightforward when you are using server actions since the csrf stuff is handled for you internally. To get error and loading states working just use useFormState and useFormStatus.

@kjetilhartveit
Copy link
Author

kjetilhartveit commented Nov 27, 2023

Hi, I'm using v5 in combination with Next 14. My custom login page is using the signin function created by NextAuth(). It's really straightforward when you are using server actions since the csrf stuff is handled for you internally. To get error and loading states working just use useFormState and useFormStatus.

Cool, thanks for letting me know about this. Will definitely try it soon, must port to app router first.

How would you get the list of enabled providers? Is this still possible? Am I missing something?

If there are documentation available that would be really helpful as well

@misrasaurabh1
Copy link

I'm a new nextjs and authjs user. Would it be possible to share a small snippet or to update the docs on how to get the custom Auth page working?

@kjetilhartveit
Copy link
Author

kjetilhartveit commented Dec 6, 2023

I've managed to get it working with the app router. I don't know if it's possible with the pages router.

Here are some snippets for OAuth providers and the Email provider:

// SignInOAuthComponent.tsx

"use client";

import { useState, useTransition } from "react";
import { signInOAuth } from "actions/signin";

export default function SignInOAuthComponent() {
  const [error, setError] = useState(null);
  const [, startTransition] = useTransition();
  return <>
    {error && <div>{error}</div>}
    <button type="button" onClick={() => {
      startTransition(async () => {
        const result = await signInOAuth({
          providerId: 'google',
        });
        if (result?.status === "error") {
          setError(result.errorMessage);
        }
      });
    }}>
      Sign in with Google
    </button>
  </>
}
// SignInEmailComponent.tsx

"use client";

import { useFormState } from "react-dom";
import { signInEmail } from "actions/signin";

export default function SignInEmailComponent() {
  const [state, formAction] = useFormState(signInEmail, null);
  return <form action={formAction}>
    <label>
      E-mail address:
      <input type="email" name="email" />
    </label>
    <button type="submit">Sign in with email</button>
    {state?.status === "error" && (
      <p>{state.errorMessage}</p>
    )}
  </form>
}
// actions/signin.ts

import { redirect } from "next/navigation";
import {
	type RedirectableProviderType,
	type OAuthProviderType,
} from "next-auth/providers";

/**
 * Server action for OAuth sign in with AuthJS.
 */
export async function signInOAuth({ providerId }: { providerId: string }) {
	let redirectUrl: string | null = null;
	try {
                // note: could validate the providerId using something like zod to ensure only allowed providers are passed in
                // The signIn() function comes from NextAuth() in your auth.ts
		redirectUrl = await signIn(providerId satisfies OAuthProviderType, {
			redirect: false,
		});
		if (!redirectUrl) {
			return {
				status: "error",
				errorMessage: "Failed to login, redirect url not found",
			} as const;
		}
	} catch (error) {
		return {
			status: "error",
			errorMessage: "Failed to login",
		} as const;
	}

	redirect(redirectUrl);
}

export type SignInEmailResult =
	| {
			status: "error";
			errorMessage: string;
	  }
	| undefined;

/**
 * Server action for email sign in with AuthJS.
 */
export async function signInEmail(
	previousState: SignInEmailResult | null, // please the compiler..
	formData: FormData,
): Promise<SignInEmailResult> {
	let redirectUrl: string | null = null;
	try {
		const email = formData.get("email");
                // The signIn() function comes from NextAuth() in your auth.ts
		redirectUrl = await signIn("email" satisfies RedirectableProviderType, {
			redirect: false,
			email,
		});
		if (!redirectUrl) {
			return {
				status: "error",
				errorMessage: "Failed to sign in using email, redirect url not found",
			} as const;
		}
	} catch (error) {
		return {
			status: "error",
			errorMessage: "Failed to sign in using email.",
		} as const;
	}

	redirect(redirectUrl);
}

@subvertallchris
Copy link

subvertallchris commented Dec 19, 2023

the csrf stuff is handled for you internally.

@wunderlichh, if you have a moment, can you link to some details about this? It looks to me like it still needs to be provided to the login form. The docs here suggest one should be able to get a token through the API and then it looks like it needs to be in a form variable csrfToken when the form is submitted.

@kjetilhartveit, maybe you know something about this too?

Thank you!

@kjetilhartveit
Copy link
Author

kjetilhartveit commented Dec 19, 2023

the csrf stuff is handled for you internally.

@wunderlichh, if you have a moment, can you link to some details about this? It looks to me like it still needs to be provided to the login form. The docs here suggest one should be able to get a token through the API and then it looks like it needs to be in a form variable csrfToken when the form is submitted.

@kjetilhartveit, maybe you know something about this too?

Thank you!

The docs you linked to also state certain frameworks have built-in CSRF protection so perhaps that's the case for NextJS. I'm not too knowledgable on this though so would also love some pointers around this. Their official examples for the signIn() function does not not involve a CSRF token so I assume it's safe to not handle that manually..

Here's the example from the official jsdocs for the signIn function exported from NextAuth() (package next-auth version 5.0.0-beta.4):

import { AuthError } from "next-auth"
import { signIn } from "../auth"

export default function Layout() {
	return (
		<form action={async (formData) => {
			"use server"
			try {
				await signIn("credentials", formData)
			} catch(error) {
				if (error instanceof AuthError) // Handle auth errors
				throw error // Rethrow all other errors
			}
		}}>
			<button>Sign in</button>
		</form>
	)
}

@subvertallchris
Copy link

subvertallchris commented Dec 19, 2023

@kjetilhartveit this is helpful and I think it led me to something important. That snippet from the jsdoc is an example of the Layout file but it omits the form that it wraps. The signin.tsx file used by NextAuth has a hidden form field that includes the token:

<input type="hidden" name="csrfToken" value={csrfToken} />

That value comes in as a prop. This combined with references to double-submit CSRF protection tell me that this only works if you provide that field, which expects you to get the CSRF token ahead of time. You can get it by doing a GET /api/auth/csrf. It responds with a JSON object, csrfToken, and it sets a matching cookie. The signIn function reads formData and is supposed to compare the submitted value to the cookie value.

@subvertallchris
Copy link

subvertallchris commented Dec 20, 2023

I spent some time on this today. I'm using Next.js 14.0.4 with CredentialsProvider to login with an email and password. Here's what I've concluded:

  • Using the default pages, NextAuth v5 does use CSRF. You can see this by removing the hidden csrfToken field in the form.
  • If you build a custom page, calling signIn will not verify CSRF even if you provide a csrfToken field in your form. I proved this by omitting the field and by sending an invalid value. It always ignores the field and logs me in.
  • As far as I can tell, the only way to get CSRF protection with CredentialsProvider is to do it manually.

You can do it manually like this:

  1. Make an API request to {YOUR_DOMAIN}/api/auth/csrf to get a token from the client. You cannot do this from a React Server Component because you cannot set cookies with RSC. This will set a new cookie and return a valid CSRF token.
  2. Add the token to a field in your form.
  3. When you submit the form, grab the cookie from the headers, parse it, and compare its value to the form submission.

An extremely crude minimal example that uses RSC, Server Actions, and the new use hook looks like this:

// page.tsx, RSC
// You can do any pre-auth stuff here. Make sure they're not logged in and redirect if they are.

import { loginAction } from '@/controllers/admin/authController';

const LoginPage = async () => {

  return <LoginForm onSubmitForm={loginAction} />;
};

export default LoginPage;
'use client';

// LoginForm.tsx
// This makes the CSRF request and presents the form.

import * as React from 'react';
import { use } from 'react';

type SubmitFormData = {
  email: string;
  password: string;
  csrfToken: string;
};

const LoginForm = ({
  onSubmitForm,
}: {
  onSubmitForm: (formData: SubmitFormData) => Promise<{ status: string } | undefined>;
}) => {
  const memoizedRequest = React.useMemo(() => {
    return new Promise<string>((res) => {
      void fetch(`${YOUR_DOMAIN}/api/auth/csrf`, {
        next: {
          revalidate: 0,
        },
      }).then((csrfTokenResponse) => {
        return csrfTokenResponse.json().then((json) => {
          const { csrfToken } = json as { csrfToken: string };
          res(csrfToken);
        });
      });
    });
  }, []);

  // FYI, on closer reflection, I think my use of `use` is wrong here. You can assign the csrfToken in a `useMemo` instead. Show a loading state while fetching?
  const csrfToken = use(memoizedRequest);

  return (
    <div>
      <form
        onSubmit={(formData) => {
          const formDataAsObject = Object.fromEntries(new FormData(formData.currentTarget));
          void onSubmitForm(formDataAsObject as SubmitFormData);
        }}
      >
        <input type="hidden" name="csrfToken" value={csrfToken} />
        <label>
          Username
          <input type="text" name="email" id="email" />
        </label>
        <label>
          Password
          <input type="password" name="password" id="password" />
        </label>
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

export default LoginForm;
'use server';

// The React Server Action that is responsible for login

import { isRedirectError } from 'next/dist/client/components/redirect';
import { cookies } from 'next/headers';
import { z } from 'zod';

import { signIn } from '@/lib/auth';

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string(),
  csrfToken: z.string(),
});

export async function loginAction(unparsedFormData: { email: string; password: string; csrfToken: string }) {
  const formData = loginSchema.parse(unparsedFormData);
  const requestCookies = cookies();
  const csrfToken = requestCookies.get('authjs.csrf-token');
  const tokenValue = csrfToken?.value?.split('|').at(0);

  if (formData.csrfToken !== tokenValue) {
    throw new Error('CSRF token mismatch');
  }

  try {
    // This will redirect immediately on login success
    await signIn('credentials', unparsedFormData);
  } catch (e) {
    if (isRedirectError(e)) {
      throw e;
    }

    console.log('error', e);
    return {
      status: 'error',
    };
  }
}

I'm not sure if I'm using use correctly here, it's my first time 😇 so please correct me. (UPDATE: I am 99% sure my usage is wrong so maybe don't do what I'm doing...) I'm handling the form as an object so I can slap in react-hook-form and add error validations, error handling, etc,...

It's possible that there's more to parsing the token than this. Please correct any mistakes or misunderstandings. Thanks for your time and help, all. 🙏


As an alternative to some of this, you should just be able to read the cookie client-side. NextAuth with Next.js sets the cookie for you without making a request so you can just grab it, stick it in the form, and parse it on the server.

@subvertallchris
Copy link

@mwawrusch commented to the big general v5 Discussion that you can add getCsrfToken by modifying the types. See #8487 (reply in thread)

This is working for me. I replaced my useMemo + use atrocity with:

  React.useEffect(() => {
    startTransition(async () => {
      setValue('csrfToken', await getCsrfToken());
    });
  }, [setValue]);

I also found that in production, the cookie name becomes __Host-authjs.csrf-token, which changed a portion of my action to:

  const csrfToken = requestCookies.get(isProduction ? '__Host-authjs.csrf-token' : 'authjs.csrf-token');

@nadjitan
Copy link

nadjitan commented Feb 7, 2024

#9189 (comment)

redirectUrl = await signIn("email" satisfies RedirectableProviderType, {
			redirect: false,
			email,
		});

This should be changed to:

redirectUrl = await signIn("nodemailer", {
      redirect: false,
      email
    })

Reason being they changed the id of the email provider found here.

@ndom91
Copy link
Member

ndom91 commented Apr 12, 2024

Hey folks, we recently shipped a new docs site which includes detailed pages on setting up custom pages in auth.js. Including a custom signin page.

If you have any additional issues still, feel free to open a new issue 🙏

@ndom91 ndom91 closed this as completed Apr 12, 2024
@lob0codes
Copy link

Hi ndom91,

I didn't want to open a new issue to avoid losing the context of the conversation.

By reading the documentation shown in your suggested post about custom signin page, it would seem as an easy taks to do.

However, I am getting the error "[auth][error] MissingCSRF: CSRF token was missing during an action signin. Read more at https://errors.authjs.dev#missingcsrf"."

The provided auth.js documentation guides me to this page "https://developer.mozilla.org/en-US/docs/Web/Security/CSRF" which throws a 404 error.

Since it seems something easy to do, I am wondering if this is something I should research and solve outside the library "auth.js" or if it something not working from auth.js end.

I appreciate any comment on this, thanks.

@raph90
Copy link

raph90 commented May 5, 2024

I have the same problem as @lob0codes. Here is my custom login page:

import loginAction from './loginAction';

export default async function Login() {

  return (
    <Center mih="100dvh">
      <form action={loginAction}>
        <TextInput type="email" name="email" id="email" placeholder="enter your email address" />
        <Button type="submit">Sign in</Button>
      </form>
    </Center>
  );
}

And my action:

export default async function loginAction(formData: FormData) {
  console.log('the formData', formData);
  try {
    await signIn('nodemailer', {
      email: formData.get('email'),
      redirectTo: '/',
    });
  } catch (error) {
    return {
      status: 'error',
      errorMessage: 'Failed to sign in',
    };
  }
  return {
    status: 'success',
  };
}

The first time I sign in, everything works. However, if I log out and then try to sign in again, I get

[auth][error] MissingCSRF: CSRF token was missing during an action signin. Read more at https://errors.authjs.dev#missingcsrf

@raph90
Copy link

raph90 commented May 5, 2024

@subvertallchris, sorry to ping you like this, I was wondering if you might have some insight into what's going on with my CSRF token... I've followed your example above, using the RSC -> client rendered form -> server action pattern.

Here's my form:

'use client';

import { TextInput, Button } from '@mantine/core';
import { useEffect, useState, useTransition } from 'react';
import { getCsrfToken } from 'next-auth/react';
import { useFormState } from 'react-dom';
import loginAction, { ActionState } from './loginAction';

const defaultState: ActionState = {
  status: 'not_sent',
};

export default function LoginForm() {
  const [isPending, startTransition] = useTransition();
  const [csrfToken, setCsrfToken] = useState('');

  useEffect(() => {
    startTransition(async () => {
      setCsrfToken(await getCsrfToken());
    });
  }, [setCsrfToken]);

  const [state, formAction] = useFormState(loginAction, defaultState);
  return (
    <form action={formAction}>
      <TextInput type="email" name="email" id="email" placeholder="enter your email address" />
      <TextInput type="hidden" name="csrfToken" value={csrfToken} />
      <Button type="submit">Sign in</Button>
    </form>
  );
}

and my action:

'use server';

import { cookies } from 'next/headers';
import { signIn } from '@/lib/auth';

type Status = 'success' | 'error' | 'not_sent';
export type ActionState = {
  status: Status;
  errorMessage?: string;
};

export default async function loginAction(prevState: ActionState, formData: FormData) {
  console.log('CALLED');
  const requestCookies = cookies();
  const csrfToken = requestCookies.get('authjs.csrf-token');
  const tokenValue = csrfToken?.value?.split('|').at(0);

  if (formData.get('csrfToken') !== tokenValue) {
    throw new Error('CSRF token mismatch');
  }

  console.log('the submitted form data', Object.fromEntries(formData));
  try {
    await signIn('nodemailer', {
      ...Object.fromEntries(formData),
      redirectTo: '/',
    });
  } catch (error) {
    return {
      status: 'error' as Status,
      errorMessage: 'Failed to sign in',
    };
  }
  return {
    status: 'success' as Status,
  };
}

What's interesting is that when I login the first time, this works. However, if I then sign out, and then try to login again, I get the missing CSRF error noted above. Interestingly, when that happens, my server action doesn't even appear to run - the console logs in my action don't print to the console. This makes me think that there's some sort of caching going on, though I've been unable to work out what or where.

Just asking here because you clearly know your stuff, so any help would be hugely appreciated!

@lob0codes
Copy link

Hi raph90,

Check this link, it may help you solve the issue, I think is an issue with Next.js:

ndom91/next-auth-example-sign-in-page#5

I haven't had time to keep testing the solution but I tried a few times and the solution seems to work.

@bdermody
Copy link

@lob0codes Hi!!! Did you get this to work properly? I am struggling with this, seems to be too complicated it and it is supposed to be easy :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Relates to documentation triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.
Projects
None yet
Development

No branches or pull requests