Authentication#

Buntok provides a zero-dependency JWT implementation using WebCrypto (built-in). Supports HMAC-SHA256 with expiration, Bearer tokens, and HttpOnly cookies.

Info
No additional packages needed! JWT is already included in @buntok/core.

JwtService#

Creating Instance#

typescript
import { JwtService } from "@buntok/core";

const jwt = new JwtService("your-secret-key");

Sign Token#

typescript
// Sign token (expires in 1 hour)
const token = await jwt.sign(
  { userId: 1, role: "admin" },
  3600  // seconds
);

// Sign token (expires in 7 days)
const token = await jwt.sign(
  { userId: 1, role: "user" },
  7 * 24 * 60 * 60
);

Verify Token#

typescript
const payload = await jwt.verify(token);
// { userId: 1, role: "admin", exp: 1700000000 } or null if invalid

// Handle expired/invalid token
const payload = await jwt.verify(token);
if (!payload) {
  // Token is invalid or expired
}

requireAuth Middleware#

Extracts and verifies JWT from cookie or Authorization header. Injects the payload into ctx.user.

typescript
import { requireAuth } from "@buntok/core";

const secret = "your-secret-key";

// Basic usage
app.get("/protected", requireAuth(secret), (ctx) => {
  const user = ctx.user as { userId: number; role: string };
  return ctx.json({ user });
});

Set AUTH_COOKIE in your .env to enable cookie-based authentication. When set, requireAuth reads JWT from that cookie first, then falls back to the Authorization header.

Setup#

typescript
# .env (created by buntok init)
AUTH_STORE=cookie
AUTH_COOKIE=session

Login Flow#

typescript
import { Buntok } from "@buntok/core";
import { JwtService } from "@buntok/core";
import { setCookie, deleteCookie } from "@buntok/core/helpers";

const jwt = new JwtService(process.env.JWT_SECRET!);

// Login endpoint - set HttpOnly cookie
app.post("/login", async (ctx) => {
  const { email, password } = await ctx.body();

  const user = await db.user.findUnique({ where: { email } });
  if (!user || !await verifyPassword(password, user.password)) {
    return ctx.json({ error: "Invalid credentials" }, 401);
  }

  const token = await jwt.sign(
    { userId: user.id, role: user.role },
    86400  // 24 hours
  );

  // Set HttpOnly cookie (read by requireAuth automatically)
  const response = ctx.json({ success: true });
  return setCookie(response, process.env.AUTH_COOKIE!, token, {
    httpOnly: true,
    secure: true,       // HTTPS only
    sameSite: "lax",
    path: "/",
    maxAge: 86400,      // 24 hours
  });
});

// Logout endpoint - clear cookie
app.post("/logout", (ctx) => {
  const response = ctx.json({ success: true });
  return deleteCookie(response, process.env.AUTH_COOKIE!, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
  });
});
Info
When AUTH_STORE=cookie, requireAuth reads JWT from the HttpOnly cookie named in AUTH_COOKIE. Falls back to Authorization: Bearer <token> if cookie is missing.

How It Works#

Env VariableDefaultDescription
AUTH_STOREheaderWhere to read JWT: header or cookie
AUTH_COOKIEsessionCookie name (only used when AUTH_STORE=cookie)

Bearer Token (Header)#

The client can also send the token in the Authorization header:

typescript
// HTTP Header
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

// Example with fetch
const response = await fetch("/api/profile", {
  headers: {
    "Authorization": `Bearer ${token}`,
  },
});

Example: Login Flow#

typescript
import { Buntok } from "@buntok/core";
import { JwtService, requireAuth } from "@buntok/core";

const jwt = new JwtService(process.env.JWT_SECRET!);

// Login endpoint
app.post("/login", async (ctx) => {
  const { email, password } = await ctx.body();

  // Find user in database
  const user = await db.user.findUnique({ where: { email } });
  if (!user || !await verifyPassword(password, user.password)) {
    return ctx.json({ error: "Invalid credentials" }, 401);
  }

  // Generate token
  const token = await jwt.sign(
    { userId: user.id, role: user.role },
    86400  // 24 hours
  );

  return ctx.json({ token });
});

// Protected endpoint
app.get("/profile", requireAuth(secret), async (ctx) => {
  const { userId } = ctx.user as { userId: number };

  const user = await db.user.findUnique({
    where: { id: userId },
    select: { id: true, name: true, email: true },
  });

  return ctx.json({ user });
});

Error Responses#

StatusMessageCondition
401Missing or invalid authentication tokenNo token found in cookie or Authorization header
401Token is invalid or expiredToken has expired or is invalid

Best Practices#

  • Store JWT_SECRET in environment variables, not in code
  • Use appropriate expiration times (1 hour for access tokens, 7 days for refresh tokens)
  • Always use HTTPS in production
  • Use httpOnly: true for cookies to prevent XSS attacks
  • Use sameSite: "lax" or "strict" for CSRF protection
  • Use RBAC for role-based access control

Next Steps#