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:
- Vulnerable token storage (XSS-prone localStorage usage)
- Poor user experience with frequent logouts or refresh loops
- Difficulty scaling across subdomains or microservices
- OAuth flow breakage due to CORS or redirect misconfiguration
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
- Use short-lived access tokens (15 minutes) paired with refresh tokens
- Store refresh tokens in HTTP-only cookies, not localStorage
- Never store sensitive data in the JWT payload — it's encoded, not encrypted
- Validate tokens on the server using a strong secret or public key
- Implement token revocation lists for forced logouts
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
- Always set
httpOnly,secure, andsameSitecookie flags - Use a session store like Redis for production scalability
- Rotate session IDs after login and privilege changes
- Implement CSRF tokens for state-changing requests
- Set reasonable session expiration times
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:
- Your Vite app redirects the user to the provider's authorization URL
- The user logs in and grants permission on the provider's site
- The provider redirects back to your callback URL with an authorization code
- Your backend exchanges the code for an access token
- Your backend creates a session or JWT for the user and responds to the client
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
- Always use the Authorization Code flow with PKCE for SPAs
- Validate the
stateparameter to prevent CSRF attacks - Store provider tokens securely and never expose them to the browser
- Handle account linking carefully to avoid account takeover vulnerabilities
- Provide a fallback local login option for users who lose provider access
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
- JWT: Best for stateless APIs, microservices, and mobile clients. Requires careful token storage and refresh handling.
- Sessions: Best for traditional web apps with a single backend. Simpler revocation and strong security with HTTP-only cookies.
- OAuth: Best for delegating authentication to trusted providers and reducing password management burden. Usually combined with JWT or sessions.
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.