Monday, May 20, 2024
 Popular · Latest · Hot · Upcoming
71
rated 0 times [  76] [ 5]  / answers: 1 / hits: 18077  / 2 Years ago, tue, january 18, 2022, 12:00:00

I'm using Next.js with next auth v4 for authentication with credentials.


What I want to do is add a global verification for my API calls in a middleware to test before API calls the session. If the session is not null the call have to passed successfully, else if the session is null then handle an unauthorized error message and redirect to login page.


I wanna also add protected route and unprotected routes for login page and other pages that is not necessary to check authentication on it.


Here is my code:
[...nextauth].js


import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials";
import api from './api'

export default NextAuth({
providers: [
CredentialsProvider({
name: "Credentials",
async authorize(credentials, req) {
const {username,password} = credentials
const user = await api.auth({
username,
password,
})

if (user) {
return user
} else {
return null

}
}
})
],
callbacks: {
async jwt({ token, user, account }) {
let success = user?.id > 0
if (account && success) {
return {
...token,
user : user ,
accessToken: user.id
};
}
return token;
},

async session({ session, token }) {
session.user = token;
return session;
},
},
secret: "test",
jwt: {
secret: "test",
encryption: true,
},
pages: {
signIn: "/Login",
},
})

My _middleware.js


import { getSession } from "next-auth/react"
import { NextResponse } from "next/server"

/** @param {import("next/server").NextRequest} req */

export async function middleware(req) {
// return early if url isn't supposed to be protected
// Doesn't work here
if (req.url.includes("/Login")) {
return NextResponse.next()
}

const session = await getSession({req})
// You could also check for any property on the session object,
// like role === "admin" or name === "John Doe", etc.
if (!session) return NextResponse.redirect("/Login")

// If user is authenticated, continue.
return NextResponse.next()
}

More From » next.js

 Answers
39

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!" });
}
}

[#50083] Tuesday, November 30, 2021, 3 Years  [reply] [flag answer]
Only authorized users can answer the question. Please sign in first, or register a free account.
jazminkyrap

Total Points: 631
Total Questions: 89
Total Answers: 109

Location: Finland
Member since Fri, Oct 21, 2022
2 Years ago
;