Search code examples
node.jsreactjsauthenticationnext.jsnext-auth

next-auth keeps redirecting on error with credentials provider


I am trying to implement next-auth with my next js app with the Credentials provider. However, everytime I fail a login, it keeps trying to access the /api/auth/error route. I am trying to custom handle an error by staying on the login page and adding a param at the end of the url like /login?status=Failed. What am I doing wrong that it keeps trying to get to the /api/auth/error route instead of following my flow?

My code:

/pages/api/auth/[...nextauth].js

import { authenticate } from "api/login";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

export default NextAuth({
  session: {
    strategy: 'jwt'
  },
  providers: [
    CredentialsProvider({
      id: 'credentials',
      name: 'Credentials',
      credentials: {
        username: { type: 'text', label: 'Username'},
        password: { type: 'text', label: 'Password' }
      },
      async authorize(credentials, req) {
        const res = await authenticate(credentials); // custom function that returns an object with a jwt if auth is successful

        if(res.status !== 200) {
          return null;
        }

        return res.user;
      }
    })
  ],
  pages: {
    signIn: '/login'  
  },
  callbacks: {
    async signIn(user) {
      console.log('user: ' + user);
      
      if (!user) return '/login?status=fail';

      return '/'
    }
  }
});

login page (i am aware this is not best practice) /pages/login.js

import { useState } from 'react';
import { signIn } from 'next-auth/react';

import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';

export default function Login() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  
  const onSubmit = async () => {
    signIn('credentials', {
      username: username,
      password: password,
      redirect: false,
    })
    .then(res => {
      console.log(res);
    })
    .catch(err => {
      console.error(err);
    });
  };

  return (
    <Container>
      <Row>
        <Col lg={6} className="offset-lg-3">
          <Form>
            <Row>
              <Col>
                <Form.Group controlId="loginUsername">
                  <Form.Label>Username</Form.Label>
                  <Form.Control 
                    type="text"
                    onChange={(e) => setUsername(e.target.value)}
                    value={username} />
                </Form.Group>
              </Col>
            </Row>
            <Row>
              <Col>
                <Form.Group controlId="loginPassword">
                  <Form.Label>Password</Form.Label>
                  <Form.Control 
                    type="text"
                    onChange={(e) => setPassword(e.target.value)}
                    value={password} />
                </Form.Group>
              </Col>
            </Row>
            <Row className="pt-3">
              <Col lg={4}>
                <Button onClick={onSubmit}>Login</Button>
              </Col>
            </Row>
            <Row>
              <Col>
              </Col>
            </Row>
          </Form>
        </Col>
      </Row>
    </Container>
  )
}

_app.js

import React from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import { SessionProvider } from 'next-auth/react';


function MyApp({
  Component,
  pageProps: { session, ...pageProps } }) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  );
}

export default MyApp;

and additionally, here's a protected page i setup

import React from 'react';
import { getSession } from 'next-auth/react';

import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';

export default function Home() {
  return (
    <Container fluid>
      <Row>
        <Col>
          <h1>Hello World</h1>
        </Col>
      </Row>
    </Container>
  );
}

export async function getServerSideProps(context) {
  const session = await getSession({ req: context.req });

  if (!session) {
    return {
      redirect: {
        destination: '/login',
        permanent: false
      }
    }
  }

  return {
    props: { session }
  }
}

Solution

  • you need to specify the error page too, and since you want to show the error on the login page it should be like this:

    pages: {
        signIn: '/login',
        error: '/login'
      },
    

    you can try this approach I got from this blogpost to customize the login page to handle different login errors:

    // pages/login.js
    import { useState } from 'react';
    import { signIn } from 'next-auth/react';
    import { useRouter } from 'next/router';
    
    import Container from 'react-bootstrap/Container';
    import Row from 'react-bootstrap/Row';
    import Col from 'react-bootstrap/Col';
    import Form from 'react-bootstrap/Form';
    import Button from 'react-bootstrap/Button';
    
    export default function Login() {
      const [username, setUsername] = useState('');
      const [password, setPassword] = useState('');
      const { error } = useRouter().query;
    
      const onSubmit = async () => {
        signIn('credentials', {
          username: username,
          password: password,
          redirect: false,
        })
          .then((res) => {
            console.log(res);
          })
          .catch((err) => {
            console.error(err);
          });
      };
    
      return (
        <Container>
          <Row>
            <Col lg={6} className="offset-lg-3">
              {/* Error message */}
              <Row>
                <Col>{error && <SignInError error={error} />}</Col>
              </Row>
              <Form>
                <Row>
                  <Col>
                    <Form.Group controlId="loginUsername">
                      <Form.Label>Username</Form.Label>
                      <Form.Control
                        type="text"
                        onChange={(e) => setUsername(e.target.value)}
                        value={username}
                      />
                    </Form.Group>
                  </Col>
                </Row>
                <Row>
                  <Col>
                    <Form.Group controlId="loginPassword">
                      <Form.Label>Password</Form.Label>
                      <Form.Control
                        type="text"
                        onChange={(e) => setPassword(e.target.value)}
                        value={password}
                      />
                    </Form.Group>
                  </Col>
                </Row>
                <Row className="pt-3">
                  <Col lg={4}>
                    <Button onClick={onSubmit}>Login</Button>
                  </Col>
                </Row>
                <Row>
                  <Col></Col>
                </Row>
              </Form>
            </Col>
          </Row>
        </Container>
      );
    }
    
    const errors = {
      Signin: 'Try signing with a different account.',
      OAuthSignin: 'Try signing with a different account.',
      OAuthCallback: 'Try signing with a different account.',
      OAuthCreateAccount: 'Try signing with a different account.',
      EmailCreateAccount: 'Try signing with a different account.',
      Callback: 'Try signing with a different account.',
      OAuthAccountNotLinked:
        'To confirm your identity, sign in with the same account you used originally.',
      EmailSignin: 'Check your email address.',
      CredentialsSignin:
        'Sign in failed. Check the details you provided are correct.',
      default: 'Unable to sign in.',
    };
    
    const SignInError = ({ error }) => {
      const errorMessage = error && (errors[error] ?? errors.default);
      return <div>{errorMessage}</div>;
    };
    

    you can customize how the error is shown to your needs