Authentication

Katal provides JWT-based authentication using Bun's native crypto. No third-party JWT library is required.


Quick Setup

import { Application, Auth, createAuthMiddleware } from "katal";

const app = new Application({ port: 3000 });

// 1. Create the Auth instance
const auth = new Auth({
  secret:    process.env.JWT_SECRET!,  // must be long and random in production
  expiresIn: "1h",                     // or number of seconds: 3600
});

// 2. Register auth middleware under the name "auth"
app.middleware("auth", createAuthMiddleware(auth));

// 3. Protect routes
const router = app.getRouter();
router.get("/me", MeController, { middleware: ["auth"] });

Production secret: Use a randomly generated secret ≥ 32 characters stored in an environment variable.


The Auth Class

Constructor options

Option Type Description
secret string HMAC-SHA256 signing secret
expiresIn string \| number Token TTL — string like "1h", "7d", or seconds as a number

generateToken(user)

Signs and returns a JWT string containing the user payload:

const token = await auth.generateToken({
  id:    "user-1",
  email: "alice@example.com",
  role:  "admin",
});
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

verifyToken(token)

Decodes and validates a token. Returns the User payload on success, null on failure (expired, invalid signature, malformed):

const user = await auth.verifyToken(token);
if (!user) {
  // 401 Unauthorized
}

authenticate(request)

Convenience wrapper — extracts the Authorization: Bearer <token> header and calls verifyToken:

const user = await auth.authenticate(request);

extractTokenFromHeader(request)

Extract the raw token string without verifying it:

const raw = auth.extractTokenFromHeader(request);

hashPassword(password) / verifyPassword(password, hash)

Delegates to Bun's built-in bcrypt implementation:

const hash  = await auth.hashPassword("my-secret-password");
const valid = await auth.verifyPassword("my-secret-password", hash);

Auth Middleware

createAuthMiddleware(auth) — required auth

Rejects unauthenticated requests with HTTP 401 and populates ctx.user on success:

app.middleware("auth", createAuthMiddleware(auth));

Error response:

{ "success": false, "message": "Unauthorized" }

createOptionalAuthMiddleware(auth) — optional auth

Same as above but always allows the request through. ctx.user is undefined when no valid token is present:

app.middleware("auth:optional", createOptionalAuthMiddleware(auth));

// Inside a controller:
if (ctx.user) {
  // personalised response
} else {
  // anonymous response
}

Login Endpoint Pattern

class LoginController extends Controller {
  protected schema = {
    email:    { required: true, type: "string" as const },
    password: { required: true, type: "string" as const },
  };

  async handle(ctx: RequestContext) {
    const { email, password } = ctx.body as { email: string; password: string };

    const user = await db.findByEmail(email);
    if (!user) return this.error("Invalid credentials", 401);

    const valid = await auth.verifyPassword(password, user.passwordHash);
    if (!valid) return this.error("Invalid credentials", 401);

    const token = await auth.generateToken({ id: user.id, email: user.email });
    return this.success({ token });
  }
}

Testing with Auth

Use createTestToken from the Testing Kit to generate tokens in tests without running a real server:

import { createTestToken } from "katal/testing";

const token = createTestToken({ id: "1", role: "admin" });
const res = await client.withBearer(token).get("/me");

See Also

Token Management

Generating Tokens

const user = {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com'
};

const token = await auth.generateToken(user);

Verifying Tokens

const token = request.headers.get('Authorization')?.split(' ')[1];
if (token) {
    const user = await auth.verifyToken(token);
    if (user) {
        // Token is valid, user is authenticated
        console.log('Authenticated user:', user);
    }
}

Password Management

// Hash a password
const hashedPassword = await auth.hashPassword('user-password');

// Verify a password
const isValid = await auth.verifyPassword('user-password', hashedPassword);

Auth Middleware

import { AuthMiddleware } from 'katal/middleware';

// Protect routes with authentication
app.registerMiddleware('auth', new AuthMiddleware(auth));

// Use in routes
router.get('/profile', ['auth'], ProfileController);

Request Authentication

In your controllers, you can access the authenticated user:

class ProfileController extends Controller {
    async handle(context: RequestContext) {
        const token = context.request.headers.get('Authorization')?.split(' ')[1];
        if (!token) {
            return this.json({ error: 'Unauthorized' }, { status: 401 });
        }

        const user = await this.resolve<Auth>('auth').verifyToken(token);
        if (!user) {
            return this.json({ error: 'Invalid token' }, { status: 401 });
        }

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

Configuration

interface AuthConfig {
    secret: string;           // Secret key for token signing
    expiresIn: string | number; // Token expiration
}

Security Features

  • Token Signature Verification
  • Expiration Validation
  • Password Hashing using Bun's crypto
  • Base64Url Encoding/Decoding
  • Prevention of Token Tampering

Best Practices

  1. Store Secret Key Securely typescript const auth = new Auth({ secret: process.env.JWT_SECRET!, expiresIn: '1h' });

  2. Use HTTPS in Production typescript const app = new Application({ // ... other config });

  3. Implement Token Refresh Logic typescript class AuthController extends Controller { async refresh(context: RequestContext) { const refreshToken = context.request.headers.get('X-Refresh-Token'); // Implement your refresh logic } }

  4. Handle Token Expiration ```typescript class AuthMiddleware implements Middleware { async before({ request }) { const token = request.headers.get('Authorization')?.split(' ')[1]; if (!token) { return new Response('Unauthorized', { status: 401 }); }

       const user = await this.auth.verifyToken(token);
       if (!user) {
           return new Response('Token expired or invalid', { status: 401 });
       }
    
       return null;
    

    } } ```