Loading...
Loading...
Use when initializing or configuring the Bknd authentication system. Covers enabling auth, configuring password strategy, setting up JWT and cookie options, defining roles, and production security settings.
npx skill4agent add cameronapak/bknd-skills bknd-setup-authimport { serve } from "bknd/adapter/bun";
import { em, entity, text } from "bknd";
const schema = em({
posts: entity("posts", { title: text().required() }),
});
serve({
connection: { url: "file:data.db" },
config: {
data: schema.toJSON(),
auth: {
enabled: true,
},
},
});users/api/auth/*{
auth: {
enabled: true,
jwt: {
secret: process.env.JWT_SECRET, // Required in production
alg: "HS256", // Algorithm: HS256, HS384, HS512
expires: 604800, // Expiry in seconds (7 days)
issuer: "my-app", // Optional issuer claim
fields: ["id", "email", "role"], // Fields included in token
},
},
}| Option | Type | Default | Description |
|---|---|---|---|
| string | | Signing secret (256-bit minimum for production) |
| string | | Algorithm: HS256, HS384, HS512 |
| number | - | Token expiry in seconds |
| string | - | Token issuer claim (iss) |
| string[] | | User fields in payload |
{
auth: {
enabled: true,
jwt: { secret: process.env.JWT_SECRET },
cookie: {
secure: true, // HTTPS only (set false for local dev)
httpOnly: true, // Block JavaScript access
sameSite: "lax", // CSRF protection: "strict" | "lax" | "none"
expires: 604800, // Cookie expiry in seconds (7 days)
path: "/", // Cookie path scope
renew: true, // Auto-extend on requests
pathSuccess: "/", // Redirect after login
pathLoggedOut: "/", // Redirect after logout
},
},
}| Option | Type | Default | Description |
|---|---|---|---|
| boolean | | HTTPS-only flag |
| boolean | | Block JS access |
| string | | CSRF protection |
| number | | Expiry in seconds |
| boolean | | Auto-extend expiry |
| string | | Post-login redirect |
| string | | Post-logout redirect |
{
auth: {
enabled: true,
jwt: { secret: process.env.JWT_SECRET },
strategies: {
password: {
type: "password",
enabled: true,
config: {
hashing: "bcrypt", // "plain" | "sha256" | "bcrypt"
rounds: 4, // bcrypt rounds (1-10)
minLength: 8, // Minimum password length
},
},
},
},
}| Option | Security | Performance | Use Case |
|---|---|---|---|
| None | Fastest | Development only, never production |
| Good | Fast | Default, suitable for most cases |
| Best | Slower | Recommended for production |
{
auth: {
enabled: true,
jwt: { secret: process.env.JWT_SECRET },
roles: {
admin: {
implicit_allow: true, // Can do everything
},
editor: {
implicit_allow: false,
permissions: [
{ permission: "data.posts.read", effect: "allow" },
{ permission: "data.posts.create", effect: "allow" },
{ permission: "data.posts.update", effect: "allow" },
],
},
user: {
implicit_allow: false,
is_default: true, // Default role for new registrations
permissions: [
{ permission: "data.posts.read", effect: "allow" },
],
},
},
default_role_register: "user", // Role assigned on registration
},
}{
auth: {
enabled: true,
allow_register: true, // Enable/disable registration
default_role_register: "user", // Role for new users
entity_name: "users", // User entity name (default: "users")
basepath: "/api/auth", // Auth API base path
},
}import { serve, type BunBkndConfig } from "bknd/adapter/bun";
import { em, entity, text, date } from "bknd";
const schema = em({
users: entity("users", {
email: text().required().unique(),
name: text(),
avatar: text(),
created_at: date({ default_value: "now" }),
}),
posts: entity("posts", {
title: text().required(),
content: text(),
}),
});
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}
const config: BunBkndConfig = {
connection: { url: process.env.DB_URL || "file:data.db" },
config: {
data: schema.toJSON(),
auth: {
enabled: true,
basepath: "/api/auth",
entity_name: "users",
allow_register: true,
default_role_register: "user",
// JWT configuration
jwt: {
secret: process.env.JWT_SECRET!,
alg: "HS256",
expires: 604800, // 7 days
issuer: "my-app",
fields: ["id", "email", "role"],
},
// Cookie configuration
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
expires: 604800,
renew: true,
pathSuccess: "/dashboard",
pathLoggedOut: "/login",
},
// Password strategy
strategies: {
password: {
type: "password",
enabled: true,
config: {
hashing: "bcrypt",
rounds: 4,
minLength: 8,
},
},
},
// Roles
roles: {
admin: {
implicit_allow: true,
},
editor: {
implicit_allow: false,
permissions: [
{ permission: "data.posts.read", effect: "allow" },
{ permission: "data.posts.create", effect: "allow" },
{ permission: "data.posts.update", effect: "allow" },
{ permission: "data.posts.delete", effect: "allow" },
],
},
user: {
implicit_allow: false,
is_default: true,
permissions: [
{ permission: "data.posts.read", effect: "allow" },
],
},
},
},
},
options: {
seed: async (ctx) => {
// Create initial admin on first run
const adminExists = await ctx.em.repo("users").findOne({
where: { email: { $eq: "admin@example.com" } },
});
if (!adminExists) {
await ctx.app.module.auth.createUser({
email: "admin@example.com",
password: process.env.ADMIN_PASSWORD || "changeme123",
role: "admin",
});
console.log("Admin user created");
}
},
},
};
serve(config);| Method | Path | Description |
|---|---|---|
| POST | | Login with email/password |
| POST | | Register new user |
| GET | | Get current user |
| POST | | Log out (clear cookie) |
| GET | | List enabled strategies |
# .env
JWT_SECRET=your-256-bit-secret-minimum-32-characters-long
ADMIN_PASSWORD=secure-initial-admin-password# Generate 64-character random string
openssl rand -hex 32| Setting | Development | Production |
|---|---|---|
| Can use placeholder | Required, strong secret |
| | |
| | |
| | Consider |
const isDev = process.env.NODE_ENV !== "production";
{
auth: {
enabled: true,
jwt: {
secret: isDev ? "dev-secret-not-for-production" : process.env.JWT_SECRET!,
expires: isDev ? 86400 * 30 : 604800, // 30 days dev, 7 days prod
},
cookie: {
secure: !isDev,
},
strategies: {
password: {
type: "password",
config: {
hashing: isDev ? "sha256" : "bcrypt",
},
},
},
},
}Cannot sign JWT without secret{
auth: {
jwt: {
secret: process.env.JWT_SECRET, // Never hardcode in production
},
},
}secure: false{
auth: {
cookie: {
secure: process.env.NODE_ENV === "production", // false for localhost
},
},
}Role "admin" not found{
auth: {
roles: {
admin: { implicit_allow: true }, // Define first
user: { implicit_allow: false },
},
default_role_register: "user", // Now can reference
},
}Registration not allowed{
auth: {
allow_register: true, // Default is true, but check if explicitly disabled
},
}plainsha256{
auth: {
strategies: {
password: {
config: {
hashing: "bcrypt",
rounds: 4, // Balance security and performance
},
},
},
},
}curl http://localhost:7654/api/auth/strategiescurl -X POST http://localhost:7654/api/auth/password/register \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "password123"}'curl -X POST http://localhost:7654/api/auth/password/login \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "password123"}'curl http://localhost:7654/api/auth/me \
-H "Authorization: Bearer <token-from-login>"jwt.secrethashing: "bcrypt"cookie.secure: truecookie.httpOnly: truecookie.sameSite: "lax""strict"jwt.expiresallow_registerplaincookie.secure