mbkauthe 5.0.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # MBKAuthe - Node.js Authentication System
2
2
 
3
3
  [![Version](https://img.shields.io/npm/v/mbkauthe.svg)](https://www.npmjs.com/package/mbkauthe)
4
- [![License](https://img.shields.io/badge/License-GPL--2.0-blue.svg)](LICENSE)
5
- [![Node.js](https://img.shields.io/badge/node-%3E%3D14.0.0-brightgreen.svg)](https://nodejs.org/)
4
+ [![License](https://img.shields.io/badge/License-LGPL--3.0-blue.svg)](LICENSE)
5
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
6
6
  [![Publish](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/publish.yml)
7
7
  [![Downloads](https://img.shields.io/npm/dm/mbkauthe.svg)](https://www.npmjs.com/package/mbkauthe)
8
8
 
@@ -61,7 +61,7 @@ The schema includes a default SuperAdmin user (`support` / `12345678`). Change t
61
61
  ```javascript
62
62
  import express from "express";
63
63
  import dotenv from "dotenv";
64
- import mbkauthe, { sessVal, roleChk } from "mbkauthe";
64
+ import mbkauthe, { sessVal, roleChk, sessRole } from "mbkauthe";
65
65
 
66
66
  dotenv.config();
67
67
 
@@ -77,6 +77,11 @@ app.get("/admin", sessVal, roleChk("SuperAdmin"), (req, res) => {
77
77
  res.send("Admin Panel");
78
78
  });
79
79
 
80
+ // Or combine session and role checks into one middleware:
81
+ app.get("/admin", sessRole("SuperAdmin"), (req, res) => {
82
+ res.send("Admin Panel");
83
+ });
84
+
80
85
  app.listen(3000);
81
86
  ```
82
87
 
@@ -149,7 +154,7 @@ Vercel deployments can use shared OAuth credentials through `mbkauthShared`.
149
154
 
150
155
  ## License
151
156
 
152
- GPL v2.0 - see [LICENSE](LICENSE).
157
+ LGPL v3.0 - see [LICENSE](LICENSE).
153
158
 
154
159
  ## Author
155
160
 
@@ -35,7 +35,7 @@ This document describes the environment variables MBKAuth expects and keeps brie
35
35
  - Required: Yes
36
36
 
37
37
  - DOMAIN
38
- - Description: App domain (e.g., `localhost` or `yourdomain.com`). Required when deployed.
38
+ - Description: Root app domain used for cross-subdomain cookie sharing in production (e.g., `yourdomain.com`, not `auth.yourdomain.com`). When `IS_DEPLOYED=true`, session cookies are scoped to `.yourdomain.com` so they are sent on all subdomains.
39
39
  - Example: `"DOMAIN":"localhost"`
40
40
  - Required: Yes
41
41
 
@@ -95,11 +95,6 @@ This document describes the environment variables MBKAuth expects and keeps brie
95
95
  - If `GOOGLE_LOGIN_ENABLED=true`, `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` are required.
96
96
  - If `GITHUB_LOGIN_ENABLED=true`, GitHub App client credentials are required.
97
97
 
98
- - GITHUB_APP_SLUG
99
- - Description: GitHub App slug (optional for login flow in this package; useful for install/link flows handled elsewhere).
100
- - Required: No
101
- - Create GitHub App: https://github.com/settings/apps
102
-
103
98
  - GITHUB_APP_CLIENT_ID / GITHUB_APP_CLIENT_SECRET
104
99
  - Description: GitHub App OAuth credentials used for user sign-in.
105
100
  - Required when `GITHUB_LOGIN_ENABLED=true`.
@@ -16,7 +16,7 @@ Postgres enum `role`: `SuperAdmin`, `NormalUser`, `Guest`, `member`. The script
16
16
 
17
17
  Core accounts table (`"Users"`): username, activation flag, role, mail flag, `AllowedApps` and `Positions` as JSONB, timestamps, optional `last_login`, and password hash column `PasswordEnc` (no plaintext passwords).
18
18
 
19
- Profile-style columns include `FullName`, `email`, `Image`, `Bio`, `SocialAccounts`, and password-reset fields (`resetToken`, `resetTokenExpires`, `resetAttempts`, `lastResetAttempt`).
19
+ Profile-style columns include `FullName`, `UserId` (exactly 9 characters, unique business identifier assigned externally), `email`, `Image`, `Bio`, `SocialAccounts`, and password-reset fields (`resetToken`, `resetTokenExpires`, `resetAttempts`, `lastResetAttempt`).
20
20
 
21
21
  Indexes cover username, role, active, email, last login, and GIN indexes on JSONB for `AllowedApps` and `Positions`. The SQL file also adds optional covering indexes used on hot auth paths.
22
22
 
@@ -17,13 +17,16 @@ CREATE TABLE IF NOT EXISTS "Users" (
17
17
  last_login timestamp with time zone,
18
18
  "PasswordEnc" character varying(255),
19
19
  "FullName" character varying(100),
20
+ "UserId" character varying(9),
20
21
  email text DEFAULT 'support@mbktech.org'::text,
21
22
  "Image" text DEFAULT 'https://portal.mbktech.org/icon.svg'::text,
22
23
  "Bio" text DEFAULT 'I am ....'::text,
23
24
  "SocialAccounts" text DEFAULT '{}'::text,
24
25
  "Positions" jsonb DEFAULT '{"Not_Permanent": "Member Is Not Permanent"}'::jsonb,
25
26
  CONSTRAINT "Users_pkey" PRIMARY KEY (id),
26
- CONSTRAINT "Users_UserName_key" UNIQUE ("UserName")
27
+ CONSTRAINT "Users_UserName_key" UNIQUE ("UserName"),
28
+ CONSTRAINT "Users_UserId_key" UNIQUE ("UserId"),
29
+ CONSTRAINT chk_users_userid_length CHECK ("UserId" IS NULL OR length("UserId") = 9)
27
30
  );
28
31
  CREATE INDEX IF NOT EXISTS idx_users_active ON "Users" USING btree ("Active");
29
32
  CREATE INDEX IF NOT EXISTS idx_users_allowedapps_gin ON "Users" USING gin ("AllowedApps");
@@ -32,6 +35,7 @@ CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" USING btree (last_log
32
35
  CREATE INDEX IF NOT EXISTS idx_users_positions_gin ON "Users" USING gin ("Positions");
33
36
  CREATE INDEX IF NOT EXISTS idx_users_role ON "Users" USING btree ("Role");
34
37
  CREATE INDEX IF NOT EXISTS idx_users_username_cover ON "Users" USING btree ("UserName") INCLUDE ("Active", "Role");
38
+ CREATE INDEX IF NOT EXISTS idx_users_userid ON "Users" USING btree ("UserId");
35
39
  COMMENT ON TABLE "Users" IS '[core]';
36
40
 
37
41
  -- Table: ApiTokens
@@ -326,3 +330,20 @@ COMMENT ON TABLE "website_access_logs" IS '[core]';
326
330
  INSERT INTO "Users" ("UserName", "PasswordEnc", "Role", "Active", "HaveMailAccount", "FullName")
327
331
  VALUES ('support', 'b8b10c1c9006d8c30ab81c412463c65ff6dae3293d9bfbaf5fd8e275081d0947f000a828004e2fbd3a8f6ef5a35ae3eddd4c57b00ecab376b12e607a16a57459', 'SuperAdmin', true, false, 'Support User')
328
332
  ON CONFLICT ("UserName") DO NOTHING;
333
+
334
+ -- Migration: add UserId to existing Users tables (idempotent)
335
+ ALTER TABLE "Users" ADD COLUMN IF NOT EXISTS "UserId" character varying(9);
336
+
337
+ DO $$
338
+ BEGIN
339
+ IF NOT EXISTS (
340
+ SELECT 1 FROM pg_constraint WHERE conname = 'Users_UserId_key'
341
+ ) THEN
342
+ ALTER TABLE "Users" ADD CONSTRAINT "Users_UserId_key" UNIQUE ("UserId");
343
+ END IF;
344
+ END $$;
345
+
346
+ ALTER TABLE "Users" DROP CONSTRAINT IF EXISTS chk_users_userid_length;
347
+ ALTER TABLE "Users" ADD CONSTRAINT chk_users_userid_length CHECK ("UserId" IS NULL OR length("UserId") = 9);
348
+
349
+ CREATE INDEX IF NOT EXISTS idx_users_userid ON "Users" USING btree ("UserId");
package/index.d.ts CHANGED
@@ -19,7 +19,7 @@ declare global {
19
19
 
20
20
  interface Session {
21
21
  user?: {
22
- id: number;
22
+ userId?: string;
23
23
  username: string;
24
24
  fullname?: string;
25
25
  role: 'SuperAdmin' | 'NormalUser' | 'Guest';
@@ -28,7 +28,7 @@ declare global {
28
28
  tokenScope?: 'read-only' | 'write' | null;
29
29
  };
30
30
  preAuthUser?: {
31
- id: number;
31
+ userId?: string;
32
32
  username: string;
33
33
  role: 'SuperAdmin' | 'NormalUser' | 'Guest';
34
34
  loginMethod?: 'password' | 'github' | 'google';
@@ -55,7 +55,6 @@ declare module 'mbkauthe' {
55
55
  COOKIE_EXPIRE_TIME?: number;
56
56
  DEVICE_TRUST_DURATION_DAYS?: number;
57
57
  GITHUB_LOGIN_ENABLED?: 'true' | 'false' | 'f';
58
- GITHUB_APP_SLUG?: string;
59
58
  GITHUB_APP_CLIENT_ID?: string;
60
59
  GITHUB_APP_CLIENT_SECRET?: string;
61
60
  GOOGLE_LOGIN_ENABLED?: 'true' | 'false' | 'f';
@@ -67,7 +66,6 @@ declare module 'mbkauthe' {
67
66
 
68
67
  export interface OAuthConfig {
69
68
  GITHUB_LOGIN_ENABLED?: 'true' | 'false' | 'f';
70
- GITHUB_APP_SLUG?: string;
71
69
  GITHUB_APP_CLIENT_ID?: string;
72
70
  GITHUB_APP_CLIENT_SECRET?: string;
73
71
  GOOGLE_LOGIN_ENABLED?: 'true' | 'false' | 'f';
@@ -79,7 +77,7 @@ declare module 'mbkauthe' {
79
77
  export type UserRole = 'SuperAdmin' | 'NormalUser' | 'Guest';
80
78
 
81
79
  export interface SessionUser {
82
- id: number;
80
+ userId?: string;
83
81
  username: string;
84
82
  fullname?: string;
85
83
  role: UserRole;
@@ -88,7 +86,7 @@ declare module 'mbkauthe' {
88
86
  }
89
87
 
90
88
  export interface PreAuthUser {
91
- id: number;
89
+ userId?: string;
92
90
  username: string;
93
91
  role: UserRole;
94
92
  allowedApps?: string[];
@@ -100,6 +98,7 @@ declare module 'mbkauthe' {
100
98
  export interface DBUser {
101
99
  id: number;
102
100
  UserName: string;
101
+ UserId?: string;
103
102
  PasswordEnc?: string;
104
103
  Role: UserRole;
105
104
  Active: boolean;
@@ -300,6 +299,18 @@ declare module 'mbkauthe' {
300
299
  httpOnly: boolean;
301
300
  };
302
301
 
302
+ export function resolveCookieDomain(
303
+ isDeployed: 'true' | 'false' | 'f' | string,
304
+ domain?: string,
305
+ isTestDev?: boolean
306
+ ): string | undefined;
307
+
308
+ export function getCookieDomain(): string | undefined;
309
+
310
+ export function getCookieSecure(): boolean;
311
+
312
+ export function isAllowedOriginHostname(hostname: string, domain?: string): boolean;
313
+
303
314
  export function getClearCookieOptions(): {
304
315
  domain?: string;
305
316
  secure: boolean;
@@ -321,6 +332,8 @@ declare module 'mbkauthe' {
321
332
 
322
333
  export function hashPassword(password: string, username: string): string;
323
334
 
335
+ export function verifyPassword(password: string, username: string, storedHash: string): Promise<boolean>;
336
+
324
337
  export function clearSessionCookies(res: Response): void;
325
338
 
326
339
  export function getLatestVersion(): Promise<string>;
@@ -6,7 +6,7 @@ const MAX_REMEMBERED_ACCOUNTS = 5;
6
6
  const ACCOUNT_LIST_COOKIE = 'mbkauthe_accounts';
7
7
 
8
8
  // Cookie security: encryption and signing
9
- const COOKIE_ENCRYPTION_KEY = mbkautheVar.SESSION_SECRET_KEY || 'fallback-secret-key-change-this';
9
+ const COOKIE_ENCRYPTION_KEY = mbkautheVar.SESSION_SECRET_KEY;
10
10
  const ENCRYPTION_ALGORITHM = 'aes-256-gcm';
11
11
 
12
12
  // Derive encryption key from session secret
@@ -121,8 +121,7 @@ const decryptCookiePayload = (payload) => {
121
121
  // Generate fingerprint from user-agent only (salted)
122
122
  const generateFingerprint = (req) => {
123
123
  const userAgent = req.headers['user-agent'] || '';
124
- // Use SESSION_SECRET_KEY as salt if available, otherwise fallback to encryption key
125
- const salt = mbkautheVar.SESSION_SECRET_KEY || COOKIE_ENCRYPTION_KEY;
124
+ const salt = mbkautheVar.SESSION_SECRET_KEY;
126
125
 
127
126
  // Hash user-agent with salt to prevent rainbow table attacks on UAs
128
127
  return crypto
@@ -152,19 +151,41 @@ export const decryptSessionId = (encryptedSessionId) => {
152
151
  }
153
152
  };
154
153
 
154
+ const isTestDevEnvironment = () => process.env.test === 'dev';
155
+
156
+ export const resolveCookieDomain = (isDeployed, domain, isTestDev = isTestDevEnvironment()) => {
157
+ if (isDeployed !== 'true' || isTestDev || !domain) {
158
+ return undefined;
159
+ }
160
+
161
+ return `.${String(domain).replace(/^\.+/, '')}`;
162
+ };
163
+
164
+ export const getCookieDomain = () => resolveCookieDomain(mbkautheVar.IS_DEPLOYED, mbkautheVar.DOMAIN);
165
+
166
+ export const getCookieSecure = () => mbkautheVar.IS_DEPLOYED === 'true' && !isTestDevEnvironment();
167
+
168
+ export const isAllowedOriginHostname = (hostname, domain = mbkautheVar.DOMAIN) => {
169
+ if (!hostname || !domain) {
170
+ return false;
171
+ }
172
+
173
+ return hostname === domain || hostname.endsWith(`.${domain}`);
174
+ };
175
+
155
176
  // Shared cookie options functions
156
177
  const getCookieOptions = () => ({
157
178
  maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
158
- domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
159
- secure: mbkautheVar.IS_DEPLOYED === 'true',
179
+ domain: getCookieDomain(),
180
+ secure: getCookieSecure(),
160
181
  sameSite: 'lax',
161
182
  path: '/',
162
183
  httpOnly: true
163
184
  });
164
185
 
165
186
  const getClearCookieOptions = () => ({
166
- domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
167
- secure: mbkautheVar.IS_DEPLOYED === 'true',
187
+ domain: getCookieDomain(),
188
+ secure: getCookieSecure(),
168
189
  sameSite: 'lax',
169
190
  path: '/',
170
191
  httpOnly: true
@@ -183,16 +204,20 @@ export const generateDeviceToken = () => {
183
204
  return crypto.randomBytes(32).toString('hex');
184
205
  };
185
206
 
207
+ const getDeviceTokenKey = () => {
208
+ return crypto.createHash('sha256').update(`${mbkautheVar.SESSION_SECRET_KEY}:device-token`).digest();
209
+ };
210
+
186
211
  // Hash a device token for safe storage in the database
187
212
  export const hashDeviceToken = (token) => {
188
213
  if (!token || typeof token !== 'string') return null;
189
- return crypto.createHmac('sha256').update(token).digest('hex');
214
+ return crypto.createHmac('sha256', getDeviceTokenKey()).update(token).digest('hex');
190
215
  };
191
216
 
192
217
  export const getDeviceTokenCookieOptions = () => ({
193
218
  maxAge: DEVICE_TRUST_DURATION_MS,
194
- domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
195
- secure: mbkautheVar.IS_DEPLOYED === 'true',
219
+ domain: getCookieDomain(),
220
+ secure: getCookieSecure(),
196
221
  sameSite: 'lax',
197
222
  path: '/',
198
223
  httpOnly: true