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¶
-
Store Secret Key Securely
typescript const auth = new Auth({ secret: process.env.JWT_SECRET!, expiresIn: '1h' }); -
Use HTTPS in Production
typescript const app = new Application({ // ... other config }); -
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 } } -
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;} } ```