Reputation:
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:
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
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
Reputation: 2018
Following nextjs documentation:
The next/server module provides several exports for server-only helpers, such as 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
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