Next auth v4 with Next.js middleware
Asked Answered
H

1

7

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()
}
Hals answered 18/1, 2022 at 11:12 Comment(0)
D
16

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!" });
    }
}
Dependable answered 12/2, 2022 at 1:5 Comment(1)
Thank you very much for your time and your answer, I will try this method and I will get back to you, but it seems to me very usefulHals

© 2022 - 2024 — McMap. All rights reserved.