User Authentication in React
Learn how to implement user authentication in a React application using Nhost
React user authentication sign up sign in email verification password authenticationThis tutorial part builds upon the Protected Routes part by adding complete email/password authentication with email verification functionality. You’ll implement sign up, sign in, email verification, and sign out features to create a full authentication flow.
Full-Stack React Development with Nhost
Section titled “Full-Stack React Development with Nhost”Prerequisites
Section titled “Prerequisites”- Complete the Protected Routes part first
- The project from the previous part set up and running
Step-by-Step Guide
Section titled “Step-by-Step Guide”Create the Sign In Page
Section titled “Create the Sign In Page”Build a comprehensive sign-in form with proper error handling and loading states. This page handles user authentication and includes special logic for post-verification sign-in.
import { useEffect, useId, useState } from "react";import { Link, useNavigate } from "react-router-dom";import { useAuth } from "../lib/nhost/AuthProvider";
export default function SignIn() { const { nhost, isAuthenticated } = useAuth(); const navigate = useNavigate();
const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null);
const emailId = useId(); const passwordId = useId();
// Use useEffect for navigation after authentication is confirmed useEffect(() => { if (isAuthenticated) { navigate("/profile"); } }, [isAuthenticated, navigate]);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); setIsLoading(true); setError(null);
try { // Use the signIn function from auth context const response = await nhost.auth.signInEmailPassword({ email, password, });
// If we have a session, sign in was successful if (response.body?.session) { navigate("/profile"); } else { setError("Failed to sign in. Please check your credentials."); } } catch (err) { const message = (err as Error).message || "Unknown error"; setError(`An error occurred during sign in: ${message}`); } finally { setIsLoading(false); } };
return ( <div> <h1>Sign In</h1>
<form onSubmit={handleSubmit} className="auth-form"> <div className="auth-form-field"> <label htmlFor={emailId}>Email</label> <input id={emailId} type="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="auth-input" /> </div>
<div className="auth-form-field"> <label htmlFor={passwordId}>Password</label> <input id={passwordId} type="password" value={password} onChange={(e) => setPassword(e.target.value)} required className="auth-input" /> </div>
{error && <div className="auth-error">{error}</div>}
<button type="submit" disabled={isLoading} className={`auth-button secondary`} > {isLoading ? "Signing In..." : "Sign In"} </button> </form>
<div className="auth-links"> <p> Don't have an account? <Link to="/signup">Sign Up</Link> </p> </div> </div> );}Create the Sign Up Page
Section titled “Create the Sign Up Page”Implement user registration with email verification flow. This page collects user information, creates accounts, and guides users through the email verification process.
import { generatePKCEPair } from "@nhost/nhost-js/auth";import { useEffect, useId, useState } from "react";import { Link, useNavigate } from "react-router-dom";import { useAuth } from "../lib/nhost/AuthProvider";
export default function SignUp() { const { nhost, isAuthenticated } = useAuth(); const navigate = useNavigate();
const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [displayName, setDisplayName] = useState(""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [success, setSuccess] = useState(false); const displayNameId = useId();
const emailId = useId(); const passwordId = useId();
// Redirect authenticated users to profile useEffect(() => { if (isAuthenticated) { navigate("/profile"); } }, [isAuthenticated, navigate]);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); setIsLoading(true); setError(null); setSuccess(false);
try { // Generate PKCE pair and store verifier for email verification const { verifier, challenge } = await generatePKCEPair(); localStorage.setItem("nhost_pkce_verifier", verifier);
const response = await nhost.auth.signUpEmailPassword({ email, password, options: { displayName, redirectTo: `${window.location.origin}/verify`, }, codeChallenge: challenge, });
if (response.body?.session) { // Successfully signed up and automatically signed in navigate("/profile"); } else { // Verification email sent setSuccess(true); } } catch (err) { const message = (err as Error).message || "Unknown error"; setError(`An error occurred during sign up: ${message}`); } finally { setIsLoading(false); } };
if (success) { return ( <div> <h1>Check Your Email</h1> <div className="success-message"> <p> We've sent a verification link to <strong>{email}</strong> </p> <p> Please check your email and click the verification link to activate your account. </p> </div> <p> <Link to="/signin">Back to Sign In</Link> </p> </div> ); }
return ( <div> <h1>Sign Up</h1>
<form onSubmit={handleSubmit} className="auth-form"> <div className="auth-form-field"> <label htmlFor={displayNameId}>Display Name</label> <input id={displayNameId} type="text" value={displayName} onChange={(e) => setDisplayName(e.target.value)} required className="auth-input" /> </div>
<div className="auth-form-field"> <label htmlFor={emailId}>Email</label> <input id={emailId} type="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="auth-input" /> </div>
<div className="auth-form-field"> <label htmlFor={passwordId}>Password</label> <input id={passwordId} type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={8} className="auth-input" /> <small className="help-text">Minimum 8 characters</small> </div>
{error && <div className="auth-error">{error}</div>}
<button type="submit" disabled={isLoading} className={`auth-button primary`} > {isLoading ? "Creating Account..." : "Sign Up"} </button> </form>
<div className="auth-links"> <p> Already have an account? <Link to="/signin">Sign In</Link> </p> </div> </div> );}Create the Email Verification Page
Section titled “Create the Email Verification Page”Build a dedicated verification page that exchanges authorization codes for sessions using PKCE. This page handles the verification flow when users click the email verification link.
import { useEffect, useState } from "react";import { useLocation, useNavigate } from "react-router-dom";import { useAuth } from "../lib/nhost/AuthProvider";
const PKCE_VERIFIER_KEY = "nhost_pkce_verifier";
function consumePKCEVerifier(): string | null { const verifier = localStorage.getItem(PKCE_VERIFIER_KEY); if (verifier) { localStorage.removeItem(PKCE_VERIFIER_KEY); } return verifier;}
export default function Verify() { const location = useLocation(); const navigate = useNavigate();
const [status, setStatus] = useState<"verifying" | "success" | "error">( "verifying", ); const [error, setError] = useState<string>(""); const [urlParams, setUrlParams] = useState<Record<string, string>>({});
const { nhost } = useAuth();
useEffect(() => { const params = new URLSearchParams(location.search); const code = params.get("code");
if (!code) { const allParams: Record<string, string> = {}; params.forEach((value, key) => { allParams[key] = value; }); setUrlParams(allParams);
setStatus("error"); setError("No authorization code found in URL"); return; }
const authCode = code; let isMounted = true;
async function exchangeCode(): Promise<void> { try { // Small delay to ensure component is fully mounted await new Promise((resolve) => setTimeout(resolve, 500));
if (!isMounted) return;
const codeVerifier = consumePKCEVerifier(); if (!codeVerifier) { setStatus("error"); setError( "No PKCE verifier found. The sign-in must be initiated from the same browser tab.", ); return; }
await nhost.auth.tokenExchange({ code: authCode, codeVerifier });
if (!isMounted) return;
setStatus("success");
setTimeout(() => { if (isMounted) navigate("/profile"); }, 1500); } catch (err) { const message = (err as Error).message || "Unknown error"; if (!isMounted) return;
setStatus("error"); setError(`An error occurred during verification: ${message}`); } }
exchangeCode();
return () => { isMounted = false; }; }, [location.search, navigate, nhost.auth]);
return ( <div> <h1>Email Verification</h1>
<div className="page-center"> {status === "verifying" && ( <div> <p className="margin-bottom">Verifying your email...</p> <div className="spinner-verify" /> <style>{` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `}</style> </div> )}
{status === "success" && ( <div> <p className="verification-status">✓ Successfully verified!</p> <p>You'll be redirected to your profile page shortly...</p> </div> )}
{status === "error" && ( <div> <p className="verification-status error">Verification failed</p> <p className="margin-bottom">{error}</p>
{Object.keys(urlParams).length > 0 && ( <div className="debug-panel"> <p className="debug-title">URL Parameters:</p> {Object.entries(urlParams).map(([key, value]) => ( <div key={key} className="debug-item"> <span className="debug-key">{key}:</span>{" "} <span className="debug-value">{value}</span> </div> ))} </div> )}
<button type="button" onClick={() => navigate("/signin")} className="auth-button secondary" > Back to Sign In </button> </div> )} </div> </div> );}Update the App Component to Include New Routes
Section titled “Update the App Component to Include New Routes”Configure your application’s routing structure to include the new authentication pages. This integrates all the authentication flows into your app’s navigation.
import { createBrowserRouter, createRoutesFromElements, Navigate, Outlet, Route, RouterProvider,} from "react-router-dom";import Navigation from "./components/Navigation";import ProtectedRoute from "./components/ProtectedRoute";import { AuthProvider } from "./lib/nhost/AuthProvider";import Home from "./pages/Home";import Profile from "./pages/Profile";import SignIn from "./pages/SignIn";import SignUp from "./pages/SignUp";import Verify from "./pages/Verify";
// Root layout component to wrap all routesconst RootLayout = () => { return ( <> <Navigation /> <div className="app-content"> <Outlet /> </div> </> );};
// Create router with routesconst router = createBrowserRouter( createRoutesFromElements( <Route element={<RootLayout />}> <Route index element={<Home />} /> <Route path="signin" element={<SignIn />} /> <Route path="signup" element={<SignUp />} /> <Route path="verify" element={<Verify />} /> <Route element={<ProtectedRoute />}> <Route path="profile" element={<Profile />} /> </Route> <Route path="*" element={<Navigate to="/" />} /> </Route>, ),);
function App() { return ( <AuthProvider> <RouterProvider router={router} /> </AuthProvider> );}
export default App;Add Navigation Links and Sign Out Functionality
Section titled “Add Navigation Links and Sign Out Functionality”Update the navigation component to include links to the sign-in and sign-up pages, and implement the sign-out.
import { Link, useNavigate } from "react-router-dom";import { useAuth } from "../lib/nhost/AuthProvider";
export default function Navigation() { const { isAuthenticated, session, nhost } = useAuth(); const navigate = useNavigate();
const handleSignOut = async () => { try { if (session) { await nhost.auth.signOut({ refreshToken: session.refreshToken, }); } navigate("/"); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); console.error("Error signing out:", message); } };
return ( <nav className="navigation"> <div className="nav-container"> <Link to="/" className="nav-logo"> Nhost React Demo </Link>
<div className="nav-links"> <Link to="/" className="nav-link"> Home </Link>
{isAuthenticated ? ( <> <Link to="/profile" className="nav-link"> Profile </Link> <button type="button" onClick={handleSignOut} className="nav-link nav-button" > Sign Out </button> </> ) : ( <> <Link to="/signin" className="nav-link"> Sign In </Link> <Link to="/signup" className="nav-link"> Sign Up </Link> </> )} </div> </div> </nav> );}Run and Test the Application
Section titled “Run and Test the Application”Start your development server and test the complete authentication flow to ensure everything works properly.
npm run devThings to try out:
- Try signing up with a new email address. Check your email for the verification link and click it. See how you are sent to the verification page and then redirected to your profile.
- Try signing out and then signing back in with the same credentials.
- Notice how navigation links change based on authentication state showing “Sign In” and “Sign Up” when logged out, and “Profile” and “Sign Out” when logged in.
- Check how the homepage also reflects the authentication state with appropriate messages.
- Open multiple tabs and test signing out from one tab to see how other tabs respond. Now sign back in and see the changes propagate across tabs.
Key Features Demonstrated
Section titled “Key Features Demonstrated”Complete Registration Flow
Full email/password registration with proper form validation and user feedback.
Email Verification
Custom /verify endpoint that securely processes email verification tokens.
Error Handling
Comprehensive error handling for unverified emails, failed authentication, and network issues.
Visual Feedback
Loading states, success messages, and clear error displays throughout the authentication flow.
Session Management
Complete sign out functionality and proper session state management across the application.