Flink plugin that provides a generic user authentification solution.
npm install @flink-app/generic-auth-pluginA 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.
- 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)
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)
``bash`
npm install @flink-app/generic-auth-plugin @flink-app/jwt-auth-plugin @flink-app/email-plugin
For SMS authentication:
`bash`
npm install @flink-app/sms-plugin
Create a user repository in your project:
src/repos/UserRepo.ts:
`typescript
import { FlinkRepo } from "@flink-app/flink";
import { User } from "@flink-app/generic-auth-plugin";
import { Ctx } from "../Ctx";
class UserRepo extends FlinkRepo
export default UserRepo;
`
src/Ctx.ts:
`typescript
import { FlinkContext } from "@flink-app/flink";
import UserRepo from "./repos/UserRepo";
export interface Ctx extends FlinkContext {
repos: {
userRepo: UserRepo;
};
}
`
index.ts:
`typescript
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
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: "noreply@example.com",
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();
`
| 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 |
`typescript`
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
`typescript`
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
}
The plugin exposes the following functions via ctx.plugins.genericAuthPlugin:
Authenticate a user with username and password or initiate SMS authentication.
`typescript`
const result = await ctx.plugins.genericAuthPlugin.loginUser(
repo,
auth,
username,
password,
validatePasswordMethod?,
smsOptions?,
onSuccessfulLogin?,
req?
);
Returns: UserLoginRes
Complete SMS authentication using the validation token and code.
`typescript`
const result = await ctx.plugins.genericAuthPlugin.loginByToken(
repo,
auth,
token,
code,
jwtSecret
);
Returns: UserLoginRes
Create a new user with password, SMS, or BankID authentication.
`typescript`
const result = await ctx.plugins.genericAuthPlugin.createUser(
repo,
auth,
username,
password,
authentificationMethod, // "password" | "sms" | "bankid"
roles,
profile,
createPasswordHashAndSaltMethod?,
onUserCreated?,
personalNumber?
);
Returns: UserCreateRes
Change a user's password.
`typescript`
const result = await ctx.plugins.genericAuthPlugin.changePassword(
repo,
auth,
userId,
newPassword,
createPasswordHashAndSaltMethod?
);
Returns: UserPasswordChangeRes
Initiate password reset process and send email with code.
`typescript`
const result = await ctx.plugins.genericAuthPlugin.passwordResetStart(
repo,
auth,
jwtSecret,
username,
numberOfDigits?,
lifeTime?,
passwordResetReusableTokens?
);
Returns: UserPasswordResetStartRes
Complete password reset with token, code, and new password.
`typescript`
const result = await ctx.plugins.genericAuthPlugin.passwordResetComplete(
repo,
auth,
jwtSecret,
passwordResetToken,
code,
newPassword,
createPasswordHashAndSaltMethod?,
passwordResetReusableTokens?
);
Returns: UserPasswordResetCompleteRes
When enableRoutes: true (default), the following endpoints are automatically registered:
Create a new user account.
Request:
`json`
{
"username": "user@example.com",
"password": "mypassword123",
"authentificationMethod": "password",
"profile": {
"name": "John Doe",
"age": 30
}
}
Response:
`json`
{
"data": {
"status": "success",
"user": {
"_id": "507f1f77bcf86cd799439011",
"username": "user@example.com",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
}
Error Codes:
- userExists - Username already takenpasswordError
- - Password doesn't meet requirementsusernameError
- - Username doesn't meet format requirements
Login with username and password.
Request:
`json`
{
"username": "user@example.com",
"password": "mypassword123"
}
Response:
`json`
{
"data": {
"status": "success",
"user": {
"_id": "507f1f77bcf86cd799439011",
"username": "user@example.com",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"profile": {
"name": "John Doe",
"age": 30
}
}
}
}
Error Codes:
- failed - Invalid username or password
Initiate password reset (sends email with code).
Request:
`json`
{
"username": "user@example.com"
}
Response:
`json`
{
"data": {
"status": "success",
"passwordResetToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
Error Codes:
- userNotFound - User doesn't exist
Complete password reset with code from email.
Request:
`json`
{
"passwordResetToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"code": "123456",
"password": "mynewpassword123"
}
Response:
`json`
{
"data": {
"status": "success"
}
}
Error Codes:
- invalidCode - Code is wrong or expiredpasswordError
- - Password doesn't meet requirementsuserNotFound
- - User not found
Change password for authenticated user.
Authentication: Required
Request:
`json`
{
"password": "mynewpassword123"
}
Response:
`json`
{
"data": {
"status": "success"
}
}
Error Codes:
- passwordError - Password doesn't meet requirementsfailed
- - Internal error
Get current user's profile.
Authentication: Required
Response:
`json`
{
"data": {
"name": "John Doe",
"age": 30
}
}
Update current user's profile.
Authentication: Required
Request:
`json`
{
"name": "Jane Doe",
"age": 31,
"city": "Stockholm"
}
Response:
`json`
{
"data": {
"name": "Jane Doe",
"age": 31,
"city": "Stockholm"
}
}
Register push notification token.
Authentication: Required
Request:
`json`
{
"deviceId": "device-123",
"token": "firebase-token-xyz"
}
Response:
`json`
{
"data": {
"status": "success"
}
}
Remove push notification token.
Authentication: Required
Request:
`json`
{
"deviceId": "device-123",
"token": "firebase-token-xyz"
}
Response:
`json`
{
"data": {
"status": "success"
}
}
Refresh JWT token for current user (useful after role changes).
Authentication: Required
Response:
`json`
{
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
Install and configure SMS plugin:
`bash`
npm install @flink-app/sms-plugin
`typescript
import { 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,
},
})
`
POST /user/create:
`json`
{
"username": "+46701234567",
"authentificationMethod": "sms"
}
Step 1: Initiate - POST /user/login
`json`
{
"username": "+46701234567"
}
Response:
`json`
{
"data": {
"status": "success",
"validationToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
Step 2: Complete - POST /user/login-by-token
`json`
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"code": "123456"
}
Response:
`json`
{
"data": {
"status": "success",
"user": {
"_id": "507f1f77bcf86cd799439011",
"username": "+46701234567",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"profile": {}
}
}
}
You can provide custom password hashing functions to support legacy systems:
`typescript
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.
`typescript
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;
}
`
Called after successful authentication:
`typescript`
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(),
});
},
})
Called after user creation:
`typescript
genericAuthPlugin({
repoName: "userRepo",
onUserCreated: async (user) => {
// Send welcome email
await ctx.plugins.email.send({
to: user.username,
subject: "Welcome!",
html:
Welcome to our app, ${user.profile.name}!
,
});
},
})
`Protecting Your Routes
Use permissions from jwt-auth-plugin to protect routes:
`typescript
// 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
`typescript
// 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({
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: "noreply@example.com",
subject: "Password Reset - {{username}}",
html:
Your password reset code is: {{code}}
This code expires in 1 hour.
,
},
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
$3
Validate username format to prevent injection attacks:
`typescript
usernameFormat: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
`$3
- 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
Enable
deregisterOtherDevices to prevent token duplication:`typescript
deregisterOtherDevices: true
`$3
Never commit secrets to version control:
`bash
JWT_SECRET=xxx
PASSWORD_RESET_SECRET=yyy
SENDGRID_API_KEY=zzz
`$3
Always use HTTPS in production to prevent credential interception.
TypeScript Types
`typescript
import {
User,
UserProfile,
UserLoginRes,
UserCreateRes,
UserPasswordResetStartRes,
UserPasswordResetCompleteRes,
GenericAuthPluginOptions,
} from "@flink-app/generic-auth-plugin";
`Troubleshooting
$3
Solution: Verify email plugin is configured and test email settings:
`typescript
await ctx.plugins.email.send({
to: "test@example.com",
subject: "Test",
html: "Test email
",
});
``Solution: Check password policy matches between jwt-auth-plugin and user creation.
Solution: Verify SMS plugin configuration and check SMS provider logs.
MIT