← Back to DevBytes

Koa Authentication: JWT, Sessions, and OAuth Integration

Understanding Authentication in Koa

Koa is a modern, lightweight Node.js web framework designed by the team behind Express. Its middleware-first approach and use of async/await make it an excellent choice for building APIs and web applications. Authentication—verifying who a user is—is a foundational requirement for nearly every production application. In this tutorial, we’ll explore three common authentication patterns in Koa: JSON Web Tokens (JWT), traditional server-side sessions, and OAuth integration with third-party providers. You’ll learn what each method is, why it matters, how to implement it step by step, and the best practices that keep your users and data safe.

What is Koa Authentication?

Koa authentication refers to the strategies and middleware used to identify a user during an HTTP request. It can be as simple as checking a password against a database record, or as complex as federated login through Google, GitHub, or other OAuth providers. In Koa, authentication typically happens in a middleware layer that runs before your route handlers. This middleware can attach a user object to ctx.state or ctx.session, allowing downstream handlers to make authorization decisions.

Why Authentication Matters

Without authentication, your application has no way to distinguish between different users or to protect sensitive data. Authentication enables personalization, access control, and audit trails. A broken authentication system can lead to data breaches, account takeovers, and compliance failures. Koa’s middleware architecture makes it easy to implement robust authentication that is both modular and testable, but you must understand the trade-offs between stateless JWT tokens, stateful session cookies, and delegated OAuth flows to pick the right tool for your project.

JWT Authentication in Koa

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

JSON Web Tokens (JWT) provide a stateless mechanism for authentication. A server issues a signed token after a successful login, and the client includes that token in subsequent requests (usually in the Authorization header). The server verifies the token’s signature and extracts user information without needing to store server-side session state. This is ideal for REST APIs, microservices, and mobile backends where horizontal scaling is important.

Setting Up koa-jwt

Install the required packages:

npm install koa koa-jwt jsonwebtoken koa-router koa-body

koa-jwt is a middleware that automatically verifies JWT tokens and makes the decoded payload available on ctx.state.user. You can exclude certain paths (like the login route) from authentication using the unless option.

const Koa = require('koa');
const jwt = require('koa-jwt');
const Router = require('koa-router');
const koaBody = require('koa-body');

const app = new Koa();
const router = new Router();

// JWT secret – store in environment variable in production!
const JWT_SECRET = 'your-256-bit-secret-key-change-me';

// Apply JWT middleware globally, except for public/login routes
app.use(koaBody());
app.use(jwt({ secret: JWT_SECRET }).unless({ path: [/^\/public/, /^\/login/] }));

// Custom middleware to extract user info if token exists (optional)
app.use(async (ctx, next) => {
  // ctx.state.user is set by koa-jwt when token is valid
  await next();
});

Generating Tokens on Login

When a user logs in, validate their credentials and, if successful, sign a JWT with the jsonwebtoken library. Include claims like the user ID (sub), role, and an expiration time (expiresIn).

const jwtSign = require('jsonwebtoken').sign;

// POST /login - authenticate user and return JWT
router.post('/login', async (ctx) => {
  const { username, password } = ctx.request.body;

  // Validate user credentials (pseudo-code: replace with real DB lookup)
  const user = await findUserByUsername(username);
  if (!user || !verifyPassword(password, user.hash)) {
    ctx.throw(401, 'Invalid credentials');
  }

  // Sign token with user ID and role
  const token = jwtSign(
    {
      sub: user.id,
      role: user.role,
    },
    JWT_SECRET,
    { expiresIn: '1h' }
  );

  ctx.body = { token, expiresIn: 3600 };
});

// Protected route example
router.get('/dashboard', async (ctx) => {
  // koa-jwt already verified token; user info is in ctx.state.user
  ctx.body = {
    message: `Welcome, user #${ctx.state.user.sub}`,
    role: ctx.state.user.role,
  };
});

app.use(router.routes());
app.listen(3000);

Protecting Routes with JWT Middleware

While koa-jwt handles verification automatically, you might sometimes need a custom middleware for more control—for example, to check specific claims or to handle token extraction from cookies instead of headers. Below is a custom JWT guard:

const jwt = require('jsonwebtoken');

app.use(async (ctx, next) => {
  // Allow public routes to proceed without token
  if (ctx.path.startsWith('/public') || ctx.path === '/login') {
    await next();
    return;
  }

  // Extract token from Authorization header (Bearer token)
  const authHeader = ctx.request.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    ctx.throw(401, 'Missing or malformed token');
  }

  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    ctx.state.user = decoded; // attach to state for downstream use
  } catch (err) {
    ctx.throw(401, 'Invalid or expired token');
  }

  await next();
});

Refreshing Tokens

JWTs are often short-lived (e.g., 15 minutes) to limit damage if leaked. A refresh token (stored securely, e.g., in an HTTP-only cookie) allows clients to obtain a new access token without re-entering credentials. Implement a /refresh endpoint that verifies the refresh token and issues a new JWT:

router.post('/refresh', async (ctx) => {
  const { refreshToken } = ctx.request.body;
  // Verify refresh token from secure storage (DB or Redis)
  const stored = await getRefreshTokenEntry(refreshToken);
  if (!stored || stored.expires < Date.now()) {
    ctx.throw(401, 'Invalid refresh token');
  }

  const user = await getUserById(stored.userId);
  const newAccessToken = jwtSign(
    { sub: user.id, role: user.role },
    JWT_SECRET,
    { expiresIn: '15m' }
  );

  ctx.body = { accessToken: newAccessToken };
});

Session-Based Authentication in Koa

Session-based authentication relies on a server-side session store (memory, database, or Redis) and a cookie containing a session identifier. After a user logs in, their identity is stored in the session, and the server looks it up on every request. This approach is stateful but straightforward, works well with traditional server-rendered applications, and avoids exposing user data in tokens.

Installing and Configuring koa-session

The koa-session middleware provides signed cookie-based sessions out of the box. It requires app keys for cookie signing and a configuration object.

npm install koa koa-session koa-router koa-body
const Koa = require('koa');
const session = require('koa-session');
const Router = require('koa-router');
const koaBody = require('koa-body');

const app = new Koa();
const router = new Router();

// Set keys for signing session cookies (use long, random strings)
app.keys = ['some secret key', 'another secret key'];

// Session configuration
const SESSION_CONFIG = {
  key: 'koa.sess',           // cookie name
  maxAge: 86400000,          // 1 day in ms
  httpOnly: true,            // prevent client-side JS access
  signed: true,              // sign cookie to prevent tampering
  rolling: true,             // refresh maxAge on each response
  renew: true,               // renew session when it's near expiry
  secure: false,             // set to true in production (HTTPS only)
  sameSite: 'lax',           // protect against CSRF
};

app.use(koaBody());
app.use(session(SESSION_CONFIG, app));

// Sample login route
router.post('/login', async (ctx) => {
  const { username, password } = ctx.request.body;
  const user = await validateUser(username, password);
  if (!user) {
    ctx.throw(401, 'Invalid credentials');
  }
  // Store user info in session
  ctx.session.user = { id: user.id, name: user.name, role: user.role };
  ctx.body = { message: 'Logged in successfully' };
});

// Protected dashboard
router.get('/dashboard', async (ctx) => {
  if (!ctx.session.user) {
    ctx.throw(401, 'Please log in');
  }
  ctx.body = `Hello, ${ctx.session.user.name}! Your role is ${ctx.session.user.role}.`;
});

// Logout – destroy session
router.post('/logout', async (ctx) => {
  ctx.session = null; // destroy session
  ctx.body = { message: 'Logged out' };
});

app.use(router.routes());
app.listen(3000);

Session Security Best Practices

Using an External Session Store (Redis)

For production deployments with multiple Koa processes, an in-memory session store won’t work because sessions would be isolated per process. You can integrate koa-session with an external store:

const Redis = require('ioredis');
const redisStore = require('koa-redis');
const redisClient = new Redis();

app.use(session({
  ...SESSION_CONFIG,
  store: redisStore({ client: redisClient })
}, app));

OAuth Integration in Koa

OAuth 2.0 allows users to log in using their existing accounts from providers like Google, GitHub, or Facebook. Instead of managing passwords yourself, you delegate authentication to the provider, receive an access token, and use it to fetch the user’s profile. In Koa, the easiest way to add OAuth is with the koa-passport middleware, which adapts the popular Passport.js library for Koa’s async context.

Setting Up Passport in Koa

You’ll need koa-passport, passport, and a strategy package for each provider (e.g., passport-google-oauth20). Passport requires serialize/deserialize functions to translate a user instance to and from a session identifier.

npm install koa koa-passport passport passport-google-oauth20 koa-session
const Koa = require('koa');
const passport = require('koa-passport');
const session = require('koa-session');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

// Session middleware is required for OAuth state and temporary data
app.keys = ['super-secret-oauth-key'];
app.use(session({}, app));

// Initialize Passport
app.use(passport.initialize());
app.use(passport.session()); // uses session to persist login across requests

// ---- Google OAuth Strategy ----
const GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: "http://localhost:3000/auth/google/callback"
  },
  async (accessToken, refreshToken, profile, done) => {
    // profile contains id, displayName, emails, etc.
    // Find or create a user in your database
    try {
      let user = await findUserByGoogleId(profile.id);
      if (!user) {
        user = await createUserFromGoogleProfile(profile);
      }
      return done(null, user); // user object attached to ctx.state.user later
    } catch (err) {
      return done(err);
    }
  }
));

// Serialize/Deserialize user for session storage
passport.serializeUser((user, done) => {
  // Store user ID in session
  done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
  try {
    const user = await getUserById(id);
    done(null, user); // full user object available in ctx.state.user
  } catch (err) {
    done(err);
  }
});

Routes for OAuth Login

Define routes to start the authentication flow and handle the callback. Passport automatically manages redirects, state validation, and token exchange.

// Start authentication – redirects user to Google
router.get('/auth/google',
  passport.authenticate('google', {
    scope: ['profile', 'email'],
    // Optional: prompt user to select account
    prompt: 'select_account'
  })
);

// Callback URL after Google authenticates user
router.get('/auth/google/callback',
  passport.authenticate('google', {
    failureRedirect: '/login',
    // successRedirect can also be used, or handle manually
  }),
  async (ctx) => {
    // ctx.state.user is now populated by passport
    ctx.body = {
      message: `Welcome, ${ctx.state.user.name}`,
      user: ctx.state.user
    };
  }
);

app.use(router.routes());
app.listen(3000);

Combining OAuth with JWT or Sessions

After a successful OAuth login, you can either keep the user in a session (as shown above) or issue a JWT for stateless API access. For a hybrid approach, after passport authenticates and sets ctx.state.user, issue a JWT and send it to the client:

const jwt = require('jsonwebtoken');

router.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  async (ctx) => {
    const user = ctx.state.user;
    const token = jwt.sign(
      { sub: user.id, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    ctx.body = { token, user: { id: user.id, name: user.name } };
  }
);

Authentication Best Practices in Koa

Conclusion

Authentication in Koa is remarkably flexible thanks to its middleware ecosystem. JWT offers a stateless, scalable approach perfect for APIs and microservices. Server-side sessions provide a traditional, straightforward model ideal for server-rendered apps with minimal token exposure. OAuth integration through Passport lets you delegate authentication to trusted third parties, reducing the burden of credential management while still giving you control over user identity. Regardless of the method you choose, always follow security best practices: encrypt, sign, validate, and monitor. With the patterns and code examples in this tutorial, you have a complete foundation to implement robust authentication in your Koa applications. Build on these examples, adapt them to your data layer, and keep security at the forefront of every authentication decision.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles