@hapiboo/module-auth
v2.0.1
Published
MK13 Studio Hapiboo - Auth Module
Readme
@hapiboo/module-auth
Authentication and authorization for HapiBoo applications: password credentials, registration and invitations, JWT access and refresh tokens, account membership, roles and permissions, and Express routes and middleware.
Persistence uses @hapiboo/flux and your database provider. Applications supply
email delivery, account provisioning, token data, and deployment configuration.
Installation
pnpm add @hapiboo/module-auth @hapiboo/core @hapiboo/flux @hapiboo/server express
pnpm add -D typescript @types/express @types/nodeEnable experimentalDecorators in TypeScript applications using Flux models.
The package contains JavaScript and TypeScript declarations. Its public entry
point exports auth, the Credential, User, Permission, Role, and
UserAccess models, configuration interfaces, and Express request interfaces.
Application Setup
import express from 'express';
import type { Express } from 'express';
import { auth } from '@hapiboo/module-auth';
import type { IAuthPerformers, IAuthSettings } from '@hapiboo/module-auth';
import { fluxApplication } from '@hapiboo/flux';
import type { IDBProviderFlux } from '@hapiboo/flux';
import { routingProvider } from '@hapiboo/server';
export function configureAuth(
jwtSecret: string,
settings: IAuthSettings,
performers: IAuthPerformers,
provider: IDBProviderFlux
): Express {
const app = express();
app.use(express.json());
auth.init(jwtSecret, settings, performers);
fluxApplication.registerModule(auth.module);
fluxApplication.initialize(provider);
fluxApplication.start();
routingProvider.processApp(app, auth.routes);
return app;
}Register all application modules before initializing and starting Flux. Coordinate
database readiness before exposing routes. auth.module registers the five auth
models; initial security data and the first administrator are created separately
through auth.services.authentication.general.initData(baseURL).
Use a securely generated JWT signing secret and HTTPS in deployment. Configuration
and caches are shared within the loaded module instance. HTTP controllers obtain
the frontend base URL from process.env.FRONTEND_URL; set it before requests that
generate confirmation, invitation, or recovery email links.
Settings
Pass a complete IAuthSettings object to auth.init(jwtSecret, settings, performers):
| Setting | Purpose |
| --- | --- |
| allowSelfRegistration | Whether users may register without an invitation. |
| allowMultiLogin | Multi-login configuration flag exposed by the settings service. |
| allowTestMode | Controls use of the fixed confirmation code when test mode is active. Keep disabled in production. |
| passwordHashLength | Password hash length passed to the crypto package. |
| passwordHashIterations | Password hash iteration count passed to the crypto package. |
| firstAdmin | Initial administrator's name and email. |
| overrulingPermission | Permission ID that satisfies every permission check, or undefined. |
| getPermissions? | Supplies initial Permission models. |
| getRoles? | Supplies initial Role models. |
Choose hashing parameters according to the crypto provider and your deployment's security and performance requirements. Initial role and permission definitions are inserted when their IDs are absent; existing definitions are retained.
Application Performers
IAuthPerformers integrates the module with application-specific services:
| Method | Return and responsibility |
| --- | --- |
| sendConfirmation(emailAddress, displayName, isInvited, confirmationLink) | IResponsePromiseVoid; deliver a registration or invitation link. |
| sendRecover(emailAddress, recoverLink) | IResponsePromiseVoid; deliver a password recovery link. |
| sendAfterPasswordChanged(emailAddress) | IResponsePromiseVoid; deliver a password-change notification. |
| afterRegistration(account_id, user_id, account_name, account_email) | IResponsePromise<{ id: string }>; provision application account data and return its ID. |
| getTokenPayload<T>(user) | IResponsePromise<T>; supply application data included in standard users' access tokens. |
IAuthEmailLink contains url and title. Generated frontend paths are
confirm/?code=..., invitation/?code=..., and recover/?code=....
Performers report failures through rejected HapiBoo operations using
ResponseError. These are the callback-based operations from @hapiboo/core,
not native Promises; use .then, .catch, .finally, and core composition
helpers rather than await or native Promise combinators.
Authentication Services
Access these through auth.services.authentication. Service calls are trusted
server-side APIs: the application is responsible for authorizing the caller and
validating externally supplied identifiers and values.
Password login, refresh login, and password changes share the same eligibility
rule: both the user and credential must have status active. This check precedes
password changes and token generation. Newly registered or invited users activate
their accounts through the confirmation actions instead.
| Namespace and method | Purpose |
| --- | --- |
| general.initData(baseURL) | If there are no users, initialize security definitions and invite the configured first administrator. Returns { id }, with an empty ID when users already exist. |
| general.testMode() | Toggle process-local test mode and return { testMode }. |
| general.get(user_id) | Retrieve a user model. |
| me.passwordLogin(name, password) | Authenticate an active user and password credential and return { token, refreshToken }. |
| me.refreshTokenLogin(user_id, refreshCode) | Issue tokens using a stored credential refresh code. |
| me.changePassword(user_id, credentialName, oldPassword, newPassword) | Request a password change, token generation, and notification. |
| me.get(user_id) | Return profile data, account ID, admin flag, and permission IDs. |
| me.update(user_id, name) | Update the user's display name. |
| user.registerPassword(credentialName, userDisplayName, userEmailAddress, password, account_name, account_email, baseUrl) | Register a pending standard user and invoke account provisioning; returns { user, account }. |
| user.invite(account_id, credentialName, displayName, emailAddress, baseUrl) | Invite a standard user into an existing account; returns { id }. |
| user.updatePayload(user_id, account_id, payload) | Return a newly signed standard-user access token with application data. |
| user.remove(user_id, currentUserId, account_id) | Remove another standard user from the specified account, with associated access records and credentials. |
| user.getForAccount(account_id) | Retrieve standard users for an account. |
| admin.invite(name, emailAddress, displayName, baseURL) | Invite an administrator; returns { id }. |
| admin.remove(id, currentUserId) | Remove another administrator and associated credentials. |
| admin.getAll() | Retrieve administrator user models. |
| actions.confirmRegistration(code, password) | Confirm a pending registration and return tokens. |
| actions.confirmInvitation(code, password) | Accept an invitation with a password and return tokens. |
| actions.resendConfirmation(user_id, baseUrl) | Send the existing confirmation or invitation link for a newly created user. |
| actions.forgotPassword(credentialName, baseUrl) | Generate a recovery code, store it, and send a recovery link. |
| actions.recoverPassword(recovery, password) | Apply a valid recovery code and password and return tokens. |
Application services control account provisioning, transactional boundaries, request throttling, password policy, and public error presentation.
HTTP Routes
auth.routes mounts the following routes under /auth. Install an Express JSON
body parser before mounting them. Field names below describe JSON request bodies.
| Method and path | Input | Module middleware |
| --- | --- | --- |
| PUT /auth/init | No body. Initialize security data and the first administrator when there are no users. | None. |
| POST /auth/init/test | No body. Toggle process-local test mode. | None. |
| POST /auth/user/register | name, login, password. Login also supplies user/account email; name supplies account name. | None. |
| POST /auth/user/confirm | code, password. | None. |
| POST /auth/user/invite | code, password. Accepts an existing invitation; does not create one. | None. |
| POST /auth/password/login | login, password. | None. |
| POST /auth/password/forgot | email, used as the credential name. | None. |
| POST /auth/password/recover | recovery, password. | None. |
| POST /auth/password/refresh | Refresh token in the authorization header; no body. | refresh. |
| POST /auth/password/password | email, oldPassword, newPassword. | simple. |
| GET /auth/profile | No body. | simple. |
| POST /auth/profile | name. | simple. |
| GET /auth/access/roles | No body. | simple. |
| GET /auth/access/permissions | No body. | simple. |
Restrict initialization and test-mode endpoints through application-level access
controls. allowTestMode controls fixed-code generation; it does not protect the
test-mode route. Apply request throttling and your password policy at the public
API boundary. Credential-name normalization should be consistent across
registration, invitations, login, and password recovery; password login lowercases
the supplied name.
Password values are passed as strings to the crypto provider without Base64 decoding by this module. Use the same string representation consistently for registration, login, confirmation, recovery, and password changes.
Authorization Services
Use auth.services.authorization:
| Namespace and method | Purpose |
| --- | --- |
| general.initializeSecurityModel() | Ensure configured initial permissions and roles exist. |
| permission.create(name) / permission.edit(id, name) / permission.all() | Manage permission definitions. |
| role.create(name, description, lucide_icon, permissions) | Create a role containing permission IDs. |
| role.edit(id, name, description, lucide_icon, permissions) / role.all() | Update or list role definitions. |
| access.byRole.assign(account_id, user_id, role_id) | Materialize a role's permissions as user-access records. |
| access.byRole.assignList(account_id, user_id, role_ids) | Replace role-derived access records and return the user's resulting access records. |
| access.byRole.remove(account_id, user_id) | Remove role-derived access records. |
| access.byPermission.assign(user_id, permission_id) / assignList(user_id, permission_ids) | Reserved functions; currently return void without writing records. |
| access.byPermission.remove(account_id, user_id) | Returns a successful void operation without changing records. |
| access.removeAllForUser(user_id) | Remove all persisted user-access records. |
| access.getForAccount(account_id) / access.getForUser(user_id) | Retrieve access records. |
| access.split(records) | Separate role-derived and direct permission records, representing each role once. |
| middleware.permission(permission_id) | Express middleware requiring the permission or the configured overruling permission. |
Role assignment stores the permission IDs contained in the role at assignment time. Middleware loads permission IDs into a process-local cache on first use. Access-record writes through the authorization service invalidate the affected user's cached permissions in the current process. Subsequent checks reload the records. Results from reads started before invalidation are discarded; failed writes also invalidate the cache because the persisted outcome may be uncertain.
While permissions are loading, middleware checks again every 100 ms using a
one-second wait budget measured from the start of each request's permission
check. Read failures and cache invalidation do not reset that budget. Once it
expires, middleware returns 403 with permission data loading, try again later
and stops scheduling checks for that request. Later read completion can populate
the cache for subsequent requests but does not resume the expired request.
Each process has its own cache. Applications coordinate invalidation across workers and changes made directly in storage. Editing a role definition does not rewrite previously materialized user-access records.
Security Services and Middleware
Access these through auth.services.security. initialize(secret) sets the JWT
secret and clears registered client secrets; auth.init already calls it.
registerClient(client_secret) adds a client secret accepted by client middleware.
| Token factory | Result |
| --- | --- |
| token.data(user_id, account_id, admin, expirationInHours, data) | Signed token containing identity, account, admin flag, and optional application data. |
| token.refresh(user_id, refreshCode, expirationInHours) | Signed token containing user_id and refresh code. |
| token.client(client) | Signed client token valid for one hour. |
The authentication service issues access tokens valid for 24 hours and refresh
tokens valid for 720 hours. Requests use Authorization: Bearer <token>.
| Middleware | Request payload and checks |
| --- | --- |
| client | Verify a JWT with a client value matching a registered client secret. |
| simple | Verify the JWT and populate user_id and admin; sets account_id to undefined. |
| account | Verify the JWT and populate user_id, account_id, and admin. |
| full<TData> | As above, with optional application data. |
| refresh | Verify the JWT and populate user_id and code. |
| checkAdmin | Run simple, then require a truthy admin flag. |
| checkAccount | Run account, then require a truthy account ID. |
| checkData<TData> | Run full, then require truthy application data. |
Basic middleware populates claims; use the appropriate additional checks for the route's contract. Access and refresh tokens share the configured signing secret; basic JWT middleware verifies the signature and expiration without applying a token-purpose discriminator or rereading user and credential status. Applications define their required claim validation and account-state policy.
Permission middleware consumes an existing request.payload, so identity
middleware must run first. For example:
import { Router } from 'express';
import { auth } from '@hapiboo/module-auth';
import type { IFluxRequest } from '@hapiboo/module-auth';
const router = Router();
router.get(
'/reports',
auth.services.security.middleware.checkAccount,
auth.services.authorization.middleware.permission('reports.read'),
(request: IFluxRequest, response) => {
response.json({ account_id: request.payload!.account_id });
}
);IFluxRequest exposes the identity payload; IFluxTokenRequest exposes refresh
claims; IFluxDataRequest<TData> adds typed application data. Application routes
remain responsible for account-scoping data access and checking resource ownership.
Models and Storage
| Model | Storage table | Main data |
| --- | --- | --- |
| User | authentication.users | Identity, account ID, standard/admin type, created/active/banned/deleted status, profile, and timestamps. |
| Credential | authentication.credentials | Credential name and user ID, password/sso/token type, active/pending/revoked status, password data, confirmation/invitation/recovery codes, and refresh code. |
| Permission | authentication.permissions | Permission ID and name. |
| Role | authentication.roles | Role ID, name, description, icon name, and permission ID array. |
| UserAccess | authentication.user_access | Account ID, user ID, permission ID, and optional role ID. |
Models expose clone and creation/update helpers. Credential includes helpers
for password checking, password changes, confirmation, invitation, recovery,
refresh-code assignment, and invalidation. UserAccess can represent role-derived
or directly assigned permissions. Apply Flux decorators only to final model
classes; do not derive new decorated models from these classes.
The provider must persist and reconstruct the complete model, including nested password/profile data and fields without column decorators. Decorated columns also supply separately queryable attributes and index definitions. Do not return raw credential records to public clients; choose explicit response fields.
License
ISC.
