Philipos D.
Philipos D.

Reputation: 2310

Verify JWT token in Next.js middleware throws Module not found: Can't resolve 'crypto' error

I'm trying to use JWT verification for authentication with middleware, but unfortunately, I'm getting some errors that cannot find a solution for.

./node_modules/jwa/index.js:3:0
Module not found: Can't resolve 'crypto'

Import trace for requested module:
./node_modules/jws/lib/sign-stream.js
./node_modules/jws/index.js
./node_modules/jsonwebtoken/verify.js
./middleware.ts

https://nextjs.org/docs/messages/module-not-found

You're using a Node.js module (crypto) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime

package.json

{
  "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",
    "buffer": "^6.0.3",
    "cookie": "^0.5.0",
    "jsonwebtoken": "^8.5.1",
    "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"
  }
}

middleware.ts

import { NextResponse } from "next/server";
import verify from "jsonwebtoken/verify";
import { urlToHttpOptions } from "url";
import type { NextRequest } from 'next/server'

const secret = process.env.SECRET;

export default function middleware(req: NextRequest) {
    const { cookies } = req;
    const { search, protocol, host } = req.nextUrl

    const jwt = cookies.OutsiteJWT;
    const url = req.url;

    if (url.includes('/dashboard')) {
        if (jwt === undefined) {
            return NextResponse.redirect("http://localhost:3000/login");
        }

        try {
            verify(jwt, secret); // <---- ERROR COMES FROM HERE
            return NextResponse.next();
        } catch (error) {
            return NextResponse.redirect("/login");
        }
    }

    return NextResponse.next();
}

/pages/api/auth/login.ts

/* eslint-disable import/no-anonymous-default-export */
import { sign } from "jsonwebtoken";
import { serialize } from "cookie";

const secret = process.env.SECRET;

export default async function (req, res) {
    const { username, password } = req.body;

    // Check in the database
    // if a user with this username
    // and password exists
    if (username === "Admin" && password === "Admin") {
        const token = sign(
            {
                exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 30 days
                username: username,
            },
            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!" });
    }
}

I've tried commenting out the try/catch to check if the token is set, and it works correctly, but when I try to verify in the middleware it fails.

In version 12.2.0 the middleware is stable but also has some changes.

Does somebody else encounter similar issues or know how to solve this?

Upvotes: 4

Views: 6405

Answers (1)

Philipos D.
Philipos D.

Reputation: 2310

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.

/middleware.ts

import { NextResponse } from "next/server";
import { urlToHttpOptions } from "url";
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
    if (username === "Admin" && password === "Admin") {
        const token = await sign(
            "testing",
            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!" });
    }
}

Upvotes: 8

Related Questions