Package Exports
- @bhudevswayam/blueprint-engine
- @bhudevswayam/blueprint-engine/src/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@bhudevswayam/blueprint-engine) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
@bhudevswayam/blueprint-engine
An opinionated architectural framework for Node.js. Multi-tenant auth, MFA, OAuth, RBAC, and rate limiting — all wired into your Express app with one blueprintEngine(secret) call.
⚠️ This library is multi-tenant by design
Every User, Tenant, RefreshToken, and LoginAttempt document carries a
tenantId. Every protected request must include anx-tenant-idheader that matches the tenant of the authenticated user —protectreturns 403 on mismatch andsetTenantreturns 400 when the header is missing.If your client does not send
x-tenant-idon every authenticated request, things will break:
- Any route guarded by
setTenantreturns 400 Missing x-tenant-id header.- Any route guarded by
protectreturns 403 Tenant mismatch if the header value doesn't equaluser.tenantId(unless the user's role issuperadmin).- Refresh token rotation, MFA challenge, and logout all fail with 403 if the header is wrong or missing.
The tenant is created automatically at
/register(derived from the user's name) and encoded into the JWT, so after login your client can readtenantIdfrom the decoded access token and send it on every subsequent request. Single-tenant apps: hardcode one value client-side. There is no "turn multi-tenancy off" flag.
Install
npm install @bhudevswayam/blueprint-enginePeer dependencies (install separately). dotenv is also needed so the app can read MONGO_URI, JWT_SECRET, and the OAuth vars from a .env file:
npm install express mongoose dotenvexpress@^5.2.1mongoose@^8.5.1dotenv(any recent version)
Requires Node.js 18 or newer.
Quickstart
const express = require('express');
const mongoose = require('mongoose');
require('dotenv').config();
const blueprintEngine = require('@bhudevswayam/blueprint-engine');
const app = express();
app.use(express.json());
if (!process.env.MONGO_URI) {
console.error('MONGO_URI is missing in demo/.env');
process.exit(1);
}
mongoose.set('bufferCommands', false);
mongoose.connect(process.env.MONGO_URI, { serverSelectionTimeoutMS: 5000 })
.then(() => console.log('MongoDB Connected'))
.catch(err => {
console.error('MongoDB Error:', err.message);
process.exit(1);
});
const engine = blueprintEngine(process.env.JWT_SECRET || 'dev-secret', {
hooks: {
onRegister: async (user) => console.log('[HOOK] Registered:', user.email),
onLogin: async (user) => console.log('[HOOK] Login:', user.email),
onLogout: async (user) => console.log('[HOOK] Logout:', user.email),
onPasswordReset: async (user, token) => console.log('[HOOK] Password reset token:', token),
},
oauth: {
google: {
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
successRedirect: process.env.OAUTH_SUCCESS_REDIRECT,
}
}
});
// OAuth provider redirects (and their callbacks) carry no x-tenant-id header,
// so skip the tenant guard for those paths.
const OAUTH_PATHS = ['/api/auth/google', '/api/auth/facebook', '/api/auth/twitter'];
app.use((req, res, next) => {
if (OAUTH_PATHS.some(p => req.path.startsWith(p))) return next();
return engine.setTenant(req, res, next);
});
app.use('/api/auth', engine.auth);
app.get('/health', (req, res) => res.json({
status: 'ok',
tenantId: req.tenantId
}));
// Error handler — must be last (4-arg signature)
app.use((err, req, res, next) => {
console.error('[ERROR]', err.message);
res.status(err.statusCode || 500).json({
success: false,
message: err.message || 'Internal server error'
});
});
app.listen(4000, () => {
console.log('Demo running on http://localhost:4000');
console.log('');
console.log('Auth endpoints:');
console.log(' POST /api/auth/register');
console.log(' POST /api/auth/login');
console.log(' POST /api/auth/refresh');
console.log(' POST /api/auth/logout');
console.log(' POST /api/auth/forgot-password');
console.log(' POST /api/auth/reset-password');
console.log('');
console.log('MFA endpoints:');
console.log(' GET /api/auth/mfa/setup');
console.log(' POST /api/auth/mfa/verify-setup');
console.log(' POST /api/auth/mfa/challenge');
console.log(' POST /api/auth/mfa/disable');
console.log('');
console.log('Other:');
console.log(' GET /health');
console.log('');
console.log('Required header: x-tenant-id: tenant-001');
});That's a fully functional backend with register, login, refresh, logout, password reset, MFA, and Google OAuth endpoints — all mounted under /api/auth.
.env structure
Create a .env file next to app.js with the following keys:
PORT=4000
MONGO_URI=mongodb://localhost:27017/tempdbauth
JWT_SECRET=supersecret_jwt_key_here_change_me
JWT_EXPIRES_IN=7d
SUPER_ADMIN_EMAIL=super@admin.com
SUPER_ADMIN_PWD=change_me
SUPER_TENANT_ID=global
GOOGLE_CLIENT_ID=<your-google-client-id>
GOOGLE_CLIENT_SECRET=<your-google-client-secret>
GOOGLE_CALLBACK_URL=http://localhost:4000/api/auth/google/callback
OAUTH_SUCCESS_REDIRECT=http://localhost:3000/GOOGLE_CALLBACK_URL must exactly match a URI listed under Authorized redirect URIs in your Google Cloud Console OAuth client — otherwise Google returns Error 400: redirect_uri_mismatch. OAUTH_SUCCESS_REDIRECT is where the user lands after successful auth (typically your frontend SPA); the library appends ?accessToken=...&refreshToken=... to it.
API reference
blueprintEngine(jwtSecret, options?)
Returns { auth, setTenant, protect, authorize }.
| Param | Type | Required | Description |
|---|---|---|---|
jwtSecret |
string |
yes | Secret used to sign access and refresh tokens. Must be long and random in production. |
options.hooks |
object |
no | Optional lifecycle callbacks — see below. |
options.oauth.google |
object |
no | Google OAuth config. If absent, /api/auth/google is not mounted. |
options.mfaSecret |
string |
no | AES key used to encrypt TOTP secrets before storing them. Falls back to jwtSecret if omitted. |
Lifecycle hooks
All hooks are optional. They receive the affected user document.
blueprintEngine(secret, {
hooks: {
onRegister: async (user) => {},
onLogin: async (user) => {},
onLogout: async (user) => {},
onMFAChallenge: async (user, method, otpCode) => {
// Called when email-method MFA is selected. `otpCode` is the 6-digit
// code — deliver it however you want (email, SMS, Slack).
},
onPasswordReset: async (user, rawResetToken) => {
// Send rawResetToken to the user via email, link back to your reset page.
},
},
});Endpoints
All endpoints are mounted under the router returned as be.auth.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /register |
public | Create an account. Creates a new tenant automatically. Returns user + access token. |
| POST | /login |
public | Authenticate. Returns { accessToken, refreshToken } — or { mfaPendingToken } if MFA is enabled. |
| POST | /refresh |
public (needs refresh token in body) | Rotate refresh token, issue new pair. |
| POST | /logout |
bearer token | Blacklist the access token and delete the refresh token. |
| POST | /forgot-password |
public | Generate a reset token, fire onPasswordReset hook. Always returns 200 to avoid leaking which emails exist. |
| POST | /reset-password |
public (needs reset token) | Consume reset token, set new password. Body: { token, password }. |
| POST | /change-password |
bearer token | Authenticated password change. Body: { currentPassword, newPassword }. |
| GET | /mfa/setup |
bearer token | Generate TOTP secret + QR code URI. MFA is not yet enabled. |
| POST | /mfa/verify-setup |
bearer token | Submit the first TOTP code to enable MFA. Returns 8 one-time backup codes. |
| POST | /mfa/challenge |
mfaPendingToken in body | Second factor verification. Accepts TOTP, email OTP, or a backup code. Returns access + refresh tokens. |
| POST | /mfa/disable |
bearer token | Disable MFA after verifying current TOTP or backup code. |
| GET | /google |
public | Initiate Google OAuth (mounted only if options.oauth.google is configured). |
| GET | /google/callback |
— | Google redirects here. Issues tokens, redirects to successRedirect with accessToken + refreshToken as query params. |
Request / response shapes
POST /register
Body: { name, email, password, role?, ...extraProfileFields }
201 → { id, name, email, role, tenantId, token }name is slugified into the tenant ID: john_doe_<Date.now()>. Role defaults to "user". Any extra body fields are stored on the User document as-is.
POST /login
Body: { email, password }
200 (no MFA) → { success: true, data: { id, name, email, role, tenantId, accessToken, refreshToken } }
200 (MFA enabled) → { success: true, data: { mfaPending: true, mfaPendingToken, method } }Rate-limited: 10 attempts per tenantId + IP per 15 minutes. Brute-force lockout: 5 consecutive failures per tenantId:email locks the account for 15 minutes.
POST /refresh
Body: { refreshToken }
200 → { success: true, data: { accessToken, refreshToken } } // new pair; old refresh is deletedPOST /mfa/challenge
Body: { mfaPendingToken, code }
200 → { success: true, data: { id, name, email, role, tenantId, accessToken, refreshToken } }Middleware
protect
Verifies the Authorization: Bearer <token> header, decodes the JWT, loads the user by ID, checks against the blacklist, and enforces tenant match (if x-tenant-id header is present it must equal user.tenantId, unless role is superadmin). Attaches the full Mongoose user document to req.user.
app.get('/private', be.protect, handler);authorize(...allowedRoles)
Factory. Returns a middleware that 403s if req.user.role isn't in the list. Must come after protect.
app.get('/admin', be.protect, be.authorize('admin', 'superadmin'), handler);setTenant
Requires the x-tenant-id header. Returns 400 if missing, otherwise sets req.tenantId. Safe to mount globally — OAuth provider paths (/auth/google, /auth/facebook, /auth/twitter and their callbacks) are automatically exempt because top-level browser redirects from those providers can't carry custom headers. Register and login do require the header (your client should send it on every request, including those).
// Global — recommended
app.use(engine.setTenant);
app.use('/api/auth', engine.auth);
// Or scoped to specific routers if you prefer
app.use('/api/bookings', engine.setTenant, bookingRouter);Multi-tenancy
Every User and resource document carries a tenantId. Register creates a tenant implicitly from the user's name (name_timestamp), so a fresh signup produces a fresh isolated tenant with that user as its first member.
- Clients authenticate by sending
Authorization: Bearer <token>andx-tenant-id: <theirTenant>on every protected request. protectenforces the tenant match — sending another tenant's ID returns 403.superadminrole bypasses the tenant check.
Schemas
User
{ name, email (unique), password (bcrypt, optional for OAuth), role (enum), tenantId,
phoneNo, addressLine1/2, city, state, zipCode,
provider (google/facebook/twitter), providerId,
passwordResetToken/Expires,
mfa: { enabled, secret (AES-encrypted), backupCodes (bcrypt-hashed), method },
otpCode/otpExpires }Tenant
{ name, tenantId, meta: Object }Google OAuth
- Create an OAuth 2.0 Client ID in Google Cloud Console. Under Authorized redirect URIs, add the exact URL of your callback (the value you'll put in
GOOGLE_CALLBACK_URL). Under Authorized JavaScript origins, add the origin of your frontend (e.g.http://localhost:3000). - Set env vars:
The callback URL points at the API server (this library handles it). The success redirect points at your frontend — that's where the user lands withGOOGLE_CLIENT_ID=<client id> GOOGLE_CLIENT_SECRET=<client secret> GOOGLE_CALLBACK_URL=http://localhost:4000/api/auth/google/callback OAUTH_SUCCESS_REDIRECT=http://localhost:3000/?accessToken=...&refreshToken=...appended for the SPA to read. - Pass them into the engine:
blueprintEngine(secret, { oauth: { google: { clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, callbackURL: process.env.GOOGLE_CALLBACK_URL, successRedirect: process.env.OAUTH_SUCCESS_REDIRECT, // optional, defaults to '/' }, }, });
- The library mounts
GET /googleandGET /google/callbackon the auth router. The callback issues access + refresh tokens and redirects tosuccessRedirect?accessToken=...&refreshToken=....
Browser-initiated OAuth cannot carry an x-tenant-id header, so OAuth users land under tenantId: 'global'. The setTenant middleware automatically skips OAuth provider paths, so global mounting is safe.
Common gotcha — Error 400: redirect_uri_mismatch: the value you send to Google as redirect_uri must match a URI registered on the OAuth client exactly (scheme, host, port, path, no trailing slash differences). The string in your GOOGLE_CALLBACK_URL env var and the entry in Google Cloud Console must be character-for-character identical.
Facebook and Twitter strategies ship installed but are not yet wired into the auth router. Wiring arrives in v1.1.
MFA
Two-phase login when user.mfa.enabled === true:
POST /login→ returns{ mfaPendingToken }(5-minute JWT, containsmfaPending: true).POST /mfa/challengewith{ mfaPendingToken, code }→ returns real access + refresh tokens.
Enrollment flow
GET /mfa/setup(bearer token) — generates a TOTP secret, AES-encrypts it onto the user doc, returns{ otpauthUrl, qrCodeDataURL }. MFA is not yet enabled.- User scans the QR in their authenticator app.
POST /mfa/verify-setupwith the first 6-digit code — enables MFA, returns 8 raw backup codes (shown only once, stored bcrypt-hashed).
Email-method MFA (user.mfa.method === 'email') generates a 6-digit OTP stored on the user doc for 10 minutes and fires hooks.onMFAChallenge(user, 'email', otpCode) for delivery.
Backup codes work as one-time fallbacks in both /mfa/challenge and /mfa/disable.
Security notes
jwtSecretmust be a long random string in production. 32+ bytes fromcrypto.randomBytes(32).toString('hex')is a good choice. The same secret signs both access and refresh tokens.- Access tokens expire in 15 minutes. Refresh tokens expire in 30 days, stored as SHA-256 hashes — the raw token is returned once to the client at login.
- Passwords are bcrypt-hashed (10 rounds). There is no minimum length enforced at the library layer — enforce your policy with a validator before calling
/register. - TOTP secrets are AES-encrypted with
options.mfaSecret(falls back tojwtSecret) before DB storage. - Backup codes are bcrypt-hashed.
- Login is rate-limited (10/15min per
tenantId+IP) and brute-force protected (5 failures → 15-minute lockout pertenantId:email). - Token revocation: logout blacklists the access token. If
REDIS_URLis set andioredisis installed, the blacklist is shared across instances — otherwise it's in-memory per process.
License
MIT — see LICENSE.
Author
Swayam Pandya