Server-Side Sessions, app.keys, and Login/Logout Flow
Sessions with koa-session
koa-session adds encrypted, signed cookie-backed sessions to Koa. Learn how to configure app.keys, choose a session store, and implement a complete login and logout flow.
What you'll learn
- Configure koa-session with app.keys for signed cookies
- Swap the default memory store for a Redis-backed store
- Implement login, authenticated routes, and logout
koa-session stores session data on the server and places only a session
ID in a signed cookie on the client. The cookie cannot be tampered with because
Koa’s built-in cookie signing uses HMAC-SHA256 with your app.keys.
Installation
npm install koa-session Basic Setup
app.keys is a required prerequisite for signed cookies and sessions.
Provide at least two keys so you can rotate them without immediately invalidating
existing sessions.
import Koa from "koa";
import session from "koa-session";
const app = new Koa();
// Rotate keys: the first key is used to sign; all keys are used to verify.
app.keys = [process.env.SESSION_KEY_1, process.env.SESSION_KEY_2];
const SESSION_CONFIG = {
key: "koa.sess", // cookie name
maxAge: 86_400_000, // 1 day in ms
httpOnly: true, // JS cannot access the cookie
secure: process.env.NODE_ENV === "production",
sameSite: "lax", // CSRF protection
signed: true, // HMAC signature (requires app.keys)
rolling: false, // don't reset maxAge on every request
};
app.use(session(SESSION_CONFIG, app)); Login and Logout
import Router from "@koa/router";
import bcrypt from "bcryptjs";
import { findUserByEmail } from "./db.js";
const router = new Router();
router.post("/login", async (ctx) => {
const { email, password } = ctx.request.body;
const user = await findUserByEmail(email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
ctx.status = 401;
ctx.body = { error: "Invalid credentials" };
return;
}
// Store only a minimal, non-sensitive payload in the session.
ctx.session.userId = user.id;
ctx.session.role = user.role;
ctx.body = { ok: true };
});
router.post("/logout", async (ctx) => {
ctx.session = null; // clears the session and the cookie
ctx.body = { ok: true };
}); Protecting Routes
async function requireAuth(ctx, next) {
if (!ctx.session.userId) {
ctx.status = 401;
ctx.body = { error: "Not authenticated" };
return;
}
await next();
}
router.get("/dashboard", requireAuth, async (ctx) => {
ctx.body = { userId: ctx.session.userId };
}); Store Options
| Store | Package | Notes |
|---|---|---|
| Memory (default) | built-in | Dev only — lost on restart |
| Redis | koa-redis | Production standard |
| PostgreSQL | custom get/set/destroy | Fits existing DB setups |
Pass a custom store as store in the session config. The store must implement
get(key, maxAge, { ctx }), set(key, sess, maxAge, { ctx }), and
destroy(key, { ctx }).
Up Next
Learn how to use raw signed cookies — useful for lightweight state that doesn’t need a server-side store.
Cookies in Koa →