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.
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 →