Why do I get a 401 unauthorized error when logging in?

9k Views Asked by At

I am using the NextAuth.js credentials provider for the log in procedure. When logging in I am catching the errors in a try-catch block and set the error state accordingly, but I do not display the error state anywhere yet. Even though I am catching the errors, a 401 unauthorized gets thrown when trying to log in. I am using wrong credentials, thus expecting an error called CredentialsSignin which I am getting, but additionally I am getting the 401 every time. The problem is that I am not able to detect where it is thrown, that might be the reason I am not able to handle it.

401 thrown in console

Here the code of my custom log in page:

import { InferGetServerSidePropsType } from "next"
import { CtxOrReq } from "next-auth/client/_utils";
import { getCsrfToken, getSession, signIn } from "next-auth/react"
import { useRouter } from "next/router";
import { useCallback, useEffect, useState } from "react";
import { useFormik } from "formik"
import * as Yup from "yup"

export default function Login({ csrfToken }: InferGetServerSidePropsType<typeof getServerSideProps>) {

    const [loading, setLoading] = useState(false)
    const [error, setError] = useState('')
    const router = useRouter()

    type Credentials = {
        email: string
        password: string
    }

    let credentials: Credentials;

    const handleLogin = useCallback(async (credentials) => {
        if (!credentials.email) {
            return setError('email is missing')
        }
        if (!credentials.password) {
            return setError('password is missing')
        }
        try {
            setLoading(true)
            const response: any = await signIn('credentials', { ...credentials, redirect: false }) // find right type
            if (response.error && response.error === 'CredentialsSignin') {
                setError('email or password are wrong')
            } else {
                setError('')
                router.push('/')
            }
        } catch {
            setError('login failed')
        } finally {
            setLoading(false)
        }
    }, [router])

    const formik = useFormik({
        initialValues: {
            email: "",
            password: ""
        },
        validationSchema: Yup.object({
            email: Yup.string()
                .required("email address is required"),
            password: Yup.string()
                .required("password is required")
        }),
        onSubmit: values => {
            credentials = {
                email: values.email,
                password: values.password
            }
            handleLogin(credentials)
        }
    })

    return (
        <form onSubmit={formik.handleSubmit} noValidate>
            <input name="csrfToken" type="hidden" defaultValue={csrfToken} />
            <label>
                Email
                <input
                    name="email"
                    type="email"
                    value={formik.values.email}
                    onChange={formik.handleChange}
                    onBlur={formik.handleBlur} />
            </label>
            {formik.touched.email && formik.errors.email && <p>{formik.errors.email}</p>}
            <label>
                Password
                <input
                    name="password"
                    type="password"
                    value={formik.values.password}
                    onChange={formik.handleChange}
                    onBlur={formik.handleBlur} />
            </label>
            {formik.touched.password && formik.errors.password && <p>{formik.errors.password}</p>}
            <button type="submit">Login</button>
        </form>
    )
}

export async function getServerSideProps(context: CtxOrReq | undefined) {

    const session = await getSession(context)

    if (session) {
        return {
            redirect: { destination: '/' }
        }
    }

    return {
        props: {
            csrfToken: await getCsrfToken(context)
        },
    }
}

Here the code of my [...nextauth].ts API page:

import NextAuth from 'next-auth'
import { PrismaAdapter } from '@next-auth/prisma-adapter'
import { prisma } from '../../../prisma/prisma_client'
import CredentialsProvider from "next-auth/providers/credentials"
import { compare } from 'bcryptjs'

export default NextAuth({

  adapter: PrismaAdapter(prisma),

  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: {},
        password: {}
      },
      async authorize(credentials) {
        if (!credentials) {
          return null
        }

        const { email } = credentials
        const { password } = credentials

        const storedUser = await prisma.user.findUnique({
          where: {
            email
          }, select: {
            id: true,
            email: true,
            hashedPassword: true,
            company: {
              select: {
                id: true,
                name: true
              }
            }
          }
        })

        if (!storedUser) {
          return null
        }

        const user = {
          id: storedUser?.id,
          email,
          comanyId: storedUser?.company?.id,
          companyName: storedUser?.company?.name,
        }

        const validatePassword = await compare(password, storedUser.hashedPassword)
        return validatePassword ? user : null
      }
    })
  ],

  pages: {
    signIn: '/login'
  },

  callbacks: {
    async jwt({ token, user }) {
      user && (token.user = user)
      return token
    },
    async session({ session, token }) {
      session.user = token.user
      return session
    }
  },

  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60 // 30 days
  }
})
2

There are 2 best solutions below

0
On

Encountered the same issue and manage to resolve it. The first block of code below is the buggy version and the second block is the working version.

So, there's a few things to note:

  • I returned the same data for catch and response because I wanted to determine if the issue was cause by the data which was returned. But it wasn't the case, so this was reverted in the working version.
  • In the working version, there's return for the promise and a return within the promise. My understanding is that the response and catch blocks are functions by itself, and returning within the inner block will not propagate the value such that it is returned by the authorize function. In other words, I'm returning nothing - resulting in 401.
  • With the same understanding, the issue with OP's code is it is always failing to return a user object. Without running the code, I can't pinpoint it for sure but there are a few possible paths which leads to so.
authorize: async (credentials) => {
axios.post('http://localhost:8000/auth/signin/', {
    email: credentials.email,
    id: credentials.id
}).then((response) => {
    const user = { id: 1, token: "1234", name: 'John', email: '[email protected]' }
    return user
}).catch((e) => {
    const user = { id: 1, token: "1234", name: 'John', email: '[email protected]' }
    return user
});
};
authorize: async (credentials) => {
const request = axios.post('http://localhost:8000/auth/signin/', {
    email: credentials.email,
    id: credentials.id
})

return await request.then((response) => {
    const user = { id: 1, token: "1234", name: 'John', email: '[email protected]' }
    return user
}).catch((e) => {
    console.log(e);
    return null;
});
};
1
On

Please make sure, that you set 'withCredentials' to 'true' for your API object