← Back to DevBytes

Vite Authentication: JWT, Sessions, and OAuth Integration

Introduction to Vite Authentication

Authentication is one of the most critical aspects of any modern web application. When building apps with Vite — the lightning-fast build tool and dev server — you have several authentication strategies to choose from. This tutorial covers the three most common approaches: JSON Web Tokens (JWT), server-side sessions, and OAuth integration with third-party providers.

By the end of this guide, you'll understand how each method works, when to use which, and how to implement them in a Vite-powered application with practical, production-ready code.

Why Authentication Strategy Matters in Vite

Vite is primarily a frontend build tool, which means authentication logic typically lives on a backend API. However, the way your Vite app handles tokens, cookies, and redirects directly impacts security, user experience, and scalability. Choosing the wrong strategy can lead to:

Understanding the tradeoffs between JWT, sessions, and OAuth helps you build apps that are both secure and maintainable.

Setting Up a Vite Project

Let's start by scaffolding a fresh Vite project with React. The same concepts apply to Vue, Svelte, or vanilla JS.

npm create vite@latest vite-auth-app -- --template react
cd vite-auth-app
npm install
npm install axios react-router-dom

We'll use axios for HTTP requests and react-router-dom for protected routing. Create a basic folder structure:

src/
  api/
    auth.js
  components/
    ProtectedRoute.jsx
    Login.jsx
    Dashboard.jsx
  context/
    AuthContext.jsx
  main.jsx
  App.jsx

Strategy 1: JWT Authentication

What Is JWT?

JSON Web Tokens are self-contained tokens that encode user information and claims in a signed, base64-encoded string. A JWT has three parts: a header, a payload, and a signature, separated by dots. Once issued by the server, the client sends the token with each request, typically in the Authorization header.

How JWT Works with Vite

The flow is straightforward: the user submits credentials, the server validates them and returns a JWT, and the Vite app stores the token and attaches it to subsequent API calls. The server verifies the token signature on each request without needing to look up session state in a database.

Implementing JWT on the Client

First, create an API helper that manages token storage and request interception:

// src/api/auth.js
import axios from "axios";

const API_URL = "http://localhost:3000/api";

const api = axios.create({
  baseURL: API_URL,
  withCredentials: true,
});

// Attach token to every request
api.interceptors.request.use((config) => {
  const token = localStorage.getItem("access_token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Handle token expiration globally
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response && error.response.status === 401) {
      localStorage.removeItem("access_token");
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

export const login = async (email, password) => {
  const response = await api.post("/auth/login", { email, password });
  localStorage.setItem("access_token", response.data.accessToken);
  return response.data;
};

export const getProfile = async () => {
  const response = await api.get("/auth/me");
  return response.data;
};

export const logout = () => {
  localStorage.removeItem("access_token");
};

export default api;

Next, build an auth context to manage user state across the app:

// src/context/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from "react";
import { getProfile, login as loginApi, logout as logoutApi } from "../api/auth";

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const token = localStorage.getItem("access_token");
    if (!token) {
      setLoading(false);
      return;
    }
    getProfile()
      .then(setUser)
      .catch(() => logoutApi())
      .finally(() => setLoading(false));
  }, []);

  const login = async (email, password) => {
    await loginApi(email, password);
    const profile = await getProfile();
    setUser(profile);
  };

  const logout = () => {
    logoutApi();
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, loading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

Protect routes with a wrapper component:

// src/components/ProtectedRoute.jsx
import { Navigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";

export default function ProtectedRoute({ children }) {
  const { user, loading } = useAuth();

  if (loading) return <p>Loading...</p>;
  if (!user) return <Navigate to="/login" replace />;

  return children;
}

JWT Best Practices

Strategy 2: Session-Based Authentication

What Are Sessions?

Session-based authentication relies on server-side storage. When a user logs in, the server creates a session record in memory or a database and sends a session ID to the client as a cookie. On each subsequent request, the browser automatically includes the cookie, and the server looks up the session to identify the user.

Why Choose Sessions Over JWT

Sessions are often simpler to secure because the session ID is opaque — it carries no user data. Revocation is trivial: just delete the session on the server. Sessions also work seamlessly with traditional CSRF protection and avoid the pitfalls of token storage on the client.

Implementing Sessions with Vite

The key difference from JWT is that you rely on cookies rather than manually attaching tokens. Configure axios to send credentials:

// src/api/auth.js
import axios from "axios";

const api = axios.create({
  baseURL: "http://localhost:3000/api",
  withCredentials: true, // Critical: send cookies with every request
});

export const login = async (email, password) => {
  const response = await api.post("/auth/login", { email, password });
  return response.data;
};

export const getProfile = async () => {
  const response = await api.get("/auth/me");
  return response.data;
};

export const logout = async () => {
  await api.post("/auth/logout");
};

export default api;

The auth context is similar, but without manual token management:

// src/context/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from "react";
import { getProfile, login as loginApi, logout as logoutApi } from "../api/auth";

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    getProfile()
      .then(setUser)
      .catch(() => setUser(null))
      .finally(() => setLoading(false));
  }, []);

  const login = async (email, password) => {
    await loginApi(email, password);
    const profile = await getProfile();
    setUser(profile);
  };

  const logout = async () => {
    await logoutApi();
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, loading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

Server-Side Cookie Configuration

For sessions to work securely, the backend must set cookies with the right attributes. Here's an example using Express:

// Server-side example (Express)
app.post("/api/auth/login", async (req, res) => {
  const user = await validateCredentials(req.body.email, req.body.password);
  if (!user) return res.status(401).json({ error: "Invalid credentials" });

  req.session.userId = user.id;

  res.cookie("sessionId", req.session.id, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
  });

  res.json({ user: { id: user.id, email: user.email } });
});

Session Best Practices

Strategy 3: OAuth Integration

What Is OAuth?

OAuth 2.0 is an authorization framework that lets users grant third-party applications access to their resources without sharing passwords. In practice, it powers "Login with Google," "Sign in with GitHub," and similar flows. OAuth is typically combined with JWT or sessions on your backend to maintain the user's authenticated state after the provider redirects back.

The OAuth Authorization Code Flow

The most secure OAuth flow for web apps is the Authorization Code flow with PKCE. The steps are:

Implementing OAuth in Vite

Start by creating a login page with provider buttons:

// src/components/Login.jsx
import { useAuth } from "../context/AuthContext";

const PROVIDERS = {
  google: "http://localhost:3000/api/auth/google",
  github: "http://localhost:api/auth/github",
};

export default function Login() {
  const { login } = useAuth();

  const handleOAuth = (provider) => {
    window.location.href = PROVIDERS[provider];
  };

  const handleLocalLogin = async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    await login(formData.get("email"), formData.get("password"));
  };

  return (
    <div>
      <h2>Sign In</h2>
      <form onSubmit={handleLocalLogin}>
        <input name="email" type="email" placeholder="Email" required />
        <input name="password" type="password" placeholder="Password" required />
        <button type="submit">Login</button>
      </form>
      <hr />
      <button onClick={() => handleOAuth("google")}>Login with Google</button>
      <button onClick={() => handleOAuth("github")}>Login with GitHub</button>
    </div>
  );
}

After the provider redirects back, your backend exchanges the code and redirects to your Vite app with either a cookie (session) or token in the URL. Here's how to handle the callback in your Vite app:

// src/components/OAuthCallback.jsx
import { useEffect } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useAuth } from "../context/AuthContext";

export default function OAuthCallback() {
  const [params] = useSearchParams();
  const navigate = useNavigate();
  const { user } = useAuth();

  useEffect(() => {
    const token = params.get("token");
    const error = params.get("error");

    if (error) {
      navigate("/login?error=" + encodeURIComponent(error));
      return;
    }

    if (token) {
      localStorage.setItem("access_token", token);
      navigate("/dashboard");
    } else if (user) {
      // Session cookie was already set by backend
      navigate("/dashboard");
    } else {
      navigate("/login");
    }
  }, [params, navigate, user]);

  return <p>Completing authentication...</p>;
}

Server-Side OAuth Handler Example

Here's a simplified Express handler using Passport.js for Google OAuth:

// Server-side (Express + Passport)
import passport from "passport";
import { GoogleStrategy } from "passport-google-oauth20";

passport.use(new GoogleStrategy({
  clientID: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  callbackURL: "/api/auth/google/callback",
}, async (accessToken, refreshToken, profile, done) => {
  const user = await findOrCreateUser(profile);
  done(null, user);
}));

app.get("/api/auth/google", passport.authenticate("google", {
  scope: ["profile", "email"],
}));

app.get("/api/auth/google/callback",
  passport.authenticate("google", { session: false }),
  (req, res) => {
    // Option A: Issue a JWT and redirect with token in URL
    const token = generateJWT(req.user);
    res.redirect(`http://localhost:5173/auth/callback?token=${token}`);

    // Option B: Set session cookie and redirect
    // res.cookie("sessionId", createSession(req.user), { httpOnly: true });
    // res.redirect("http://localhost:5173/auth/callback");
  }
);

OAuth Best Practices

Wiring Everything Together in App.jsx

Now let's connect all the pieces with routing:

// src/App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext";
import ProtectedRoute from "./components/ProtectedRoute";
import Login from "./components/Login";
import Dashboard from "./components/Dashboard";
import OAuthCallback from "./components/OAuthCallback";

export default function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/auth/callback" element={<OAuthCallback />} />
          <Route
            path="/dashboard"
            element={
              <ProtectedRoute>
                <Dashboard />
              </ProtectedRoute>
            }
          />
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
}

Handling Vite Dev Server Proxy

During development, your Vite dev server runs on port 5173 while your API runs elsewhere. To avoid CORS issues, configure a proxy in vite.config.js:

// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        changeOrigin: true,
      },
    },
  },
});

With this proxy, you can use relative URLs like /api/auth/login in your axios calls, and Vite will forward them to your backend during development. In production, your reverse proxy (Nginx, Caddy, etc.) handles the same routing.

Comparing the Three Strategies

Conclusion

Authentication in a Vite application is less about Vite itself and more about how your frontend interacts with your authentication backend. JWT offers stateless flexibility, sessions provide robust server-side security, and OAuth enables seamless third-party login experiences. The right choice depends on your architecture: use JWT for distributed APIs, sessions for monolithic backends, and OAuth when you want to offload identity to providers like Google or GitHub. Whichever strategy you pick, prioritize secure token storage, proper cookie attributes, and careful handling of redirects and callbacks. By following the patterns and best practices in this tutorial, you'll be well-equipped to build secure, production-ready authentication into your Vite applications.

— Ad —

Google AdSense will appear here after approval

← Back to all articles