← Back to DevBytes

Tailwind CSS Authentication: JWT, Sessions, and OAuth Integration

Introduction to Tailwind CSS Authentication

Authentication is one of the most critical features in modern web applications. Whether you're building a simple login form or a complex multi-provider OAuth system, the user interface plays a significant role in the overall user experience. Tailwind CSS, with its utility-first approach, makes it incredibly efficient to build polished, responsive, and accessible authentication interfaces. In this tutorial, we'll explore how to build authentication UIs with Tailwind CSS and integrate them with JWT, session-based, and OAuth authentication strategies.

Why Authentication UI Matters

Authentication is the gateway to your application. A poorly designed auth flow can frustrate users, increase abandonment rates, and even introduce security vulnerabilities. Tailwind CSS helps address these concerns by allowing you to rapidly prototype and build consistent, accessible, and responsive auth components without writing custom CSS. Combined with proper backend authentication strategies like JWT, sessions, and OAuth, you can create a secure and seamless experience for your users.

Key Benefits of Using Tailwind for Auth UIs

Setting Up the Project

Before we dive into building authentication components, let's set up a project with Tailwind CSS. We'll use a simple HTML project with Vite for this tutorial, but the concepts apply to React, Vue, Next.js, or any other framework.

First, create a new project directory and initialize it:

mkdir tailwind-auth-tutorial
cd tailwind-auth-tutorial
npm init -y
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Configure your tailwind.config.js file to scan your HTML and JavaScript files:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./*.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {
      colors: {
        brand: {
          50: "#eff6ff",
          100: "#dbeafe",
          500: "#3b82f6",
          600: "#2563eb",
          700: "#1d4ed8",
        },
      },
    },
  },
  plugins: [],
};

Create a CSS file at ./src/input.css with the Tailwind directives:

@tailwind base;
@tailwind components;
@tailwind utilities;

Build the CSS:

npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch

Now you're ready to build authentication components with Tailwind CSS.

Building Auth UI Components with Tailwind

Let's start by building a clean, responsive login form. This form will serve as the foundation for all three authentication strategies we'll cover.

Login Form Component

Create an index.html file with the following login form:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Login | Tailwind Auth Tutorial</title>
  <link rel="stylesheet" href="https://agentechip.com/dist/output.css" />
</head>
<body class="bg-gray-50 min-h-screen flex items-center justify-center px-4">
  <div class="max-w-md w-full bg-white rounded-2xl shadow-lg p-8">
    <div class="text-center mb-8">
      <h1 class="text-3xl font-bold text-gray-900">Welcome back</h1>
      <p class="text-gray-500 mt-2">Sign in to your account</p>
    </div>

    <form id="loginForm" class="space-y-5">
      <div>
        <label for="email" class="block text-sm font-medium text-gray-700 mb-1">
          Email address
        </label>
        <input
          type="email"
          id="email"
          name="email"
          required
          class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none transition"
          placeholder="you@example.com"
        />
      </div>

      <div>
        <label for="password" class="block text-sm font-medium text-gray-700 mb-1">
          Password
        </label>
        <input
          type="password"
          id="password"
          name="password"
          required
          class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none transition"
          placeholder="••••••••"
        />
      </div>

      <div class="flex items-center justify-between">
        <label class="flex items-center gap-2 text-sm text-gray-600">
          <input type="checkbox" class="rounded border-gray-300 text-brand-600 focus:ring-brand-500" />
          Remember me
        </label>
        <a href="#" class="text-sm text-brand-600 hover:text-brand-700 font-medium">
          Forgot password?
        </a>
      </div>

      <button
        type="submit"
        class="w-full bg-brand-600 hover:bg-brand-700 text-white font-semibold py-2.5 rounded-lg transition focus:ring-2 focus:ring-offset-2 focus:ring-brand-500"
      >
        Sign in
      </button>
    </form>

    <div class="relative my-6">
      <div class="absolute inset-0 flex items-center">
        <div class="w-full border-t border-gray-200"></div>
      </div>
      <div class="relative flex justify-center text-sm">
        <span class="bg-white px-4 text-gray-400">Or continue with</span>
      </div>
    </div>

    <div class="grid grid-cols-2 gap-3">
      <button class="flex items-center justify-center gap-2 border border-gray-300 rounded-lg py-2.5 hover:bg-gray-50 transition">
        <svg class="w-5 h-5" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
        Google
      </button>
      <button class="flex items-center justify-center gap-2 border border-gray-300 rounded-lg py-2.5 hover:bg-gray-50 transition">
        <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
        GitHub
      </button>
    </div>

    <p class="text-center text-sm text-gray-500 mt-6">
      Don't have an account?
      <a href="#" class="text-brand-600 hover:text-brand-700 font-medium">Sign up</a>
    </p>
  </div>

  <script src="https://agentechip.com/src/auth.js"></script>
</body>
</html>

This form includes email and password fields, a remember me checkbox, a forgot password link, and OAuth provider buttons. The Tailwind classes provide a clean, modern look with proper focus states and hover effects.

Registration Form Component

Let's also create a registration form. You can place this in a separate register.html file:

<div class="max-w-md w-full bg-white rounded-2xl shadow-lg p-8">
  <div class="text-center mb-8">
    <h1 class="text-3xl font-bold text-gray-900">Create account</h1>
    <p class="text-gray-500 mt-2">Get started in just a few seconds</p>
  </div>

  <form id="registerForm" class="space-y-5">
    <div>
      <label for="name" class="block text-sm font-medium text-gray-700 mb-1">Full name</label>
      <input
        type="text"
        id="name"
        name="name"
        required
        class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none transition"
        placeholder="John Doe"
      />
    </div>

    <div>
      <label for="reg-email" class="block text-sm font-medium text-gray-700 mb-1">Email address</label>
      <input
        type="email"
        id="reg-email"
        name="email"
        required
        class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none transition"
        placeholder="you@example.com"
      />
    </div>

    <div>
      <label for="reg-password" class="block text-sm font-medium text-gray-700 mb-1">Password</label>
      <input
        type="password"
        id="reg-password"
        name="password"
        required
        minlength="8"
        class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none transition"
        placeholder="At least 8 characters"
      />
      <p class="text-xs text-gray-400 mt-1">Use a mix of letters, numbers, and symbols</p>
    </div>

    <label class="flex items-start gap-2 text-sm text-gray-600">
      <input type="checkbox" required class="mt-1 rounded border-gray-300 text-brand-600 focus:ring-brand-500" />
      <span>I agree to the <a href="#" class="text-brand-600 hover:text-brand-700">Terms of Service</a> and <a href="#" class="text-brand-600 hover:text-brand-700">Privacy Policy</a></span>
    </label>

    <button
      type="submit"
      class="w-full bg-brand-600 hover:bg-brand-700 text-white font-semibold py-2.5 rounded-lg transition focus:ring-2 focus:ring-offset-2 focus:ring-brand-500"
    >
      Create account
    </button>
  </form>
</div>

JWT Authentication

JSON Web Tokens (JWT) are a popular stateless authentication mechanism. When a user logs in, the server issues a signed token that the client stores and sends with subsequent requests. Let's build a complete JWT authentication flow.

How JWT Works

A JWT consists of three parts: a header, a payload, and a signature, separated by dots. The header specifies the token type and signing algorithm. The payload contains claims like user ID and expiration time. The signature ensures the token hasn't been tampered with.

Backend: Login Endpoint

Here's a Node.js Express endpoint that issues JWT tokens on successful login:

// server.js
const express = require("express");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const cors = require("cors");

const app = express();
app.use(cors());
app.use(express.json());

const JWT_SECRET = "your-super-secret-key-change-in-production";
const JWT_EXPIRES_IN = "1h";

// Mock user database
const users = [
  {
    id: 1,
    email: "user@example.com",
    // Hash of "password123"
    passwordHash: "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy",
  },
];

app.post("/api/auth/login", async (req, res) => {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ error: "Email and password are required" });
  }

  const user = users.find((u) => u.email === email);
  if (!user) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  const isMatch = await bcrypt.compare(password, user.passwordHash);
  if (!isMatch) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  const token = jwt.sign(
    { userId: user.id, email: user.email },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRES_IN }
  );

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

app.listen(3000, () => console.log("Server running on port 3000"));

Frontend: JWT Login Handler

Now let's handle the login form submission on the client side. We'll store the JWT in localStorage and attach it to future requests:

// src/auth.js

const loginForm = document.getElementById("loginForm");

if (loginForm) {
  loginForm.addEventListener("submit", async (e) => {
    e.preventDefault();

    const email = document.getElementById("email").value;
    const password = document.getElementById("password").value;
    const submitBtn = loginForm.querySelector('button[type="submit"]');

    // Show loading state
    submitBtn.disabled = true;
    submitBtn.textContent = "Signing in...";

    try {
      const response = await fetch("http://localhost:3000/api/auth/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || "Login failed");
      }

      // Store the JWT token
      localStorage.setItem("authToken", data.token);
      localStorage.setItem("user", JSON.stringify(data.user));

      // Redirect to dashboard
      window.location.href = "/dashboard.html";
    } catch (error) {
      showError(error.message);
    } finally {
      submitBtn.disabled = false;
      submitBtn.textContent = "Sign in";
    }
  });
}

function showError(message) {
  const existing = document.querySelector(".error-banner");
  if (existing) existing.remove();

  const banner = document.createElement("div");
  banner.className =
    "error-banner bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm mb-4";
  banner.textContent = message;

  const form = document.getElementById("loginForm");
  form.parentNode.insertBefore(banner, form);
}

// Helper to get the auth token
function getAuthToken() {
  return localStorage.getItem("authToken");
}

// Helper to make authenticated requests
async function authFetch(url, options = {}) {
  const token = getAuthToken();
  if (!token) {
    window.location.href = "/index.html";
    return;
  }

  const headers = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${token}`,
    ...options.headers,
  };

  const response = await fetch(url, { ...options, headers });

  if (response.status === 401) {
    localStorage.removeItem("authToken");
    localStorage.removeItem("user");
    window.location.href = "/index.html";
    return;
  }

  return response;
}

Protected Dashboard Page

Here's a dashboard page that requires authentication. It uses the authFetch helper to include the JWT in requests:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Dashboard</title>
  <link rel="stylesheet" href="https://agentechip.com/dist/output.css" />
</head>
<body class="bg-gray-50 min-h-screen">
  <nav class="bg-white shadow-sm border-b border-gray-200">
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
      <div class="flex justify-between h-16 items-center">
        <h1 class="text-xl font-bold text-gray-900">Dashboard</h1>
        <button
          id="logoutBtn"
          class="text-sm text-gray-600 hover:text-gray-900 font-medium px-4 py-2 rounded-lg hover:bg-gray-100 transition"
        >
          Logout
        </button>
      </div>
    </div>
  </nav>

  <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
    <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
      <h2 class="text-2xl font-bold text-gray-900 mb-2">Welcome, <span id="userName">User</span>!</h2>
      <p class="text-gray-500">You are successfully authenticated with JWT.</p>

      <div class="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-4">
        <div class="bg-brand-50 rounded-xl p-5">
          <p class="text-sm text-brand-700 font-medium">Total Projects</p>
          <p class="text-3xl font-bold text-brand-700 mt-1">12</p>
        </div>
        <div class="bg-green-50 rounded-xl p-5">
          <p class="text-sm text-green-700 font-medium">Active Tasks</p>
          <p class="text-3xl font-bold text-green-700 mt-1">5</p>
        </div>
        <div class="bg-purple-50 rounded-xl p-5">
          <p class="text-sm text-purple-700 font-medium">Completed</p>
          <p class="text-3xl font-bold text-purple-700 mt-1">28</p>
        </div>
      </div>
    </div>
  </main>

  <script src="https://agentechip.com/src/auth.js"></script>
  <script>
    // Check authentication on page load
    const token = localStorage.getItem("authToken");
    const user = JSON.parse(localStorage.getItem("user") || "null");

    if (!token || !user) {
      window.location.href = "/index.html";
    } else {
      document.getElementById("userName").textContent = user.email;
    }

    document.getElementById("logoutBtn").addEventListener("click", () => {
      localStorage.removeItem("authToken");
      localStorage.removeItem("user");
      window.location.href = "/index.html";
    });
  </script>
</body>
</html>

Backend: Protected Route with JWT Verification

On the server side, create middleware to verify JWT tokens on protected routes:

// middleware/auth.js
const jwt = require("jsonwebtoken");
const JWT_SECRET = "your-super-secret-key-change-in-production";

function authenticateToken(req, res, next) {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1];

  if (!token) {
    return res.status(401).json({ error: "Access token required" });
  }

  jwt.verify(token, JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: "Invalid or expired token" });
    }
    req.user = user;
    next();
  });
}

module.exports = { authenticateToken };

// In server.js, protect routes like this:
const { authenticateToken } = require("./middleware/auth");

app.get("/api/profile", authenticateToken, (req, res) => {
  res.json({
    user: {
      id: req.user.userId,
      email: req.user.email,
    },
  });
});

JWT Token Refresh

Since JWTs expire, you should implement a token refresh mechanism. Here's how to add a refresh endpoint:

// server.js - Add refresh token endpoint
app.post("/api/auth/refresh", authenticateToken, (req, res) => {
  const newToken = jwt.sign(
    { userId: req.user.userId, email: req.user.email },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRES_IN }
  );

  res.json({ token: newToken });
});

// src/auth.js - Auto-refresh before expiry
function setupTokenRefresh() {
  const token = getAuthToken();
  if (!token) return;

  try {
    const payload = JSON.parse(atob(token.split(".")[1]));
    const expiresIn = payload.exp * 1000 - Date.now();
    const refreshIn = expiresIn - 60000; // Refresh 1 minute before expiry

    if (refreshIn <= 0) {
      refreshToken();
    } else {
      setTimeout(refreshToken, refreshIn);
    }
  } catch (e) {
    console.error("Failed to parse token", e);
  }
}

async function refreshToken() {
  try {
    const response = await authFetch("http://localhost:3000/api/auth/refresh", {
      method: "POST",
    });
    const data = await response.json();
    localStorage.setItem("authToken", data.token);
    setupTokenRefresh();
  } catch (error) {
    console.error("Token refresh failed", error);
    localStorage.removeItem("authToken");
    localStorage.removeItem("user");
    window.location.href = "/index.html";
  }
}

Session-Based Authentication

Session-based authentication is a stateful approach where the server stores session data and sends a session ID to the client via a cookie. This is the traditional approach and is still widely used, especially with server-rendered applications.

How Sessions Work

When a user logs in, the server creates a session, stores it in memory or a session store like Redis, and sends the session ID to the client as an HTTP-only cookie. On subsequent requests, the browser automatically includes the cookie, and the server looks up the session to identify the user.

Backend: Session Login with Express

// server-session.js
const express = require("express");
const session = require("express-session");
const bcrypt = require("bcryptjs");
const cors = require("cors");

const app = express();

app.use(cors({ origin: "http://localhost:5173", credentials: true }));
app.use(express.json());
app.use(
  session({
    secret: "your-session-secret-change-in-production",
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: false, // Set to true in production with HTTPS
      maxAge: 24 * 60 * 60 * 1000, // 24 hours
    },
  })
);

const users = [
  {
    id: 1,
    email: "user@example.com",
    passwordHash: "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy",
  },
];

// Login endpoint
app.post("/api/session/login", async (req, res) => {
  const { email, password } = req.body;

  const user = users.find((u) => u.email === email);
  if (!user) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  const isMatch = await bcrypt.compare(password, user.passwordHash);
  if (!isMatch) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  req.session.userId = user.id;
  req.session.email = user.email;

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

// Protected route
app.get("/api/session/profile", (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: "Not authenticated" });
  }

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

// Logout endpoint
app.post("/api/session/logout", (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ error: "Logout failed" });
    }
    res.clearCookie("connect.sid");
    res.json({ message: "Logged out successfully" });
  });
});

app.listen(3001, () => console.log("Session server running on port 3001"));

Frontend: Session-Based Login

With session-based auth, the browser handles cookies automatically. You just need to ensure credentials: "include" is set on fetch requests:

// src/session-auth.js

const sessionLoginForm = document.getElementById("loginForm");

if (sessionLoginForm) {
  sessionLoginForm.addEventListener("submit", async (e) => {
    e.preventDefault();

    const email = document.getElementById("email").value;
    const password = document.getElementById("password").value;
    const submitBtn = sessionLoginForm.querySelector('button[type="submit"]');

    submitBtn.disabled = true;
    submitBtn.textContent = "Signing in...";

    try {
      const response = await fetch("http://localhost:3001/api/session/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "include", // Important: include cookies
        body: JSON.stringify({ email, password }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || "Login failed");
      }

      // No need to store anything in localStorage - cookie is set automatically
      window.location.href = "/dashboard.html";
    } catch (error) {
      showError(error.message);
    } finally {
      submitBtn.disabled = false;
      submitBtn.textContent = "Sign in";
    }
  });
}

// Check session status on protected pages
async function checkSession() {
  try {
    const response = await fetch("http://localhost:3001/api/session/profile", {
      credentials: "include",
    });

    if (!response.ok) {
      window.location.href = "/index.html";
      return null;
    }

    return await response.json();
  } catch (error) {
    window.location.href = "/index.html";
    return null;
  }
}

// Logout
async function sessionLogout() {
  await fetch("http://localhost:3001/api/session/logout", {
    method: "POST",
    credentials: "include",
  });
  window.location.href = "/index.html";
}

Session Dashboard Page

<script>
  // On dashboard load, verify session
  (async () => {
    const data = await checkSession();
    if (data) {
      document.getElementById("userName").textContent = data.user.email;
    }
  })();

  document.getElementById("logoutBtn").addEventListener("click", sessionLogout);
</script>

OAuth Integration

OAuth allows users to authenticate using third-party providers like Google, GitHub, Facebook, or Twitter. This provides a convenient sign-in experience and reduces the need for users to remember yet another password. Let's implement OAuth with Google and GitHub using Passport.js.

How OAuth Works

OAuth 2.0 follows an authorization code flow. The user is redirected to the provider's login page, grants permission, and is redirected back to your application with an authorization code. Your server exchanges this code for an access token and user profile information.

Backend: OAuth with Passport.js

First, install the required packages:

npm install passport passport-google-oauth20 passport-github2 express-session

Then configure Passport strategies:

// server-oauth.js
const express = require("express");
const session = require("express-session");
const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const GitHubStrategy = require("passport-github2").Strategy;
const jwt = require("jsonwebtoken");

const app = express();

const JWT_SECRET = "your-jwt-secret";
const CLIENT_URL = "http://localhost:5173";

app.use(
  session({
    secret: "oauth-session-secret",
    resave: false,
    saveUninitialized: false,
  })
);

app.use(passport.initialize());
app.use(passport.session());

// Serialize and deserialize user for session
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));

// Google OAuth Strategy
passport.use(
  new GoogleStrategy(
    {
      clientID: "your-google-client-id",
      clientSecret: "your-google-client-secret",
      callbackURL: "/api/auth/google/callback",
    },
    (accessToken, refreshToken, profile, done) => {
      // In production, find or create user in your database
      const user = {
        id: profile.id,
        email: profile.emails[0].value,
        name: profile.displayName,
        provider: "google",
      };
      return done(null, user);
    }
  )
);

// GitHub OAuth Strategy
passport.use(
  new GitHubStrategy(
    {
      clientID: "

🛠 Tools from DevBytes

Inventory Tracker Pro — Excel inventory system, low-stock alerts · $19
AI Dev Kit for Mac — local AI dev environment templates · $9.99
KeyMapper for Mac — custom keyboard shortcut toolkit · $7.99

← Back to all articles