DEV Community

Cover image for Building a Secure Auth System in Express (JWT, Redis, Refresh Tokens, and RBAC) and Automating It with a CLI
karabo seeisa
karabo seeisa

Posted on

Building a Secure Auth System in Express (JWT, Redis, Refresh Tokens, and RBAC) and Automating It with a CLI

Authentication in Express applications is often underestimated.

Most implementations stop at “generate a JWT and verify it,” but real-world systems require much more:

• Refresh token rotation

• Token invalidation

• Concurrency safety

• Role-based access control (RBAC)

• Stateful tracking (usually via Redis)

After building this stack multiple times, I decided to formalise it into a reusable system and eventually a CLI that scaffolds it in seconds.

This post breaks down the architecture behind it and how you can use it.


The Problem with Typical JWT Auth

A basic JWT setup usually looks like this:

TypeScript

const token = jwt.sign(user, secret, { expiresIn: "15m" });

Enter fullscreen mode Exit fullscreen mode

This works for simple cases, but breaks down quickly:

  1. No Token Revocation

Once issued, a JWT is valid until it expires.

You can’t easily invalidate it.

  1. Stateless Refresh = Security Risk

If refresh tokens are not tracked, they can be reused indefinitely.

  1. Concurrency Issues

If two refresh requests happen at the same time:

both can succeed

multiple valid tokens are created

This leads to token duplication and potential session abuse.


The Architecture I Settled On

To solve these issues
, the system uses:

Access Tokens (JWT)

• Short-lived (e.g. 15 minutes)

• Used for API authentication

• Stateless verification

Refresh Tokens (Stateful)

• Stored in Redis

• Rotated on every use

• Old tokens are invalidated

This gives you control over sessions.


Redis as a Token Store

Redis acts as the source of truth for refresh tokens:

• Track active sessions

• Invalidate tokens instantly

• Enforce single-use refresh tokens


Refresh Token Rotation

Every refresh request:

• Verifies the token

• Checks Redis for validity

• Deletes the old token

• Issues a new refresh token

This ensures:

A refresh token can only be used once.


Concurrency Safety

A key problem is handling simultaneous refresh requests.

Solution:

• Atomic operations in Redis

• Only one request can invalidate + rotate

• The second request fails with 401

This prevents replay attacks and token duplication.


RBAC (Role-Based Access Control)

Authentication is not enough , you also need authorization.

Example middleware:

TypeScript

app.get("/admin", auth.requireAdmin, (req, res) => {

res.json({ message: "Admin only" });

});

Enter fullscreen mode Exit fullscreen mode

This ensures only users with the correct role can access protected routes.

Putting It Together
The system exposes a simple interface:

TypeScript:

const auth = await createAuthenik8({

jwtSecret: process.env.JWT_SECRET!,

refreshSecret: process.env.REFRESH_SECRET!,

});

Enter fullscreen mode Exit fullscreen mode

From there:

signToken() → access tokens

generateRefreshToken() → refresh tokens

Refresh token() → rotation logic

requireAdmin → RBAC middleware

Automating the Setup
After building this repeatedly, I created a CLI to remove the setup step entirely:

Bash

npx create-authenik8-app my-app

Enter fullscreen mode Exit fullscreen mode

This generates:

• Express + TypeScript project

• JWT + refresh token system

• Redis integration

•RBAC middleware

• Preconfigured routes

You can go from zero → running auth server in minutes.

Why Redis is Required
A common question is: “Why not keep everything stateless?”

Because:

• You can’t revoke tokens

• You can’t prevent reuse

• You can’t track sessions

Redis enables:

• Immediate invalidation

• Session tracking

• Single-use refresh tokens


Trade-offs

This approach introduces:

• External dependency (Redis)

• Slight complexity increase

• Network latency for token validation

But in exchange, you get:

• Proper session control

• Stronger security guarantees

• Predictable auth behavior


Final Thoughts

Authentication is one of those systems that seems simple until it isn’t.

The difference between a basic implementation and a production-ready one is:

• handling edge cases

• controlling state

• preventing abuse

This project started as a way to standardize those decisions and avoid rewriting the same logic repeatedly.

If you’ve built auth systems before, I’d be interested in how you approach:

  • refresh token handling
  • revocation strategies
  • concurrency edge cases

Links:
Authenik8 site:https://authenik8.vercel.app/

Gitlab: https://gitlab.com/COD434/create-authenik8-app

Tiktok: https://www.tiktok.com/@thesbd8?\_r=1&\_t=ZS-9577WVfucT4

Try It

Bash

npx create-authenik8-app my-app
Enter fullscreen mode Exit fullscreen mode

Closing

If you’re working with Express and need a solid starting point for authentication, this should save you a significant amount of setup time.

Feedback is welcome.

Top comments (0)