npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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:

Installation

npm install @flink-app/generic-auth-plugin @flink-app/jwt-auth-plugin @flink-app/email-plugin

For SMS authentication:

npm install @flink-app/sms-plugin

Setup

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 taken
  • passwordError - Password doesn't meet requirements
  • usernameError - 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 expired
  • passwordError - Password doesn't meet requirements
  • userNotFound - 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 requirements
  • failed - 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-plugin
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,
  },
})

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: true

4. Environment Variables

Never commit secrets to version control:

JWT_SECRET=xxx
PASSWORD_RESET_SECRET=yyy
SENDGRID_API_KEY=zzz

5. 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