Credentials
To setup Auth.js with any external authentication mechanisms or use a traditional username/email and password flow, we can use the Credentials
provider. This provider is designed to forward any credentials inserted into the login form (i.e. username/password, but not limited to) to your authentication service.
The industry has come a long way since usernames and passwords as the go-to mechanism for authenticating and authorizing users to web applications. Therefore, if possible, we recommend a more modern and secure authentication mechanism such as any of the OAuth providers, Email Magic Links, or WebAuthn (Passkeys) options instead.
However, we also want to be flexible and support anything you deem appropriate for your application and use case, so there are no plans to remove this provider.
By default, the Credentials provider does not persist data in the database. However, you can still create and save any data in your database, you just have to provide the necessary logic, eg. to encrypt passwords, add rate-limiting, add password reset functionality, etc.
Credentials Provider
To use the Credentials Provider
, you’ll first need to import and configure it in your Auth.js
setup. This provider allows you to implement custom login logic based on form input values.
Here’s how to set it up:
- Import the provider.
- Add it to the
providers
array in yourAuth.js
config. - Define the
credentials
andauthorize
fields.
credentials
The credentials
object defines the input fields displayed on the default sign-in page. These inputs are automatically rendered on the route:
/api/auth/signin
(Next.js)/auth/signin
(Other frameworks)
Each field accepts the following properties:
label
: Input labeltype
: HTML input type (text
,password
, etc.)placeholder
: Placeholder text
These fields are also passed to the
authorize
function under thecredentials
argument.
For more details, see the Built-in Pages guide.
Credentials({
credentials: {
email: {
type: "email",
label: "Email",
placeholder: "johndoe@gmail.com",
},
password: {
type: "password",
label: "Password",
placeholder: "*****",
},
},
})
authorize
The authorize
function handles the custom login logic and determines whether the credentials provided are valid.
It receives the input values defined in credentials
, and you must return either a user object or null
. If null
is returned, the login fails.
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
// Your own logic for dealing with plaintext password strings; be careful!
import { saltAndHashPassword } from "@/utils/password"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Credentials({
// You can specify which fields should be submitted, by adding keys to the `credentials` object.
// e.g. domain, username, password, 2FA token, etc.
credentials: {
email: {},
password: {},
},
authorize: async (credentials) => {
let user = null
// logic to salt and hash password
const pwHash = saltAndHashPassword(credentials.password)
// logic to verify if the user exists
user = await getUserFromDb(credentials.email, pwHash)
if (!user) {
// No user found, so this is their first attempt to login
// Optionally, this is also the place you could do a user registration
throw new Error("Invalid credentials.")
}
// return user object with their profile data
return user
},
}),
],
})
If you’re using TypeScript, you can augment the User
interface to match the
response of your authorize
callback, so whenever you read the user in other
callbacks (like the jwt
) the type will match correctly.
Signin Form
Finally, let’s create a simple sign-in form.
import { signIn } from "@/auth"
export function SignIn() {
return (
<form
action={async (formData) => {
"use server"
await signIn("credentials", formData)
}}
>
<label>
Email
<input name="email" type="email" />
</label>
<label>
Password
<input name="password" type="password" />
</label>
<button>Sign In</button>
</form>
)
}
Validating credentials
Always validate the credentials server-side, i.e. by leveraging a schema validation library like Zod.
npm install zod
Next, we’ll set up the schema and parsing in our auth.ts
configuration file, using the authorize
callback on the Credentials
provider.
import { object, string } from "zod"
export const signInSchema = object({
email: string({ required_error: "Email is required" })
.min(1, "Email is required")
.email("Invalid email"),
password: string({ required_error: "Password is required" })
.min(1, "Password is required")
.min(8, "Password must be more than 8 characters")
.max(32, "Password must be less than 32 characters"),
})
import NextAuth from "next-auth"
import { ZodError } from "zod"
import Credentials from "next-auth/providers/credentials"
import { signInSchema } from "./lib/zod"
// Your own logic for dealing with plaintext password strings; be careful!
import { saltAndHashPassword } from "@/utils/password"
import { getUserFromDb } from "@/utils/db"
export const { handlers, auth } = NextAuth({
providers: [
Credentials({
// You can specify which fields should be submitted, by adding keys to the `credentials` object.
// e.g. domain, username, password, 2FA token, etc.
credentials: {
email: {},
password: {},
},
authorize: async (credentials) => {
try {
let user = null
const { email, password } = await signInSchema.parseAsync(credentials)
// logic to salt and hash password
const pwHash = saltAndHashPassword(password)
// logic to verify if the user exists
user = await getUserFromDb(email, pwHash)
if (!user) {
throw new Error("Invalid credentials.")
}
// return JSON object with the user data
return user
} catch (error) {
if (error instanceof ZodError) {
// Return `null` to indicate that the credentials are invalid
return null
}
}
},
}),
],
})
When authorize returns null, Auth.js handles the error in one of two ways:
Using built-in pages:
- The user is redirected to the login page with the query string:
?error=CredentialsSignin&code=credentials
. You can customize the code using the credentials provider options. - Using form actions or custom error handling (e.g., in Remix, SvelteKit): The error is thrown as credentialssignin and must be caught manually in your server action. See more in the Auth.js error reference.