Flink plugin for JWT auth
npm install @flink-app/jwt-auth-pluginA Flink authentication plugin that provides JWT (JSON Web Token) based authentication with role-based permissions, password hashing, and token management.
- JWT token generation and validation
- Password hashing using bcrypt
- Role-based access control with permissions
- Configurable password policies
- Token expiration support
- Bearer token authentication
- Custom token extraction (query params, cookies, custom headers)
- Dynamic permission checking (database-backed permissions)
Install the plugin in your Flink app project:
``bash`
npm install @flink-app/jwt-auth-plugin
`typescript
import { FlinkApp } from "@flink-app/flink";
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
import { Ctx } from "./Ctx";
function start() {
const app = new FlinkApp
name: "My Flink App",
auth: jwtAuthPlugin({
secret: process.env.JWT_SECRET || "your-secret-key",
getUser: async (tokenData) => {
// Retrieve user from database using token data
const user = await ctx.repos.userRepo.findById(tokenData.userId);
return {
id: user._id,
username: user.username,
roles: user.roles,
};
},
rolePermissions: {
admin: ["read", "write", "delete", "manage_users"],
user: ["read", "write"],
guest: ["read"],
},
}),
db: {
uri: "mongodb://localhost:27017/my-app",
},
});
app.start();
}
start();
`
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| secret | string | Yes | - | Secret key used to sign and verify JWT tokens. Keep this secure! |getUser
| | (tokenData: any, req: FlinkRequest) => Promise | Yes | - | Async function that retrieves user data from token payload. Has access to the full request object for context (headers, path, etc.) |rolePermissions
| | { [role: string]: string[] } | Yes | - | Maps roles to their allowed permissions |algo
| | jwtSimple.TAlgorithm | No | "HS256" | JWT signing algorithm |passwordPolicy
| | RegExp | No | /^(?=.[A-Za-z])(?=.\d)[A-Za-z\d]{8,}$/ | Regex to validate password strength |tokenTTL
| | number | No | 1000 60 60 24 365 * 100 (100 years) | Token time-to-live in milliseconds |tokenExtractor
| | (req: FlinkRequest) => string \| null \| undefined | No | - | Custom token extraction function. Return string for token, null for no token, undefined to fallback to Bearer |checkPermissions
| | (user: FlinkAuthUser, routePermissions: string[]) => Promise | No | - | Custom permission validator for dynamic permissions from database. Replaces static rolePermissions when provided |useDynamicRoles
| | boolean | No | false | When true, uses roles from the user object returned by getUser instead of roles from the token. Useful for multi-tenant scenarios where user roles vary by organization |
The default password policy requires:
- Minimum 8 characters
- At least one letter (A-Z or a-z)
- At least one number (0-9)
You can customize this by providing your own regex:
`typescript`
jwtAuthPlugin({
secret: "your-secret",
getUser: async (tokenData) => { / ... / },
rolePermissions: { / ... / },
passwordPolicy: /^(?=.[A-Za-z])(?=.\d)(?=.[@$!%#?&])[A-Za-z\d@$!%*#?&]{12,}$/,
// Requires: 12+ chars, 1 letter, 1 number, 1 special character
})
By default, the plugin extracts JWT tokens from the Authorization header as Bearer tokens:
``
Authorization: Bearer
However, you can customize token extraction using the tokenExtractor option. This is useful for:
- Mobile apps that pass tokens in query parameters
- Cookie-based authentication for web routes
- Custom header schemes for specific endpoints
- Different auth methods for different route patterns
The tokenExtractor callback supports three return values:
- string: Token found, use this token for authentication
- null: No token found, authentication should fail (no fallback to default Bearer)
- undefined: Skip custom extraction, use default Bearer token extraction
`typescript`
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => { / ... / },
rolePermissions: { / ... / },
tokenExtractor: (req) => {
// Allow query param tokens only for public API routes
if (req.path?.startsWith('/api/public/') && req.method === 'GET') {
return req.query?.token as string || null;
}
// All other routes use default Bearer token
return undefined;
}
})
Usage:
``
GET /api/public/data?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
`typescript`
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => { / ... / },
rolePermissions: { / ... / },
tokenExtractor: (req) => {
// Web routes use session cookie
if (req.path?.startsWith('/web/')) {
return req.cookies?.session_token || null;
}
// API routes use Bearer token (default)
return undefined;
}
})
`typescript`
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => { / ... / },
rolePermissions: { / ... / },
tokenExtractor: (req) => {
// Webhook endpoints use custom header
if (req.path?.startsWith('/webhooks/')) {
return req.headers['x-webhook-signature'] as string || null;
}
// Other routes use Bearer token
return undefined;
}
})
`typescript`
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => { / ... / },
rolePermissions: { / ... / },
tokenExtractor: (req) => {
// Special handling for PATCH requests
if (req.method === 'PATCH') {
return req.headers['x-patch-token'] as string || null;
}
// All other methods use Bearer
return undefined;
}
})
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => { / ... / },
rolePermissions: { / ... / },
tokenExtractor: (req) => {
// Try cookie first for browser requests
if (req.headers['user-agent']?.includes('Mozilla')) {
const cookieToken = req.cookies?.auth_token;
if (cookieToken) return cookieToken;
}
// Try query param for mobile apps
if (req.query?.token) {
return req.query.token as string;
}
// Fall back to default Bearer token extraction
return undefined;
}
})
`
- When tokenExtractor returns undefined, the plugin falls back to extracting from Authorization: Bearer null
- When it returns , authentication fails immediately (useful to enforce specific auth methods for certain routes)string
- When it returns a , that token is validated using the same JWT verification logicreq.path
- The callback has access to , req.method, req.headers, req.query, req.cookies, etc.
By default, the plugin uses static rolePermissions defined at configuration time. However, you can implement dynamic permissions that are fetched from the database on each request using the checkPermissions callback.
Use checkPermissions when:
- Permissions are stored in the database per user or per role
- Permissions can change without restarting the application
- Different organizations/tenants have different permission sets
- You need fine-grained, user-specific permissions
When you provide checkPermissions:rolePermissions
1. Token is extracted and decoded (same as before)
2. Static check is skippedgetUser
3. is called - this is where you fetch permissions from DBcheckPermissions
4. is called with the user object and required route permissionscheckPermissions
5. If returns true, authentication succeeds
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => {
// Fetch user from database
const user = await ctx.repos.userRepo.getById(tokenData.userId);
// Fetch user's permissions from database
const permissions = await ctx.repos.permissionRepo.getUserPermissions(user._id);
return {
id: user._id,
username: user.username,
roles: user.roles,
permissions, // Attach permissions to user object
};
},
// rolePermissions can be empty when using dynamic permissions
rolePermissions: {},
// Custom permission checker
checkPermissions: async (user, routePermissions) => {
// User must have ALL required permissions
return routePermissions.every(perm =>
user.permissions?.includes(perm)
);
},
})
`
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => {
const user = await ctx.repos.userRepo.getById(tokenData.userId);
// Fetch permissions based on user's organization
const permissions = await ctx.repos.permissionRepo.getOrgPermissions(
user._id,
user.organizationId
);
return {
id: user._id,
username: user.username,
organizationId: user.organizationId,
permissions,
};
},
rolePermissions: {},
checkPermissions: async (user, routePermissions) => {
return routePermissions.every(perm =>
user.permissions?.includes(perm)
);
},
})
`
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => {
const user = await ctx.repos.userRepo.getById(tokenData.userId);
// Get base permissions from roles
const rolePerms = await ctx.repos.roleRepo.getRolePermissions(user.roles);
// Get user-specific permission overrides
const userPerms = await ctx.repos.permissionRepo.getUserPermissions(user._id);
// Combine both
const allPermissions = [...new Set([...rolePerms, ...userPerms])];
return {
id: user._id,
username: user.username,
roles: user.roles,
permissions: allPermissions,
};
},
rolePermissions: {},
checkPermissions: async (user, routePermissions) => {
return routePermissions.every(perm =>
user.permissions?.includes(perm)
);
},
})
`
`typescript
checkPermissions: async (user, routePermissions) => {
// Support wildcard permissions
if (user.permissions?.includes("*")) {
return true; // User has all permissions
}
// Check specific permissions
return routePermissions.every(perm =>
user.permissions?.includes(perm)
);
}
`
`typescript`
checkPermissions: async (user, routePermissions) => {
// User needs ANY of the route permissions (OR logic)
return routePermissions.some(perm =>
user.permissions?.includes(perm)
);
}
To reduce database load, you can cache permissions:
`typescript
const permissionCache = new Map();
const CACHE_TTL = 5 60 1000; // 5 minutes
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => {
const user = await ctx.repos.userRepo.getById(tokenData.userId);
// Check cache first
const cacheKey = perms:${user._id};
const cached = permissionCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return {
id: user._id,
username: user.username,
permissions: cached.permissions,
};
}
// Fetch from DB
const permissions = await ctx.repos.permissionRepo.getUserPermissions(user._id);
// Cache it
permissionCache.set(cacheKey, {
permissions,
timestamp: Date.now(),
});
return {
id: user._id,
username: user.username,
permissions,
};
},
rolePermissions: {},
checkPermissions: async (user, routePermissions) => {
return routePermissions.every(perm => user.permissions?.includes(perm));
},
})
`
- Backward Compatible: If you don't provide checkPermissions, static rolePermissions are used (existing behavior)checkPermissions
- Performance: is called on every authenticated request, so ensure getUser is optimized (consider caching)user
- User Object: The parameter in checkPermissions is the exact object returned from getUser[]
- Public Routes: If a route has no permissions (), checkPermissions is NOT calledcheckPermissions
- Sync or Async: can return Promise or boolean
Issue: Too many database queries
Solution: Implement permission caching in getUser or use an in-memory cache like Redis
Issue: Permissions not updating after database change
Solution: Clear permission cache or reduce cache TTL
The JWT auth plugin supports multi-tenant scenarios where users have different roles depending on the organization or context they're accessing. This is achieved through the useDynamicRoles option combined with the req parameter in getUser.
Use useDynamicRoles: true when:
- Users belong to multiple organizations with different roles in each
- User roles are determined by request context (headers, subdomain, path)
- Roles need to be fetched from the database based on the current context
- The same user token should grant different permissions in different contexts
1. Default behavior (useDynamicRoles: false):getUser
- Roles from the JWT token are used for permission checking
- can modify the user object, but token roles are what matter for permissions
2. Dynamic roles (useDynamicRoles: true):getUser
- Permission checking uses roles from the user object returned by getUser
- Token roles are ignored for permission checks
- can fetch organization-specific roles from the database
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
useDynamicRoles: true,
getUser: async (tokenData, req) => {
const user = await ctx.repos.userRepo.getById(tokenData.userId);
// Get organization from request header
const orgId = req.headers['x-organization-id'] as string;
// Fetch user's role in this specific organization
const membership = await ctx.repos.orgMemberRepo.findOne({
userId: user._id,
organizationId: orgId
});
return {
id: user._id,
username: user.username,
organizationId: orgId,
roles: [membership.role], // Org-specific role
};
},
rolePermissions: {
admin: ["read", "write", "delete", "manage_users"],
user: ["read", "write"],
guest: ["read"],
},
})
`
Usage:
`bashUser is admin in org1
curl -H "Authorization: Bearer
-H "X-Organization-ID: org1" \
https://api.example.com/users✓ Has admin permissions in org1
$3
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
useDynamicRoles: true, getUser: async (tokenData, req) => {
// Extract org from subdomain (acme.yourapp.com -> "acme")
const host = req.headers.host || '';
const orgSubdomain = host.split('.')[0];
const membership = await ctx.repos.orgMemberRepo.findOne({
userId: tokenData.userId,
organizationSlug: orgSubdomain
});
if (!membership) {
return null; // User not member of this org
}
return {
id: tokenData.userId,
username: tokenData.username,
organizationSlug: orgSubdomain,
roles: [membership.role],
};
},
rolePermissions: {
owner: ["*"], // All permissions
admin: ["read", "write", "delete", "manage_users"],
member: ["read", "write"],
},
})
`$3
`typescript
// Routes like: /orgs/:orgId/projects
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
useDynamicRoles: true, getUser: async (tokenData, req) => {
// Extract org from path: /orgs/org123/projects
const pathMatch = req.path?.match(/^\/orgs\/([^\/]+)/);
const orgId = pathMatch?.[1];
if (!orgId) {
// Not an org-specific route, use default role
return {
id: tokenData.userId,
username: tokenData.username,
roles: ["user"],
};
}
const membership = await ctx.repos.orgMemberRepo.findOne({
userId: tokenData.userId,
organizationId: orgId
});
return {
id: tokenData.userId,
username: tokenData.username,
organizationId: orgId,
roles: [membership.role],
};
},
rolePermissions: {
admin: ["read", "write", "delete"],
member: ["read", "write"],
viewer: ["read"],
},
})
`$3
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
useDynamicRoles: true, getUser: async (tokenData, req) => {
const user = await ctx.repos.userRepo.getById(tokenData.userId);
const orgId = req.headers['x-organization-id'] as string;
// Get org-specific role
const orgMembership = await ctx.repos.orgMemberRepo.findOne({
userId: user._id,
organizationId: orgId
});
// Get global role from user
const globalRole = user.globalRole; // e.g., "super_admin"
// Combine roles (super admins have all permissions everywhere)
const roles = globalRole === 'super_admin'
? ['super_admin']
: [orgMembership.role];
return {
id: user._id,
username: user.username,
organizationId: orgId,
globalRole,
roles,
};
},
rolePermissions: {
super_admin: ["*"], // Global super admins
org_admin: ["read", "write", "delete", "manage_members"],
org_member: ["read", "write"],
org_guest: ["read"],
},
})
`$3
Since
getUser is called on every authenticated request, consider caching:`typescript
const roleCache = new Map();
const CACHE_TTL = 5 60 1000; // 5 minutesjwtAuthPlugin({
secret: process.env.JWT_SECRET!,
useDynamicRoles: true,
getUser: async (tokenData, req) => {
const orgId = req.headers['x-organization-id'] as string;
const cacheKey =
${tokenData.userId}:${orgId}; // Check cache
const cached = roleCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.user;
}
// Fetch from DB
const membership = await ctx.repos.orgMemberRepo.findOne({
userId: tokenData.userId,
organizationId: orgId
});
const user = {
id: tokenData.userId,
username: tokenData.username,
organizationId: orgId,
roles: [membership.role],
};
// Cache it
roleCache.set(cacheKey, {
user,
timestamp: Date.now(),
});
return user;
},
rolePermissions: {
admin: ["read", "write", "delete"],
member: ["read", "write"],
},
})
`$3
The
getUser callback receives the full request object, giving you access to:`typescript
getUser: async (tokenData, req) => {
// Available request properties:
req.path // "/api/users"
req.method // "GET", "POST", etc.
req.headers // { "x-organization-id": "org1", ... }
req.query // { page: "1", limit: "10" }
req.body // Request body (if applicable)
req.params // URL parameters
req.cookies // Cookies (if cookie-parser middleware is used) // Use any combination to determine context
const orgId = req.headers['x-organization-id']
|| req.query.org
|| req.params.orgId;
// Fetch and return user with context-specific roles
// ...
}
`$3
If you have an existing app with static roles, you can migrate gradually:
Before (static roles):
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData, req) => {
const user = await ctx.repos.userRepo.getById(tokenData.userId);
return {
id: user._id,
username: user.username,
// Token roles are used
};
},
rolePermissions: {
admin: ["read", "write", "delete"],
user: ["read", "write"],
},
})
`After (dynamic multi-tenant roles):
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
useDynamicRoles: true, // ← Enable dynamic roles
getUser: async (tokenData, req) => { // ← Now has req parameter
const user = await ctx.repos.userRepo.getById(tokenData.userId);
const orgId = req.headers['x-organization-id']; // Fetch org-specific role
const membership = await ctx.repos.orgMemberRepo.findOne({
userId: user._id,
organizationId: orgId
});
return {
id: user._id,
username: user.username,
organizationId: orgId,
roles: [membership.role], // ← Dynamic role
};
},
rolePermissions: {
admin: ["read", "write", "delete"],
user: ["read", "write"],
},
})
`$3
- Backward Compatible: Setting
useDynamicRoles: false (default) maintains existing behavior
- Token Still Required: Dynamic roles don't bypass token validation - the token must be valid
- Organization Validation: Always validate that the user has access to the requested organization
- Error Handling: Return null from getUser if user doesn't have access to the organization
- Cache Invalidation: Remember to invalidate role cache when user roles change in the databaseContext API
Once configured, the plugin provides the following methods via the
auth context:$3
Creates a JWT token with the provided payload and roles.
`typescript
const token = await ctx.auth.createToken(
{ userId: user._id, username: user.username },
["user"]
);
`Parameters:
-
payload: Any data to encode in the token (typically user ID and username)
- roles: Array of role names assigned to the userReturns: JWT token string
Example:
`typescript
// In a login handler
const handler: Handler = async ({ ctx, req }) => {
const user = await ctx.repos.userRepo.findOne({ username: req.body.username }); if (!user) {
return { status: 401, error: { code: "invalid_credentials" } };
}
const token = await ctx.auth.createToken(
{ userId: user._id, username: user.username },
user.roles
);
return {
data: {
token,
user: {
id: user._id,
username: user.username,
},
},
};
};
`$3
Generates a secure password hash and salt using bcrypt.
`typescript
const result = await ctx.auth.createPasswordHashAndSalt("mypassword123");
if (result) {
const { hash, salt } = result;
// Save hash and salt to database
}
`Parameters:
-
password: The plain text password to hashReturns:
- Object with
hash and salt if password meets policy
- null if password doesn't meet the configured password policySecurity Note: Both hash and salt must be stored in your database to validate passwords later.
Example:
`typescript
// Creating a new user
const handler: Handler = async ({ ctx, req }) => {
const passwordData = await ctx.auth.createPasswordHashAndSalt(req.body.password); if (!passwordData) {
return {
status: 400,
error: {
code: "weak_password",
message: "Password does not meet security requirements",
},
};
}
const user = await ctx.repos.userRepo.create({
username: req.body.username,
password: passwordData.hash,
salt: passwordData.salt,
roles: ["user"],
});
return { data: { userId: user._id } };
};
`$3
Validates a password against a stored hash and salt.
`typescript
const isValid = await ctx.auth.validatePassword(
"mypassword123",
user.password,
user.salt
);
`Parameters:
-
password: Plain text password to validate
- passwordHash: Stored password hash from database
- salt: Stored salt from databaseReturns:
true if password matches, false otherwiseExample:
`typescript
// In a login handler
const handler: Handler = async ({ ctx, req }) => {
const user = await ctx.repos.userRepo.findOne({ username: req.body.username }); if (!user) {
return { status: 401, error: { code: "invalid_credentials" } };
}
const isValidPassword = await ctx.auth.validatePassword(
req.body.password,
user.password,
user.salt
);
if (!isValidPassword) {
return { status: 401, error: { code: "invalid_credentials" } };
}
const token = await ctx.auth.createToken(
{ userId: user._id, username: user.username },
user.roles
);
return {
data: {
token,
user: { id: user._id, username: user.username },
},
};
};
`$3
Automatically called by Flink framework to authenticate requests. You typically don't call this directly.
Role-Based Access Control
$3
`typescript
jwtAuthPlugin({
secret: "your-secret",
getUser: async (tokenData) => { / ... / },
rolePermissions: {
// Admin role can do everything
admin: ["read", "write", "delete", "manage_users", "view_analytics"], // Regular user has limited permissions
user: ["read", "write"],
// Guest can only read
guest: ["read"],
},
})
`$3
Use the
permission property in your route configuration to restrict access:`typescript
// Only authenticated users (any role)
export const Route: RouteProps = {
path: "/api/profile",
permission: "read", // Must have "read" permission
};// Only admins
export const Route: RouteProps = {
path: "/api/admin/users",
permission: "manage_users", // Must have "manage_users" permission
};
// Multiple permissions (user must have at least one)
export const Route: RouteProps = {
path: "/api/content",
permission: ["read", "write"], // Must have either "read" OR "write"
};
`$3
Once authenticated, the user object is available in
req.user:`typescript
const handler: Handler = async ({ ctx, req }) => {
// Access authenticated user
const userId = req.user?.id;
const username = req.user?.username;
const roles = req.user?.roles; // Use user data in your logic
const data = await ctx.repos.dataRepo.findByUserId(userId);
return { data };
};
`Making Authenticated Requests
Clients must include the JWT token in the
Authorization header:`
Authorization: Bearer
`$3
`javascript
const response = await fetch('https://api.example.com/profile', {
method: 'GET',
headers: {
'Authorization': Bearer ${token},
'Content-Type': 'application/json',
},
});
`$3
`javascript
const response = await axios.get('https://api.example.com/profile', {
headers: {
'Authorization': Bearer ${token},
},
});
`Complete Example
`typescript
// index.ts
import { FlinkApp } from "@flink-app/flink";
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
import { Ctx } from "./Ctx";function start() {
const app = new FlinkApp({
name: "My App",
auth: jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => {
const user = await app.ctx.repos.userRepo.findById(tokenData.userId);
return {
id: user._id,
username: user.username,
roles: user.roles,
};
},
rolePermissions: {
admin: ["read", "write", "delete", "manage_users"],
user: ["read", "write"],
},
passwordPolicy: /^(?=.[A-Za-z])(?=.\d)[A-Za-z\d@$!%*?&]{10,}$/,
tokenTTL: 1000 60 60 24 7, // 7 days
}),
db: {
uri: process.env.MONGODB_URI!,
},
});
app.start();
}
start();
// handlers/auth/PostLogin.ts
import { Handler, RouteProps } from "@flink-app/flink";
import { Ctx } from "../../Ctx";
import LoginReq from "../../schemas/LoginReq";
import LoginRes from "../../schemas/LoginRes";
export const Route: RouteProps = {
path: "/auth/login",
};
const PostLogin: Handler = async ({ ctx, req }) => {
const { username, password } = req.body;
// Find user
const user = await ctx.repos.userRepo.findOne({ username });
if (!user) {
return {
status: 401,
error: { code: "invalid_credentials", message: "Invalid username or password" },
};
}
// Validate password
const isValid = await ctx.auth.validatePassword(password, user.password, user.salt);
if (!isValid) {
return {
status: 401,
error: { code: "invalid_credentials", message: "Invalid username or password" },
};
}
// Create token
const token = await ctx.auth.createToken(
{ userId: user._id, username: user.username },
user.roles
);
return {
data: {
token,
user: {
id: user._id,
username: user.username,
roles: user.roles,
},
},
};
};
export default PostLogin;
// handlers/users/PostUser.ts
import { Handler, RouteProps } from "@flink-app/flink";
import { Ctx } from "../../Ctx";
import CreateUserReq from "../../schemas/CreateUserReq";
import CreateUserRes from "../../schemas/CreateUserRes";
export const Route: RouteProps = {
path: "/users",
permission: "manage_users", // Only admins can create users
};
const PostUser: Handler = async ({ ctx, req }) => {
const { username, password, roles } = req.body;
// Check if user exists
const existingUser = await ctx.repos.userRepo.findOne({ username });
if (existingUser) {
return {
status: 409,
error: { code: "user_exists", message: "Username already taken" },
};
}
// Hash password
const passwordData = await ctx.auth.createPasswordHashAndSalt(password);
if (!passwordData) {
return {
status: 400,
error: {
code: "weak_password",
message: "Password does not meet security requirements",
},
};
}
// Create user
const user = await ctx.repos.userRepo.create({
username,
password: passwordData.hash,
salt: passwordData.salt,
roles: roles || ["user"],
createdAt: new Date(),
});
return {
data: {
id: user._id,
username: user.username,
roles: user.roles,
},
};
};
export default PostUser;
`Security Best Practices
$3
Never hardcode your JWT secret. Use environment variables:
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
// ...
})
`Generate a strong secret:
`bash
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
`$3
Set an appropriate TTL for your use case:
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
tokenTTL: 1000 60 60 24 7, // 7 days
// ...
})
`$3
Enforce strong password requirements:
`typescript
jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
// Require: 12+ chars, uppercase, lowercase, number, special char
passwordPolicy: /^(?=.[a-z])(?=.[A-Z])(?=.\d)(?=.[@$!%?&])[A-Za-z\d@$!%?&]{12,}$/,
// ...
})
`$3
Always use HTTPS in production to prevent token interception.
$3
- Avoid
localStorage (vulnerable to XSS)
- Prefer httpOnly cookies or secure session storage
- Implement token refresh mechanisms for long-lived sessions$3
Implement rate limiting on authentication endpoints to prevent brute force attacks.
TypeScript Types
`typescript
import { JwtAuthPlugin, JwtAuthPluginOptions } from "@flink-app/jwt-auth-plugin";// Token extractor callback type
type TokenExtractor = (req: FlinkRequest) => string | null | undefined;
// Permission checker callback type
type PermissionChecker = (
user: FlinkAuthUser,
routePermissions: string[]
) => Promise | boolean;
// Plugin options
interface JwtAuthPluginOptions {
secret: string;
algo?: jwtSimple.TAlgorithm;
getUser: (tokenData: any) => Promise;
passwordPolicy?: RegExp;
tokenTTL?: number;
rolePermissions: {
[role: string]: string[];
};
tokenExtractor?: TokenExtractor;
checkPermissions?: PermissionChecker;
}
// Plugin interface
interface JwtAuthPlugin extends FlinkAuthPlugin {
createToken: (payload: any, roles: string[]) => Promise;
createPasswordHashAndSalt: (
password: string
) => Promise<{ hash: string; salt: string } | null>;
validatePassword: (
password: string,
passwordHash: string,
salt: string
) => Promise;
}
// Authenticated user (from Flink framework)
interface FlinkAuthUser {
id: string;
username?: string;
roles?: string[];
[key: string]: any;
}
`Troubleshooting
$3
Issue: Requests return 401 Unauthorized
Solutions:
- Verify the token is being sent in the
Authorization header
- Check the header format: Authorization: Bearer
- Ensure the secret used to sign matches the secret used to verify
- Check if the token has expired (if TTL is configured)$3
Issue:
createPasswordHashAndSalt returns nullSolution: Password doesn't meet the configured
passwordPolicy. Update the password or adjust the policy.$3
Issue: Authentication fails with error from
getUserSolution: Ensure your
getUser function properly handles missing users:`typescript
getUser: async (tokenData) => {
const user = await ctx.repos.userRepo.findById(tokenData.userId);
if (!user) {
throw new Error("User not found");
}
return {
id: user._id,
username: user.username,
roles: user.roles,
};
}
``MIT