This is a simple login using Credentials as a CredentialProvider.

That means there is a screen to put just a username and a password based in a fixed username and password declared in the system. And after a successful login, it will redirect to your desired page.

This example is using the following structure based in SRC folder structure:

the content of the package.json:

"dependencies": {
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/material-nextjs": "^5.15.11",
"next": "14.2.3",
"next-auth": "^4.24.7",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}

Material UI could be ignored. I am using it all the time but it is not vital for this example.

Also you need to declare the .env.local variables for this example:

USER_SYSTEM_PASSWORD=123456789
USER_SYSTEM_USERNAME=rootuser123
NEXT_PUBLIC_AFTER_SIGNED=admin

Only for this example, the credentials are going to be read inside the server and the “where to go after successful login” variable. In this case, it goes to “admin” located in src/pages/admin/index.tsx

When I created this nextjs app, there was no _app or app file. It needed to be created manually to contain the “session provider” (next auth session):

//src/pages/_app.tsx
import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app";
function MyApp({ Component, pageProps }: AppProps) {
return (
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
);
}
export default MyApp;

Now we can call the session from the client. The following is the login form. In this case src/pages/login/index.tsx:

import React, { useState } from 'react';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
import { styled } from '@mui/material/styles';
import { signIn } from 'next-auth/react';
import '@globalStyle';
import Box from '@mui/material/Box';
import router from 'next/router';
const LoginForm = styled('form')(({ theme }) => ({
// display: 'flex',
// flexDirection: 'column',
// gap: theme.spacing(2),
// width: '300px',
}));
function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (event: { preventDefault: () => void; }) => {
event.preventDefault();
console.log('Submitted login form:', { username, password });
};
const signInSuccessful = () => {
if (!process.env.NEXT_PUBLIC_AFTER_SIGNED) {
console.log('afterSignInPage is: ', process.env.NEXT_PUBLIC_AFTER_SIGNED)
return <>missing after signin page</>
} else {
router.push(process.env.NEXT_PUBLIC_AFTER_SIGNED)
}
}
function onClick(event: React.MouseEvent<HTMLButtonElement>) {
const x = signIn("system-credentials",
{
redirect: false,
username, password,
callbackUrl: process.env.NEXT_PUBLIC_AFTER_SIGNED
})
.then((data) => {
if (data == undefined) {
console.log("****data is undefined")
return
}
if (data.error) {
console.log("ERRROR: ", data.error)
} else {
console.log("****signin ok: ", data)
signInSuccessful()
}
})
.catch((error) => {
console.log("*****error adminInitiateAuth: ", error)
console.log(JSON.stringify(error))
})
}
return (<>
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<Box className='loginBox' >
<h1>Login</h1>
<div>callbackUrl: {process.env.NEXT_PUBLIC_AFTER_SIGNED}</div>
<LoginForm onSubmit={handleSubmit}>
<TextField
label="Username"
variant="outlined"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<TextField
label="Password"
variant="outlined"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<Button onClick={onClick} type="submit" variant="contained">
Login
</Button>
</LoginForm>
</Box>
</main>
</>
);
}
export default LoginPage;

NOTE: I have no idea how to use “redirect:true” for the urlredirect parameter. I tried many times and failed. I am manually redirecting after a successful login.

You should be able to see this awesome login screen:

Notice the redirect url (after successful login) is used at the front via the NEXT_PUBLIC_ prefix. that means it could be accessed from client. Do not use that prefix with vital data.

so, when the user presses the button, the signIn function from the next-auth package should verified the credentials with the next-auth API. By default the api should be located in src/pages/api/auth/[…next-auth].tsx file. YES!, the name of the file is […next-auth].tsx and for this example, this is the code:

import NextAuth, { Account, Profile, Session, User } from "next-auth";
import { AdapterUser } from "next-auth/adapters";
import { JWT } from "next-auth/jwt";
import CredentialsProvider from "next-auth/providers/credentials";
interface UserWallOfShame extends User {
username: string,
isSystemAdmin: boolean;
}
export default NextAuth({
providers: [
CredentialsProvider({
id: "system-credentials",
name: "SystemUser",
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
authorize: async (credentials, req) => {
try {
console.log("credentials: ", credentials); // For debugging (remove in production)
if (credentials == undefined) {
throw new Error(JSON.stringify("internal error"))
}
if (credentials.username === process.env.USER_SYSTEM_USERNAME) {
if (credentials.password === process.env.USER_SYSTEM_PASSWORD) {
const user: UserWallOfShame = {
username: credentials.username,
isSystemAdmin: true,
id: "0"
}
return user
} else {
throw new Error("wrong password")
}
} else {
throw new Error("wrong username")
}
} catch (error) {
// Handle login errors (e.g., invalid credentials, server errors)
console.error("Login error:", error);
throw new Error("" + error)
}
},
})
],
callbacks: {
async signIn() {
return true
},
async redirect(params: {
url: string;
baseUrl: string;
}) {
const { url, baseUrl } = params;
return baseUrl
},
session(params: {
session: Session;
token: JWT;
// user: AdapterUser;
}) {
const { session, token } = params
if (token && session && session.user) {
const user: UserWallOfShame = {
username: "asdf",
isSystemAdmin: true,
id: "0"
}
session.user = user
}
return session
},
async jwt(params: {
token: JWT;
user: User | AdapterUser;
account: Account | null;
profile?: Profile | undefined;
trigger?: "signIn" | "update" | "signUp" | undefined;
isNewUser?: boolean | undefined;
session?: any;
}) {
const { account, token } = params
if (account) {
token.accessToken = account.accessToken; // Add access token
}
return token;
}
}
});

I have modified the User type to include more properties according to your requirements.

Check how the callback session is returning the user with new data. You can put your web app logic in the callbacks.

assuming you logged properly, this should redirect to “admin”, located in src/pages/admin/index.tsx:

import React from 'react';
import '@globalStyle';
import { useSession } from 'next-auth/react';
export default function AdminPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return <p>Loading...</p>;
}
if (status === 'unauthenticated') {
return <p>Please sign in to access this content.</p>;
}
console.log(session);
return <>
<div>status: {status}</div>
<div>
Welcome back, {session &&
session.user && session.user.name && <div>session.user.name</div>}!
<br />
</div>
</>
}

This page will show you something like this:

And if you check your developer tools, you should be able to see the client session:

As you can see, you are loading data in your client session. You can see also the username has changed from the logic included in the callbacks. Do not include sensitive data, such as passwords.

Conclusion

This was a pain in the rear, but now you should be able to understand how to customize the login