@flink-app/generic-auth-plugin
v0.12.1-alpha.45
Published
Flink plugin that provides a generic user authentification solution.
Readme
Generic Auth Plugin
A comprehensive Flink plugin that provides a complete user authentication system with user management, password reset, SMS authentication, and push notification token management. This plugin builds on top of the JWT Auth Plugin to provide ready-to-use authentication endpoints and functions.
Features
- User registration and login
- Password-based and SMS-based authentication
- BankID authentication support
- Password reset flow with email verification
- User profile management
- Push notification token management
- Customizable password hashing
- Pre-built API endpoints (optional)
- Management API integration for admin interfaces
- Lifecycle hooks (onSuccessfulLogin, onUserCreated)
Dependencies
This plugin requires:
- @flink-app/jwt-auth-plugin - For JWT token management
- @flink-app/email-plugin - For password reset emails
- @flink-app/sms-plugin - For SMS authentication (optional)
Installation
npm install @flink-app/generic-auth-plugin @flink-app/jwt-auth-plugin @flink-app/email-pluginFor SMS authentication:
npm install @flink-app/sms-pluginSetup
Step 1: Create User Repository
Create a user repository in your project:
src/repos/UserRepo.ts:
import { FlinkRepo } from "@flink-app/flink";
import { User } from "@flink-app/generic-auth-plugin";
import { Ctx } from "../Ctx";
class UserRepo extends FlinkRepo<Ctx, User> {}
export default UserRepo;src/Ctx.ts:
import { FlinkContext } from "@flink-app/flink";
import UserRepo from "./repos/UserRepo";
export interface Ctx extends FlinkContext {
repos: {
userRepo: UserRepo;
};
}Step 2: Configure Authentication
index.ts:
import { FlinkApp } from "@flink-app/flink";
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
import { genericAuthPlugin } from "@flink-app/generic-auth-plugin";
import { emailPlugin } from "@flink-app/email-plugin";
import { Ctx } from "./Ctx";
function start() {
const app = new FlinkApp<Ctx>({
name: "My Flink App",
debug: true,
auth: jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => {
const user = await app.ctx.repos.userRepo.findById(tokenData.userId);
if (!user) throw new Error("User not found");
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]{8,}$/,
}),
db: {
uri: process.env.MONGODB_URI!,
},
plugins: [
emailPlugin({
// Email configuration for password resets
provider: "sendgrid",
apiKey: process.env.SENDGRID_API_KEY!,
}),
genericAuthPlugin({
repoName: "userRepo",
enableRoutes: true, // Enable built-in API endpoints
enablePasswordReset: true,
enablePushNotificationTokens: true,
usernameFormat: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, // Email format
passwordResetSettings: {
email: {
from_address: "[email protected]",
subject: "Password Reset Code",
html: "Your password reset code is: {{code}}",
},
code: {
numberOfDigits: 6,
lifeTime: "1h", // Uses ms package format
jwtSecret: process.env.PASSWORD_RESET_SECRET!,
},
},
}),
],
});
app.start();
}
start();Configuration Options
GenericAuthPluginOptions
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| repoName | string | Yes | - | Name of the user repository in your context |
| enableRoutes | boolean | No | true | Enable built-in HTTP endpoints |
| enablePasswordReset | boolean | No | false | Enable password reset functionality |
| passwordResetReusableTokens | boolean | No | false | Allow password reset tokens to be reused |
| enablePushNotificationTokens | boolean | No | false | Enable push notification token management |
| enableUserCreation | boolean | No | true | Enable user creation endpoint |
| enableProfileUpdate | boolean | No | true | Enable profile update endpoint |
| enablePasswordUpdate | boolean | No | true | Enable password update endpoint |
| enableUserLogin | boolean | No | true | Enable user login endpoint |
| passwordResetSettings | UserPasswordResetSettings | No | - | Password reset configuration |
| baseUrl | string | No | - | Base URL for email links |
| pluginId | string | No | "genericAuthPlugin" | Plugin identifier |
| usernameFormat | RegExp | No | /.{1,}$/ | Regex to validate username format |
| sms | GenericAuthsmsOptions | No | - | SMS authentication configuration |
| createPasswordHashAndSaltMethod | Function | No | - | Custom password hashing function |
| validatePasswordMethod | Function | No | - | Custom password validation function |
| onSuccessfulLogin | Function | No | - | Callback after successful login |
| onUserCreated | Function | No | - | Callback after user creation |
| deregisterOtherDevices | boolean | No | false | Deregister other devices when new device is registered |
| allowMultipleDevices | boolean | No | true | Allow multiple devices with same deviceId |
Password Reset Settings
interface UserPasswordResetSettings {
email: {
from_address: string;
subject: string; // Handlebars template
html: string; // Handlebars template
};
code: {
numberOfDigits: number; // Length of reset code
lifeTime: string; // e.g., "1h", "30m", "1d" (ms package format)
jwtSecret: string; // Secret for reset token JWT
};
}Handlebars Context:
{{username}}- User's username{{code}}- Password reset code{{profile}}- User profile object
SMS Authentication Options
interface GenericAuthsmsOptions {
smsClient: smsClient; // SMS client instance
smsFrom: string; // Sender name/number
smsMessage: string; // Message template with {{code}}
jwtToken: string; // Secret for SMS JWT tokens
codeType: "numeric" | "alphanumeric";
codeLength: number; // Length of SMS code
}Context API
The plugin exposes the following functions via ctx.plugins.genericAuthPlugin:
loginUser()
Authenticate a user with username and password or initiate SMS authentication.
const result = await ctx.plugins.genericAuthPlugin.loginUser(
repo,
auth,
username,
password,
validatePasswordMethod?,
smsOptions?,
onSuccessfulLogin?,
req?
);Returns: UserLoginRes
loginByToken()
Complete SMS authentication using the validation token and code.
const result = await ctx.plugins.genericAuthPlugin.loginByToken(
repo,
auth,
token,
code,
jwtSecret
);Returns: UserLoginRes
createUser()
Create a new user with password, SMS, or BankID authentication.
const result = await ctx.plugins.genericAuthPlugin.createUser(
repo,
auth,
username,
password,
authentificationMethod, // "password" | "sms" | "bankid"
roles,
profile,
createPasswordHashAndSaltMethod?,
onUserCreated?,
personalNumber?
);Returns: UserCreateRes
changePassword()
Change a user's password.
const result = await ctx.plugins.genericAuthPlugin.changePassword(
repo,
auth,
userId,
newPassword,
createPasswordHashAndSaltMethod?
);Returns: UserPasswordChangeRes
passwordResetStart()
Initiate password reset process and send email with code.
const result = await ctx.plugins.genericAuthPlugin.passwordResetStart(
repo,
auth,
jwtSecret,
username,
numberOfDigits?,
lifeTime?,
passwordResetReusableTokens?
);Returns: UserPasswordResetStartRes
passwordResetComplete()
Complete password reset with token, code, and new password.
const result = await ctx.plugins.genericAuthPlugin.passwordResetComplete(
repo,
auth,
jwtSecret,
passwordResetToken,
code,
newPassword,
createPasswordHashAndSaltMethod?,
passwordResetReusableTokens?
);Returns: UserPasswordResetCompleteRes
Built-in API Endpoints
When enableRoutes: true (default), the following endpoints are automatically registered:
POST /user/create
Create a new user account.
Request:
{
"username": "[email protected]",
"password": "mypassword123",
"authentificationMethod": "password",
"profile": {
"name": "John Doe",
"age": 30
}
}Response:
{
"data": {
"status": "success",
"user": {
"_id": "507f1f77bcf86cd799439011",
"username": "[email protected]",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
}Error Codes:
userExists- Username already takenpasswordError- Password doesn't meet requirementsusernameError- Username doesn't meet format requirements
POST /user/login
Login with username and password.
Request:
{
"username": "[email protected]",
"password": "mypassword123"
}Response:
{
"data": {
"status": "success",
"user": {
"_id": "507f1f77bcf86cd799439011",
"username": "[email protected]",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"profile": {
"name": "John Doe",
"age": 30
}
}
}
}Error Codes:
failed- Invalid username or password
POST /user/password/reset
Initiate password reset (sends email with code).
Request:
{
"username": "[email protected]"
}Response:
{
"data": {
"status": "success",
"passwordResetToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}Error Codes:
userNotFound- User doesn't exist
POST /user/password/reset/complete
Complete password reset with code from email.
Request:
{
"passwordResetToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"code": "123456",
"password": "mynewpassword123"
}Response:
{
"data": {
"status": "success"
}
}Error Codes:
invalidCode- Code is wrong or expiredpasswordError- Password doesn't meet requirementsuserNotFound- User not found
PUT /user/password
Change password for authenticated user.
Authentication: Required
Request:
{
"password": "mynewpassword123"
}Response:
{
"data": {
"status": "success"
}
}Error Codes:
passwordError- Password doesn't meet requirementsfailed- Internal error
GET /user/profile
Get current user's profile.
Authentication: Required
Response:
{
"data": {
"name": "John Doe",
"age": 30
}
}PUT /user/profile
Update current user's profile.
Authentication: Required
Request:
{
"name": "Jane Doe",
"age": 31,
"city": "Stockholm"
}Response:
{
"data": {
"name": "Jane Doe",
"age": 31,
"city": "Stockholm"
}
}POST /user/push
Register push notification token.
Authentication: Required
Request:
{
"deviceId": "device-123",
"token": "firebase-token-xyz"
}Response:
{
"data": {
"status": "success"
}
}DELETE /user/push
Remove push notification token.
Authentication: Required
Request:
{
"deviceId": "device-123",
"token": "firebase-token-xyz"
}Response:
{
"data": {
"status": "success"
}
}GET /user/token
Refresh JWT token for current user (useful after role changes).
Authentication: Required
Response:
{
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}SMS Authentication
Setup
Install and configure SMS plugin:
npm install @flink-app/sms-pluginimport { sms46elksClient } from "@flink-app/sms-plugin";
genericAuthPlugin({
repoName: "userRepo",
sms: {
smsClient: new sms46elksClient({
username: process.env.SMS_USERNAME!,
password: process.env.SMS_PASSWORD!,
}),
smsFrom: "MyApp",
smsMessage: "Your verification code is {{code}}",
jwtToken: process.env.SMS_JWT_SECRET!,
codeType: "numeric",
codeLength: 6,
},
})Create SMS User
POST /user/create:
{
"username": "+46701234567",
"authentificationMethod": "sms"
}Login with SMS (Two-Step Process)
Step 1: Initiate - POST /user/login
{
"username": "+46701234567"
}Response:
{
"data": {
"status": "success",
"validationToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}Step 2: Complete - POST /user/login-by-token
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"code": "123456"
}Response:
{
"data": {
"status": "success",
"user": {
"_id": "507f1f77bcf86cd799439011",
"username": "+46701234567",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"profile": {}
}
}
}Custom Password Hashing
You can provide custom password hashing functions to support legacy systems:
import passwordHash from "password-hash";
genericAuthPlugin({
repoName: "userRepo",
createPasswordHashAndSaltMethod: async (password) => {
// Custom hash creation
return {
hash: passwordHash.generate(password),
salt: "",
};
},
validatePasswordMethod: async (password, hash, salt) => {
// Custom validation
return passwordHash.verify(password, hash);
},
})This allows validating both new and legacy password formats.
User Model
interface User {
_id: string;
username: string;
personalNumber?: string;
password?: string;
salt?: string;
pwdResetStartedAt?: string | null;
roles: string[];
authentificationMethod: "password" | "sms" | "bankid";
profile: UserProfile;
pushNotificationTokens: PushNotificationToken[];
}
interface UserProfile {
[key: string]: any; // Custom profile fields
}
interface PushNotificationToken {
deviceId: string;
token: string;
registeredAt: Date;
}Management API Integration
Integrate with @flink-app/management-api-plugin for admin interfaces:
import { managementApiPlugin } from "@flink-app/management-api-plugin";
import { GetManagementModule } from "@flink-app/generic-auth-plugin";
import schemas from "./.flink/schemas.json"; // Generated schemas
const genericAuthManagementModule = GetManagementModule({
ui: true,
profileSchema: schemas.UserProfile, // Enable profile editing
uiSettings: {
title: "App Users",
enableUserEdit: true,
enableUserCreate: true,
enableUserDelete: true,
enableUserView: true,
},
userView: {
getData(user) {
return {
data: {
"Email": user.username,
"Name": user.profile.name,
"Status": user.roles.join(", "),
},
buttons: [
{
text: "Send Email",
url: `mailto:${user.username}`,
},
],
};
},
},
});
// Add to plugins
plugins: [
genericAuthPlugin({ /* ... */ }),
managementApiPlugin({
token: process.env.ADMIN_TOKEN!,
jwtSecret: process.env.ADMIN_JWT_SECRET!,
modules: [genericAuthManagementModule],
}),
]Lifecycle Hooks
onSuccessfulLogin
Called after successful authentication:
genericAuthPlugin({
repoName: "userRepo",
onSuccessfulLogin: async (user, req) => {
// Track login
await ctx.repos.auditLogRepo.create({
userId: user._id,
action: "login",
ip: req?.ip,
timestamp: new Date(),
});
},
})onUserCreated
Called after user creation:
genericAuthPlugin({
repoName: "userRepo",
onUserCreated: async (user) => {
// Send welcome email
await ctx.plugins.email.send({
to: user.username,
subject: "Welcome!",
html: `<p>Welcome to our app, ${user.profile.name}!</p>`,
});
},
})Protecting Your Routes
Use permissions from jwt-auth-plugin to protect routes:
// Only authenticated users
export const Route: RouteProps = {
path: "/api/data",
permission: "read",
};
// Only admins
export const Route: RouteProps = {
path: "/api/admin/users",
permission: "manage_users",
};Complete Example
// index.ts
import { FlinkApp } from "@flink-app/flink";
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
import { genericAuthPlugin } from "@flink-app/generic-auth-plugin";
import { emailPlugin } from "@flink-app/email-plugin";
import { Ctx } from "./Ctx";
function start() {
const app = new FlinkApp<Ctx>({
name: "My App",
auth: jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => {
const user = await app.ctx.repos.userRepo.findById(tokenData.userId);
if (!user) throw new Error("User not found");
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 * 30, // 30 days
}),
db: {
uri: process.env.MONGODB_URI!,
},
plugins: [
emailPlugin({
provider: "sendgrid",
apiKey: process.env.SENDGRID_API_KEY!,
}),
genericAuthPlugin({
repoName: "userRepo",
enableRoutes: true,
enablePasswordReset: true,
enablePushNotificationTokens: true,
usernameFormat: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
passwordResetSettings: {
email: {
from_address: "[email protected]",
subject: "Password Reset - {{username}}",
html: `
<h2>Password Reset Request</h2>
<p>Your password reset code is: <strong>{{code}}</strong></p>
<p>This code expires in 1 hour.</p>
`,
},
code: {
numberOfDigits: 6,
lifeTime: "1h",
jwtSecret: process.env.PASSWORD_RESET_SECRET!,
},
},
onSuccessfulLogin: async (user) => {
console.log(`User ${user.username} logged in`);
},
onUserCreated: async (user) => {
console.log(`New user created: ${user.username}`);
},
}),
],
});
app.start();
}
start();Security Best Practices
1. Username Validation
Validate username format to prevent injection attacks:
usernameFormat: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/2. Password Reset Security
- Use short-lived tokens (1 hour or less)
- Use single-use tokens (set
passwordResetReusableTokens: false) - Use separate JWT secret for password resets
- Include rate limiting on reset endpoints
3. Push Token Management
Enable deregisterOtherDevices to prevent token duplication:
deregisterOtherDevices: true4. Environment Variables
Never commit secrets to version control:
JWT_SECRET=xxx
PASSWORD_RESET_SECRET=yyy
SENDGRID_API_KEY=zzz5. HTTPS Only
Always use HTTPS in production to prevent credential interception.
TypeScript Types
import {
User,
UserProfile,
UserLoginRes,
UserCreateRes,
UserPasswordResetStartRes,
UserPasswordResetCompleteRes,
GenericAuthPluginOptions,
} from "@flink-app/generic-auth-plugin";Troubleshooting
Password Reset Emails Not Sending
Solution: Verify email plugin is configured and test email settings:
await ctx.plugins.email.send({
to: "[email protected]",
subject: "Test",
html: "<p>Test email</p>",
});Users Cannot Login After Creation
Solution: Check password policy matches between jwt-auth-plugin and user creation.
SMS Code Not Received
Solution: Verify SMS plugin configuration and check SMS provider logs.
License
MIT
