user16949958
user16949958

Reputation:

NEXTJS: Invalid hook call

I'm using the new middleware in nextjs 12 and am trying to add authentication using firebase.

But when i try to use the hook useAuthState it's giving me an error saying: "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem. null"

I have not made any changes in the app other than creating 2 components both in pages directory

login.js and _middleware.js

here is my _middleware.js

import { NextResponse } from "next/server";
// import firebase from "firebase"
import { initializeApp } from "firebase/app";
import "firebase/auth";
import { getAuth } from "firebase/auth";
import { useAuthState } from "react-firebase-hooks/auth";

initializeApp({ ... });

const auth = getAuth();

export async function middleware(req) {
  const [user] = useAuthState(auth);
  const { pathname } = req.nextUrl;

  if (!user) {
    return NextResponse.redirect("/login");
  }

  return NextResponse.next();
}

Here is my login.js

function login() {
  const signInWithGoogle = () => {
    console.log("Clicked!");
  };

  return (
    <div>
      <button onClick={signInWithGoogle}>Sign in with google</button>
    </div>
  );
}

export default login;

And here's my package.json

{
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "firebase": "^9.5.0",
    "next": "^12.0.4",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-firebase-hooks": "^4.0.1"
  },
  "devDependencies": {
    "autoprefixer": "^10.2.6",
    "postcss": "^8.3.5",
    "tailwindcss": "^2.2.4"
  }
}

Upvotes: 2

Views: 15400

Answers (3)

KarlDivad
KarlDivad

Reputation: 161

Sadly Middleware is using V8 engine https://v8.dev/ and Firebase package doesn't support this (wasted too many hours). To authenticate a user you should decrypt it yourself with a library like jose https://www.npmjs.com/package/jose

I managed to work with it using Node 18 and node-fetch 👀. Your code should look like this on typescript:

import { NextResponse } from 'next/server'
import { jwtVerify, importX509, JWTPayload } from 'jose'
import fetch from 'node-fetch'

import type { NextRequest } from 'next/server'

interface TokenHeader {
  alg: string
  kid: string
  typ: string
}

const authAlgorithm = 'RS256'

const appID = 'your-app-id-here'
const tokenISS = `https://securetoken.google.com/${appID}`

function verifyFirebasePayload(payload: JWTPayload) {
  const currentDate = new Date()
  if (
    !payload ||
    (payload.exp ?? 0) * 1000 < currentDate.getTime() ||
    (payload.iat ?? currentDate.getTime()) * 1000 > currentDate.getTime() ||
    payload.aud !== appID ||
    payload.iss !== tokenISS ||
    !payload.sub ||
    !payload.user_id ||
    payload.sub !== payload.user_id ||
    (payload.auth_time as number) * 1000 > currentDate.getTime()
  ) {
    throw Error('Token expired')
  }
}

// function used to removed token cookies if it's invalid
function responseWithoutCookies(request: NextRequest) {
  const response = NextResponse.redirect(new URL('/', request.url))
  const { value, options } = request.cookies.getWithOptions('token')
  if (value) {
    response.cookies.set('token', value, options)
    response.cookies.delete('token')
  }
  return response
}

async function getPayloadFromToken(token: string) {
  const currentKeysx509: Record<string, string> = await (
    await fetch(
      'https://www.googleapis.com/robot/v1/metadata/x509/[email protected]'
    )
  ).json() // this may need to be improved, too many queries?
  const headerbase64 = token.split('.')[0] ?? ''
  const headerConverted = JSON.parse(Buffer.from(headerbase64, 'base64').toString()) as TokenHeader

  const matchKeyx509 = currentKeysx509[headerConverted.kid]
  if (!matchKeyx509) {
    throw Error('No match key')
  }
  const publicKey = await importX509(matchKeyx509, authAlgorithm)

  const { payload } = await jwtVerify(token, publicKey, {
    issuer: tokenISS,
    audience: appID,
    algorithms: [authAlgorithm]
  })
  return payload
}

async function middleware(request: NextRequest) {
  const isHomepage = request.nextUrl.pathname === '/'
  if (isHomepage || request.nextUrl.pathname.startsWith('/_next')) {
    return NextResponse.next()
  } // not necessary to process when the middleware is used internally
  const token = request.cookies.get('token')

  let isAuthenticated = false

  if (!isHomepage && token) {
    if (process.env.NEXT_PUBLIC_ENV !== 'prod') { // just for testing on `dev`
      return NextResponse.next()
    }
    try {
      const payload = await getPayloadFromToken(token)
      verifyFirebasePayload(payload)

      isAuthenticated = true
    } catch (error) {
      isAuthenticated = false
    }
  }

  if (isAuthenticated) {
    return NextResponse.next()
  }

  return responseWithoutCookies(request)
}

export default middleware

Upvotes: 0

Fiodorov Andrei
Fiodorov Andrei

Reputation: 2018

Following nextjs documentation:

next/server

The next/server module provides several exports for server-only helpers, such as Middleware.

Middleware

Middleware enables you to use code over configuration. This gives you full flexibility in Next.js, because you can run code before a request is completed. Based on the user's incoming request, you can modify the response by rewriting, redirecting, adding headers, or even streaming HTML.

The middleware is not a react component and cannot use hooks.

Upvotes: 1

Jasur Kurbanov
Jasur Kurbanov

Reputation: 840

According to React docs

Don’t call Hooks from regular JavaScript functions. Instead, you can:

✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks (we’ll learn about them on the next page).

So you're using hooks inside regular Javascript function below.

export async function middleware(req) { // this is regular JavaScript function
  const [user] = useAuthState(auth); // here you're using hook
  const { pathname } = req.nextUrl;

  if (!user) {
    return NextResponse.redirect("/login");
  }

  return NextResponse.next();
}

Solution for you case might be

import React, { useEffect } from "react";
import { NextResponse } from "next/server";
import { useAuthState } from "react-firebase-hooks/auth";

function login() {
  const [user] = useAuthState(auth); // here you're using hook
  const { pathname } = req.nextUrl;
  
  useEffect(() => {
    if (!user) {
      return NextResponse.redirect("/login");
    }
  
    return NextResponse.next();
  }, [user]);

  const signInWithGoogle = () => {
    console.log("Clicked!");
  };


  return (
    <div>
      <button onClick={signInWithGoogle}>Sign in with google</button>
    </div>
  );
}

export default login;

Don't forget to import requried imports in your component

Upvotes: 4

Related Questions