awinogrodzki / next-firebase-auth-edge

Next.js Firebase Authentication for Edge and Node.js runtimes. Compatible with latest Next.js features.
https://next-firebase-auth-edge-docs.vercel.app/
MIT License
489 stars 42 forks source link

How to verify email #208

Closed tech-a-go-go closed 2 months ago

tech-a-go-go commented 3 months ago

Thank you for the awesome library !

I tried the next-typescript-minimal example and now want to add an email verification feature.

This is my middleware.ts If user hasn't verified his/her email, the user is forced to redirect to /email_verification page.

import { NextRequest, NextResponse } from "next/server";
import { authMiddleware, redirectToHome, redirectToLogin } from "next-firebase-auth-edge";
import { clientConfig, serverConfig } from "./config";

const EMAIL_VERIFICATOIN_PATH = '/email_verification';
const PUBLIC_PATHS = [EMAIL_VERIFICATOIN_PATH, '/register', '/login'];

function redirectToEmailVerification(request: NextRequest) {
    const url = request.nextUrl.clone();
    url.pathname = '/email_verification';
    url.search = '';
    return NextResponse.redirect(url);
}

export async function middleware(request: NextRequest) {
    return authMiddleware(request, {
        loginPath: "/api/login",
        logoutPath: "/api/logout",
        apiKey: clientConfig.apiKey,
        cookieName: serverConfig.cookieName,
        cookieSignatureKeys: serverConfig.cookieSignatureKeys,
        cookieSerializeOptions: serverConfig.cookieSerializeOptions,
        serviceAccount: serverConfig.serviceAccount,
        handleValidToken: async ({token, decodedToken}, headers) => {

            // If the email is not verified, redirect to /email_verification.
            if (!decodedToken.email_verified) {
                if (EMAIL_VERIFICATOIN_PATH !== request.nextUrl.pathname) {
                    return redirectToEmailVerification(request);
                }
            } else {
                if (PUBLIC_PATHS.includes(request.nextUrl.pathname)) {
                    return redirectToHome(request);
                }
            }

            return NextResponse.next({
                request: {
                    headers
                }
            });
        },
        handleInvalidToken: async (reason) => {
            console.info('Missing or malformed credentials', {reason});

            return redirectToLogin(request, {
                path: '/login',
                publicPaths: PUBLIC_PATHS
            });
        },
        handleError: async (error) => {
            console.error('Unhandled authentication error', {error});

            return redirectToLogin(request, {
                path: '/login',
                publicPaths: PUBLIC_PATHS
            });
        }
    });
}

export const config = {
    matcher: ["/api/login", "/api/logout", "/((?!_next|favicon.ico|api|.*\\.|$).*)"],
  };

Here is /email_verificatoin/page.tsx

'use client';

import { sendEmailVerification, getAuth } from 'firebase/auth';
import { app } from "../../firebase";

export default function Page() {

  async function sendVerificationEmail() {
      const auth = getAuth(app);
      const user = auth.currentUser;

      user is null here !!

      await sendEmailVerification(user);
  }

  return (
    <div>
      <button onClick={sendVerificationEmail}>
        Send verification email.
      </button>
    </div>
  );
}

User can click the button to send a verification email but since currentUser from getAuth is null, I cannot send the verification email. (auth object is not null since the user has been logged in) How can I retrieve the user object so that I can use sendEmailVerification ?

Thanks a lot.

tech-a-go-go commented 3 months ago

I've figured out the problem. I had to use Auth#onAuthStateChanged to get User object.

Here is the working version of /email_verificatoin/page.tsx.

'use client';

import { sendEmailVerification, getAuth } from 'firebase/auth';
import { app } from "../../firebase";
import { useEffect, useState } from 'react';

export default function Page() {
  const [userLoaded, setUserLoaded] = useState<boolean>(false);
  const [mailSent, setMailSent] = useState<boolean>(false);

  const auth = getAuth(app);

  async function sendVerificationEmail() {
    const user = auth.currentUser;

    if (!user) {
      throw new Error('Something unexpected happened. User is not logged in.');
    }

    console.log("sending a verification email ...")
    await sendEmailVerification(user);

    setMailSent(true)
  }

  useEffect(() => {
    /**
     * @see {@link https://firebase.google.com/docs/auth/web/manage-users}
     */
    const unsubscribed = auth.onAuthStateChanged((user) => {
      if (user) {
      // Update the state to activate the button.
        setUserLoaded(true)
      }
    })
    return () => {
      // Must be unsubscribed when unmount.
      unsubscribed()
    }
  }, [auth])  

  return (
    <div>
      <button disabled={!userLoaded || mailSent} onClick={sendVerificationEmail}
        className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
        Send verification email.
      </button>

      {mailSent && <div>Verification email has been sent. Please check your email box.</div>}
    </div>
  );
}

But now I got new problem.

Even though I verified my email, decodedToken.email_verified never become true in /middleware.ts

export async function middleware(request: NextRequest) {
    return authMiddleware(request, {
        loginPath: "/api/login",
        logoutPath: "/api/logout",
        apiKey: clientConfig.apiKey,
        cookieName: serverConfig.cookieName,
        cookieSignatureKeys: serverConfig.cookieSignatureKeys,
        cookieSerializeOptions: serverConfig.cookieSerializeOptions,
        serviceAccount: serverConfig.serviceAccount,
        handleValidToken: async ({token, decodedToken}, headers) => {

            decodedToken.email_verified <---- always false even after email was verified.

            // If the email is not verified, redirect to /email_verification.
            if (!decodedToken.email_verified) {
                if (EMAIL_VERIFICATOIN_PATH !== request.nextUrl.pathname) {
                    return redirectToEmailVerification(request);
                }

Is decodedToken cached or something ? I want to know how to refresh the email_verified value after email is verified.

Thank you.

tech-a-go-go commented 2 months ago

sign out and in fixed the problem.