Passport Strategies

Strategy-Based Auth with koa-passport and Local Strategy

Passport Strategies

koa-passport ports the Passport.js strategy ecosystem to Koa's async middleware model. This lesson wires up the local strategy with koa-session for a classic username/password login flow.

4 min read Level 3/5 #koa#passport#auth
What you'll learn
  • Install and initialize koa-passport with koa-session
  • Configure passport-local with a secure verify callback
  • Implement serialize/deserialize to persist the user across requests

koa-passport wraps Passport.js to work with Koa’s ctx and async/await. It gives you access to the same 500+ strategies (local, Google, GitHub, SAML, etc.) without changing how Koa middleware is written.

Installation

npm install koa-passport passport-local koa-session bcryptjs

Wiring It Up

Passport depends on sessions, so koa-session must be mounted before koa-passport.

import Koa from "koa";
import session from "koa-session";
import passport from "koa-passport";

const app = new Koa();

app.keys = [process.env.SESSION_KEY_1, process.env.SESSION_KEY_2];
app.use(session({ signed: true, httpOnly: true, sameSite: "lax" }, app));

app.use(passport.initialize());
app.use(passport.session()); // reads ctx.session.passport.user

Local Strategy

import { Strategy as LocalStrategy } from "passport-local";
import bcrypt from "bcryptjs";
import { findUserByUsername } from "./db.js";

passport.use(
  new LocalStrategy(async (username, password, done) => {
    try {
      const user = await findUserByUsername(username);
      if (!user) return done(null, false, { message: "Unknown user" });

      const ok = await bcrypt.compare(password, user.passwordHash);
      if (!ok) return done(null, false, { message: "Bad password" });

      return done(null, user);
    } catch (err) {
      return done(err);
    }
  })
);

Serialize / Deserialize

Passport stores only the user ID in the session and reloads the full user object on each request via deserializeUser.

passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
  try {
    const user = await findUserById(id);
    done(null, user ?? false);
  } catch (err) {
    done(err);
  }
});

Login and Logout Routes

import Router from "@koa/router";

const router = new Router();

router.post("/login", passport.authenticate("local"), async (ctx) => {
  // Reached only on success; ctx.state.user is the deserialized user.
  ctx.body = { ok: true, userId: ctx.state.user.id };
});

router.post("/logout", async (ctx) => {
  await ctx.logout(); // koa-passport adds this helper
  ctx.body = { ok: true };
});

// Guard middleware
async function requireAuth(ctx, next) {
  if (!ctx.isAuthenticated()) {
    ctx.status = 401;
    ctx.body = { error: "Not authenticated" };
    return;
  }
  await next();
}

router.get("/me", requireAuth, async (ctx) => {
  ctx.body = ctx.state.user;
});

ctx.state.user vs ctx.user

koa-passport places the authenticated user on ctx.state.user (Koa convention), not ctx.user (Express/Passport convention). Both are available, but prefer ctx.state.user for consistency with other Koa middleware.

Up Next

Extend Passport to support social login (Google, GitHub) through OAuth 2.0 strategies.

OAuth & Social Login →