Back to List

Building a Lab-Only Login System

Concepts and flow for implementing registration, login, and Google social login with Express + Passport.js. The first step in research data access control.

Intermediate
|
60min
|
Verified (2026-06)
LoginPassport.jsOAuthRegistrationAccess ControlSocial Login
Progress0/19 (0%)

Building a Lab-Only Login System

So far you've built pages with HTML, set up servers with Express, stored samples in databases, and managed sessions with cookies. At this point, a natural question arises:

"Can't we make this web app accessible only to our lab members?"

If your sample management system contains sensitive experiment data, it shouldn't be open to everyone. You might want to share results only with a collaborative research group, or show presentation materials only to logged-in reviewers.

That's when you need a login system.

From Cookies to Login

In the auth-cookies topic, you learned about cookies and sessions โ€” the mechanism for the server to remember "this browser belongs to someone who logged in earlier."

But one thing was missing โ€” how do you verify "the person logging in"? The process of receiving an ID and password, comparing them against what's in the database, and creating a session if they match. Building this yourself leads to complex code and easy security mistakes.

Passport.js is the tool that handles this complex authentication process for you.

Passport.js: Authentication Middleware

Passport.js is an authentication middleware for Express โ€” a tool built specifically for handling login.

bash
npm install passport
npm install passport-local

The core concept is strategies. Each authentication method has its own strategy package:

StrategyPackageAuth Method
Localpassport-localEmail + password
Googlepassport-google-oauth20Google account
GitHubpassport-github2GitHub account
JWTpassport-jwtJSON Web Token

Passport.js currently has over 300 registered strategies. Just install the strategy for the auth method you need.

Passport Initial Setup: Plugging into Express

The basic configuration for connecting Passport to Express:

javascript
const express = require("express");
const session = require("express-session");
const passport = require("passport");

const app = express();

app.use(express.urlencoded({ extended: false }));
app.use(session({
  secret: "lab-secret-key",
  resave: false,
  saveUninitialized: false,
}));
app.use(passport.initialize());
app.use(passport.session());

passport.initialize() โ€” registers Passport with Express. passport.session() โ€” restores user information stored in the session on every request.

Used together with express-session from the auth-cookies topic. If sessions are "memory," Passport decides "who should be remembered."

serializeUser and deserializeUser

When a user logs in, you need to store their info in the session. But putting the entire user object in the session is heavy. Passport solves this with the serialize/deserialize pattern:

javascript
passport.serializeUser(function(user, done) {
  done(null, user.id);
});

passport.deserializeUser(function(id, done) {
  db.query("SELECT * FROM users WHERE id = ?", [id], function(err, rows) {
    done(err, rows[0]);
  });
});

serializeUser โ€” runs on login success. Decides what to store in the session. Usually just user.id. deserializeUser โ€” runs on every request. Finds the user in the database using the stored id and puts it in req.user.

Analogy โ€” writing only the membership number on a library card (serialize), then looking up member info by that number later (deserialize).

Local Authentication: Email + Password

The most basic login method. The user enters email and password, the server checks against the database, and processes the login.

javascript
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const bcrypt = require("bcrypt");

passport.use(new LocalStrategy(
  { usernameField: "email" },
  function(email, password, done) {
    db.query("SELECT * FROM users WHERE email = ?", [email], function(err, rows) {
      if (err) return done(err);
      if (rows.length === 0) return done(null, false, { message: "Unregistered email" });

      const user = rows[0];
      const isMatch = bcrypt.compareSync(password, user.password_hash);
      if (!isMatch) return done(null, false, { message: "Incorrect password" });

      return done(null, user);
    });
  }
));

done(null, user) โ€” authentication success. Passport passes this user to serializeUser. done(null, false) โ€” authentication failure. Not an error, but login denied. done(err) โ€” server error (DB connection failure, etc.).

What Passport handles:

  • Passes email/password from the login form to the strategy
  • On auth success: serializeUser โ†’ stores user ID in session
  • On subsequent requests: deserializeUser โ†’ access the logged-in user via req.user

Same pattern as Express middleware (app.use()) you learned before. Plug Passport in as middleware, and you can check the logged-in user with req.user in routes.

Registration: Adding New Researchers

Login requires registration first. The full flow:

text
User โ†’ Registration form (enter email, password)
โ†’ Server: Check email duplication
โ†’ Server: Hash the password (bcrypt)
โ†’ Server: Store hashed password in database
โ†’ Login enabled

Implemented as an Express route:

javascript
const bcrypt = require("bcrypt");

app.post("/register", function(req, res) {
  const { email, password, name } = req.body;

  // 1. Check email duplication
  db.query("SELECT * FROM users WHERE email = ?", [email], function(err, rows) {
    if (rows.length > 0) {
      return res.status(400).send("Email already registered");
    }

    // 2. Hash the password
    const hashedPassword = bcrypt.hashSync(password, 10);

    // 3. Save to database
    db.query(
      "INSERT INTO users (email, password_hash, name, role) VALUES (?, ?, ?, ?)",
      [email, hashedPassword, name, "member"],
      function(err) {
        if (err) return res.status(500).send("Registration failed");
        res.redirect("/login");
      }
    );
  });
});

Never store passwords in plain text. Always convert them with a hash function like bcrypt before storing.

javascript
const bcrypt = require("bcrypt");

// Registration: plain text โ†’ hash
const hashedPassword = bcrypt.hashSync("myLabPassword", 10);
// โ†’ Stores hash like "$2b$10$X7YKz..."

// Login: compare input hash with stored hash
const isMatch = bcrypt.compareSync("myLabPassword", hashedPassword);
// โ†’ true or false

bcrypt.hashSync(password, 10) โ€” 10 is the salt round (hash iteration count). Higher = more secure but slower. 10 is the commonly recommended value.

This is like lab security. If building access card codes were stored in plain text, every door opens when the database is breached. Hashing is like making cards impossible to duplicate.

Login/Logout Routes

After registration, the actual login/logout routes:

javascript
// Render login page
app.get("/login", function(req, res) {
  res.send(`
    <form action="/login" method="POST">
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">Login</button>
    </form>
  `);
});

// Process login โ€” Passport auto-runs LocalStrategy
app.post("/login",
  passport.authenticate("local", {
    successRedirect: "/dashboard",
    failureRedirect: "/login",
  })
);

// Logout
app.get("/logout", function(req, res) {
  req.logout(function(err) {
    if (err) return res.status(500).send("Logout failed");
    res.redirect("/");
  });
});

passport.authenticate("local") โ€” specifying the strategy name "local" tells Passport to run that strategy. On success it goes to successRedirect, on failure to failureRedirect. "local" is the default name registered by passport-local.

Access Control: Who Can See What

The real purpose of a login system is access control. "This page is for logged-in users only," "This feature is admin only" โ€” that's what we implement.

javascript
// Middleware allowing only logged-in users
function requireLogin(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect("/login");
}

// Sample data โ€” login required
app.get("/samples/confidential", requireLogin, function(req, res) {
  res.json({ data: "Confidential experiment data", user: req.user.email });
});

// Public page โ€” anyone can access
app.get("/publications", function(req, res) {
  res.json({ papers: publicPapers });
});

These scenarios are common in bio research:

ScenarioAccess Control
Internal lab sample managementLab members only
Collaborative research data sharingRegistered research group only
Research presentation materialsLogged-in reviewers only
Equipment reservation systemLogged-in researchers only
Results dashboardPI sees all, students see own data only

Multiple Users and Roles

With multiple users, you need roles:

javascript
// User table structure
// id | email | password_hash | role
// 1  | pi@lab.com | $2b$10$... | admin
// 2  | grad1@lab.com | $2b$10$... | member
// 3  | intern@lab.com | $2b$10$... | viewer

function requireAdmin(req, res, next) {
  if (req.isAuthenticated() && req.user.role === "admin") {
    return next();
  }
  res.status(403).json({ error: "Admin privileges required" });
}

// Delete sample โ€” admin only
app.delete("/sample/:id", requireAdmin, function(req, res) {
  // deletion logic
});

PI (professor) is admin, grad students are members, interns are viewers โ€” translating the lab's permission structure directly into code.

OAuth 2.0: How Social Login Works

You've likely seen "Sign in with Google" buttons. That's OAuth 2.0.

The core principle explained with an analogy โ€” it's like the hotel key card issuance process:

text
1. Guest (user) wants to check into hotel (my web app)
2. Hotel can't verify identity on its own
3. Hotel: "Please verify your identity at the front desk (Google)"
4. Guest shows passport (Google ID/password) at front desk
5. Front desk issues temporary voucher (Authorization Code)
6. Hotel exchanges voucher + business license (Client Secret) for key card (Access Token)
7. Key card allows lookup of guest info (name, email)

Key point: my web app never sees the user's Google password. Google verifies identity on our behalf and only passes a token saying "this person is verified."

OAuth Cast of 3

RoleIdentityAnalogy
Resource OwnerUser (researcher)Hotel guest
ClientMy web app (sample management system)Hotel
Resource ServerGoogle, GitHub, etc.Front desk (identity verification)

Using Google OAuth with Passport.js:

javascript
const GoogleStrategy = require("passport-google-oauth20").Strategy;

passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: "/auth/google/callback"
  },
  function(accessToken, refreshToken, profile, done) {
    // profile.emails[0].value โ†’ user email
    // Find in database or create new
    done(null, user);
  }
));

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

clientID and clientSecret are obtained from Google Cloud Console โ€” the process of registering "I'm building this app and want to delegate user authentication to Google."

OAuth Key Terms

Client ID and Client Secret

When you register an app in Google Cloud Console, you receive two things:

TermAnalogyPurpose
Client IDBusiness registration numberCan be public. Identifies "this app is requesting auth"
Client SecretCompany sealStrictly private. Used only for server โ†’ Google secret authentication

If Client Secret is leaked, others can make auth requests to Google pretending to be your app.

Redirect URI (Callback URL)

The address registered as "after Google login, send them back here":

text
https://my-lab-app.com/auth/google/callback

The Redirect URI registered in Google Cloud Console and the callbackURL in your code must match exactly. Even one character difference causes an error. This is a security measure โ€” preventing auth results from going to the wrong site.

Scope: The Valet Key Analogy

OAuth's scope defines "how much access to allow."

Think of valet parking. When you hand over a valet key โ€” they can start the car and park it, but can't open the trunk and the glove box stays locked. Only the necessary permissions are granted.

javascript
passport.authenticate("google", { scope: ["profile", "email"] })
scopeAccess RangeValet Key Analogy
profileName, profile pictureDriver's seat access (can park)
emailEmail addressDashboard check (contact info)
drive.readonlyRead Google Drive filesOpening trunk (checking cargo)
calendarRead/write calendarModifying in-car schedule

For our lab login, profile and email are sufficient. We only need to know "who this person is." Requesting unnecessary scopes makes users nervous, and Google may require app review.

Authorization Code Flow (Full Flow)

The safest and most common OAuth 2.0 method:

text
1. User clicks "Sign in with Google"
   โ†’ Browser navigates to Google login page

2. User enters Google ID/password
   โ†’ Google asks "Allow this app to access your name and email?"

3. User clicks "Allow"
   โ†’ Google sends Authorization Code to our server's Redirect URI

4. Our server sends Authorization Code + Client Secret to Google
   โ†’ Google issues Access Token

5. Our server queries user profile (name, email) with Access Token
   โ†’ Passport's callback function processes login with this info

Key point: Authorization Code is a one-time voucher. It alone can't access user info โ€” it must be combined with Client Secret to exchange for an Access Token. As long as both aren't compromised simultaneously, it's secure.

Google OAuth Route Setup

The actual Express routes:

javascript
// 1. "Sign in with Google" button leads here
app.get("/auth/google",
  passport.authenticate("google", { scope: ["profile", "email"] })
);

// 2. Google redirects here after auth (Redirect URI)
app.get("/auth/google/callback",
  passport.authenticate("google", { failureRedirect: "/login" }),
  function(req, res) {
    res.redirect("/dashboard");
  }
);

From the user's perspective, pressing one "Sign in with Google" button is all it takes. Behind the scenes, Authorization Code โ†’ Access Token โ†’ profile lookup happens automatically.

Why Use Social Login

BenefitDescription
User convenienceNo need to create a new password
Security delegationPassword storage/management burden delegated to Google
ReliabilityGoogle already provides 2FA, etc.
Quick implementationJust install Passport strategy + configure

Especially useful for internal lab tools. Researchers already have Google accounts, so they can access immediately with a single "Sign in with Google" click without separate registration.

Big Picture: Authentication System Layers

Stacking everything learned so far:

text
[auth-cookies]    Cookies & Sessions โ€” "I remember this browser"
      โ†“
[login-system]    Login โ€” "I verify who you are"
      โ†“
      โ”œโ”€โ”€ Local auth (email + password + bcrypt)
      โ”œโ”€โ”€ OAuth 2.0 (Google/GitHub social login)
      โ””โ”€โ”€ Role-based access control (admin/member/viewer)

If cookies/sessions are "memory," login is "verification," and roles are "permissions." Building analogy โ€” even with an access badge (cookie), you can't enter every room. Some rooms require a PI card, others open with any researcher card.

Try It Yourself (Faded Example)

Fill in the blanks to complete a Passport.js local authentication strategy.

Fill in the Blanksjavascript
const passport = require("passport");
const = require("passport-local").Strategy;
const bcrypt = require("bcrypt");
passport.use(new LocalStrategy(
{ usernameField: "" },
function(email, password, done) {
db.query("SELECT * FROM users WHERE email = ?", [email], function(err, rows) {
if (rows.length === 0) return done(null, );
const user = rows[0];
const isMatch = bcrypt.(password, user.password_hash);
if (!isMatch) return done(null, false);
return done(null, );
});
}
));

Next Steps

This topic aims to understand the structure and flow of login systems. In actual implementation:

  1. passport-local + bcrypt โ€” implement email/password login
  2. passport-google-oauth20 โ€” add Google social login
  3. Role middleware โ€” access control with requireLogin, requireAdmin, etc.
  4. HTTPS โ€” ensure login info isn't transmitted in plain text over the network

BioPlayground itself uses Supabase's authentication system. Whether you implement with Passport.js directly or use a BaaS like Supabase โ€” the concepts from this topic (hashing, OAuth flow, role-based control) apply equally.

Common Errors & Solutions

Q: What happens if I store passwords without hashing?

If the database is breached, all user passwords are exposed. Since many people reuse the same password across multiple sites, one breach leads to cascading damage. Hashing is not optional โ€” it's mandatory.

Q: Where should I store the OAuth Client Secret?

Never put it directly in code. Store it in environment variables (.env file) and add .env to .gitignore so it doesn't get uploaded to Git. If leaked, someone can impersonate your app.

Q: Can I build login without Passport.js?

Yes. You can compare passwords with bcrypt and store user info in sessions yourself. But implementing social login, session serialization, error handling, etc. manually makes code complex. Passport.js is a framework that reduces this repetitive work.

Q: If I use Supabase, do I not need to build this myself?

Correct. Supabase provides email/password auth, Google social login, and role-based access control through configuration alone. But to understand "what Supabase is doing internally," you need the concepts from this topic. Someone who understands the principles solves problems faster, even when using tools.

๐Ÿ’ฌ Questions & Comments

0 comments

You can post without signing in. Guest comments cannot be edited or deleted by their author.

0/2000

Loading...