I would like to mention that those techniques can be improved depending on the situation and can be also migrated to TypeScript, which I'm going to follow up with a future edit, hope this might help.
I made it work by having the following:
FILE: pages/admin/_middleware.js
NOTE: The middleware file can be individually set in paths, for more check please check the execution order
import { withAuth } from "next-auth/middleware"
export default withAuth({
callbacks: {
authorized: ({ token }) => token?.userRole === "admin",
},
})
FILE: api/auth/[...nextauth].js
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export default NextAuth({
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
const res = await fetch("http://localhost:3000/api/auth/getuser", {
method: 'POST',
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" }
})
const user = await res.json()
// If no error and we have user data, return it
if (res.ok && user) {
return user;
}
return null
}
})
],
secret: process.env.JWT_SECRET,
callbacks: {
async jwt({token, user, account}) {
if (token || user) {
token.userRole = "admin";
return {...token};
}
},
},
})
FILE: api/auth/getuser.js
//YOUR OWN DATABASE
import { sql_query } from '@project/utils/db';
export default async function handler(req,res) {
let username = req.body.username;
let password = req.body.password;
let isJSON = req.headers['content-type'] == "application/json";
let isPOST = req.method === "POST";
let fieldsExisting = password && username;
if (isPOST && isJSON && fieldsExisting) {
const { createHmac } = await import('crypto');
//This will require to have password field in database set as md5
//you can also have it as simple STRING, depends on preferences
const hash = createHmac('md5', password ).digest('hex');
//YOUR OWN DATABASE
const query = `SELECT * FROM users WHERE email='${username}' AND password='${hash}' LIMIT 1;`;
let results = await sql_query(query);
if (results == undefined) {
res.status(404).json({ "error": "Not found" });
} else {
res.status(200).json({ "username": results[0].nume });
}
} else {
res.status(500).json({ "error": "Invalid request type" });
}
}
That //YOUR OWN DATABASE
for FILE: utils/db
:
import mysql from "serverless-mysql";
export const db = mysql({
config: {
host: process.env.MYSQL_HOST,
database: process.env.MYSQL_DATABASE,
user: process.env.MYSQL_USERNAME,
password: process.env.MYSQL_PASSWORD,
},
});
export async function sql_query(query_string values = []) {
try {
const results = await db.query(query_string, values);
await db.end();
return results;
} catch (e) {
if (typeof e === "string") {
e.toUpperCase() // works, `e` narrowed to string
} else if (e instanceof Error) {
e.message // works, `e` narrowed to Error
}
}
}
FILE: .env
-- NOTE: CHANGE .env variables with your own
NEXTAUTH_URL=http://localhost:3000
MYSQL_HOST="0.0.0.0"
MYSQL_DATABASE="randomNAME"
MYSQL_USERNAME="randomNAME"
MYSQL_PASSWORD="randomPASS"
NEXTAUTH_SECRET="49dc52e6bf2abe5ef6e2bb5b0f1ee2d765b922ae6cc8b95d39dc06c21c848f8c"
JWT_SECRET="49dc52e6bf2abe5ef6e2bb5b0f1ee2d765b922ae6cc8b95d39dc06c21c848f8c"
FILE: package.json
{
"name": "MyAwesomeName",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "12.0.9",
"next-auth": "^4.2.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"serverless-mysql": "^1.5.4",
"swr": "^0.4.2"
},
"devDependencies": {
"@types/node": "17.0.12",
"@types/react": "17.0.38",
"eslint": "8.7.0",
"eslint-config-next": "12.0.9",
"typescript": "4.5.5"
}
}
EDIT: 01/07/2022 for NextJS 12.2.0 middleware
As I've mentioned, I will follow up with an edit for TypeScript, and was perfectly timed with 12.2.0 release for NextJS.
I would also like to metion that:
Based on the discussion with a user in GitHub, apparently jose library works better while running Edge functions in the middleware while jsonwebtoken does not. This was based on SO question.
The files should be the following:
/package.json
{
"name": "xyz123",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@mui/material": "^5.8.6",
"@prisma/client": "^4.0.0",
"axios": "^0.27.2",
"jose": "^4.8.3",
"next": "12.2.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.33.0"
},
"devDependencies": {
"@types/node": "18.0.0",
"@types/react": "18.0.14",
"@types/react-dom": "18.0.5",
"eslint": "8.18.0",
"eslint-config-next": "12.2.0",
"prisma": "^4.0.0",
"typescript": "4.7.4"
}
}
/pages/_middleware
has been moved to /middleware
, basically in the root folder, whe are going to have the following:
/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from 'next/server'
import { verify } from "./services/jwt_sign_verify";
const secret = process.env.SECRET || "secret";
export default async function middleware(req: NextRequest) {
const jwt = req.cookies.get("OutsiteJWT");
const url = req.url;
const {pathname} = req.nextUrl;
if (pathname.startsWith("/dashboard")) {
if (jwt === undefined) {
req.nextUrl.pathname = "/login";
return NextResponse.redirect(req.nextUrl);
}
try {
await verify(jwt, secret);
return NextResponse.next();
} catch (error) {
req.nextUrl.pathname = "/login";
return NextResponse.redirect(req.nextUrl);
}
}
return NextResponse.next();
}
/services/jwt_sign_verify.ts
import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
import { Token } from "@typescript-eslint/types/dist/generated/ast-spec";
export async function sign(payload: string, secret: string): Promise<string> {
const iat = Math.floor(Date.now() / 1000);
const exp = iat + 60 * 60; // one hour
return new SignJWT({ payload })
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setExpirationTime(exp)
.setIssuedAt(iat)
.setNotBefore(iat)
.sign(new TextEncoder().encode(secret));
}
export async function verify(token: string, secret: string): Promise<JWTPayload> {
const { payload } = await jwtVerify(token, new TextEncoder().encode(secret));
// run some checks on the returned payload, perhaps you expect some specific values
// if its all good, return it, or perhaps just return a boolean
return payload;
}
/pages/api/auth/login.ts
/* eslint-disable import/no-anonymous-default-export */
import { serialize } from "cookie";
import { sign } from "../../../services/jwt_sign_verify";
const secret = process.env.SECRET || "secret";
export default async function (req, res) {
const { username, password } = req.body;
// Check-in the database for a match,
//serialize and check your data before doing any operations.
//This IF statement is for checking demo purposes only.
if (username === "Admin" && password === "Admin") {
const token = await sign(
"testing", //do some magic here
secret
);
const serialised = serialize("OursiteJWT", token, {
httpOnly: true,
secure: process.env.NODE_ENV !== "development",
sameSite: "strict",
maxAge: 60 * 60 * 24 * 30,
path: "/",
});
res.setHeader("Set-Cookie", serialised);
res.status(200).json({ message: "Success!" });
} else {
res.status(401).json({ message: "Invalid credentials!" });
}
}