mbkauthe 4.1.2 → 4.1.4

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/docs/api.md CHANGED
@@ -25,7 +25,73 @@ This document provides comprehensive API documentation for MBKAuthe authenticati
25
25
  MBKAuthe supports two authentication methods:
26
26
 
27
27
  1. **Session-based Authentication** - Cookie-based sessions for web applications
28
- 2. **Token-based Authentication** - API key authentication for server-to-server communication
28
+ 2. **Token-based Authentication** - Persistent API keys for server-to-server communication
29
+
30
+ ### Token-based Authentication
31
+
32
+ For API clients and external services, use a Bearer token in the `Authorization` header.
33
+
34
+ **Header Format:**
35
+ ```
36
+ Authorization: Bearer <your_api_token>
37
+ ```
38
+ *Token format: `mbk_` followed by 64 hexadecimal characters.*
39
+
40
+ **Behavior:**
41
+ - **Stateless:** Validates against the `ApiTokens` table on every request.
42
+ - **Expiration:** Tokens can have an optional expiration date.
43
+ - **Permissions:** API tokens inherit the permissions of the user who created them.
44
+ - **Usage Tracking:** The system updates the `LastUsed` timestamp on every successful request.
45
+
46
+ **Errors:**
47
+ - `401 Unauthorized` (Code 1005: `INVALID_AUTH_TOKEN`): Token is malformed or not found.
48
+ - `401 Unauthorized` (Code 1006: `API_TOKEN_EXPIRED`): Token exists but has passed its expiration date.
49
+
50
+ **Example Usage:**
51
+
52
+ **1. Backend Implementation (Express):**
53
+
54
+ Even when using API tokens, the `validateSession` middleware hydrates `req.session.user` for consistency, allowing you to use the same route logic for both browser and API clients.
55
+
56
+ ```javascript
57
+ import { validateSession } from 'mbkauthe';
58
+
59
+ app.get('/api/protected-resource', validateSession, (req, res) => {
60
+ // Access user info populated from the token
61
+ const user = req.session.user; // { id, username, role, ... }
62
+
63
+ res.json({
64
+ message: `Hello ${user.username}`,
65
+ role: user.role
66
+ });
67
+ });
68
+ ```
69
+
70
+ **2. Client Request Examples:**
71
+
72
+ *cURL:*
73
+ ```bash
74
+ curl -X GET https://api.yourdomain.com/api/protected-resource \
75
+ -H "Authorization: Bearer mbk_7f83a92b1dc..."
76
+ ```
77
+
78
+ *JavaScript (Fetch):*
79
+ ```javascript
80
+ const response = await fetch('https://api.yourdomain.com/api/protected-resource', {
81
+ headers: {
82
+ 'Authorization': 'Bearer mbk_7f83a92b1dc...'
83
+ }
84
+ });
85
+ const data = await response.json();
86
+ ```
87
+
88
+ **Output:**
89
+ ```json
90
+ {
91
+ "message": "Hello john.doe",
92
+ "role": "NormalUser"
93
+ }
94
+ ```
29
95
 
30
96
  ---
31
97
 
@@ -38,7 +104,7 @@ When a user logs in, MBKAuthe creates a session and sets the following cookies:
38
104
  | Cookie Name | Description | HttpOnly | Secure | SameSite |
39
105
  |------------|-------------|----------|--------|----------|
40
106
  | `mbkauthe.sid` | Session identifier | ✓ | Auto* | lax |
41
- | `sessionId` | Opaque session token (backed by `Sessions` table) | ✓ | Auto* | lax |
107
+ | `sessionId` | Encrypted session token (AES-256-GCM) | ✓ | Auto* | lax |
42
108
  | `username` | Username | ✗ | Auto* | lax |
43
109
 
44
110
  \* `secure` flag is automatically set to `true` in production when `IS_DEPLOYED=true`
package/docs/db.md CHANGED
@@ -68,6 +68,25 @@ CREATE INDEX IF NOT EXISTS idx_user_google_google_id ON user_google (google_id);
68
68
  CREATE INDEX IF NOT EXISTS idx_user_google_user_name ON user_google (user_name);
69
69
  ```
70
70
 
71
+ ### 5. API Tokens Table
72
+ Used for storing persistent API keys.
73
+
74
+ ```sql
75
+ CREATE TABLE "ApiTokens" (
76
+ "id" SERIAL PRIMARY KEY,
77
+ "UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
78
+ "Name" VARCHAR(255) NOT NULL, -- User-provided friendly name
79
+ "TokenHash" VARCHAR(128) NOT NULL UNIQUE, -- Hashed access token (SHA-256)
80
+ "Prefix" VARCHAR(32) NOT NULL, -- First few chars of token for ID
81
+ "LastUsed" TIMESTAMP WITH TIME ZONE,
82
+ "CreatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
83
+ "ExpiresAt" TIMESTAMP WITH TIME ZONE -- Optional expiration
84
+ );
85
+
86
+ CREATE INDEX IF NOT EXISTS idx_apitokens_tokenhash ON "ApiTokens" ("TokenHash");
87
+ CREATE INDEX IF NOT EXISTS idx_apitokens_username ON "ApiTokens" ("UserName");
88
+ ```
89
+
71
90
  ## How It Works
72
91
 
73
92
  ### Login Flow (GitHub/Google)
package/docs/db.sql CHANGED
@@ -130,3 +130,20 @@ CREATE INDEX IF NOT EXISTS idx_trusted_devices_expires ON "TrustedDevices"("Expi
130
130
  -- No Encrypted password for 'support' user
131
131
  INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "GuestRole")
132
132
  VALUES ('support', '12345678', 'SuperAdmin', true, false, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
133
+
134
+
135
+
136
+ -- API Tokens for persistent programmatic access
137
+ CREATE TABLE "ApiTokens" (
138
+ "id" SERIAL PRIMARY KEY,
139
+ "UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
140
+ "Name" VARCHAR(255) NOT NULL, -- User-provided friendly name
141
+ "TokenHash" VARCHAR(128) NOT NULL UNIQUE, -- Hashed access token
142
+ "Prefix" VARCHAR(32) NOT NULL, -- First few chars of token for ID
143
+ "LastUsed" TIMESTAMP WITH TIME ZONE,
144
+ "CreatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
145
+ "ExpiresAt" TIMESTAMP WITH TIME ZONE -- Optional expiration
146
+ );
147
+
148
+ CREATE INDEX IF NOT EXISTS idx_apitokens_tokenhash ON "ApiTokens" ("TokenHash");
149
+ CREATE INDEX IF NOT EXISTS idx_apitokens_username ON "ApiTokens" ("UserName");
@@ -168,6 +168,12 @@ Password doesn't meet length requirements.
168
168
  #### `1004 - INVALID_TOKEN_FORMAT`
169
169
  Token format is incorrect.
170
170
 
171
+ #### `1005 - INVALID_AUTH_TOKEN`
172
+ The provided API token is invalid.
173
+
174
+ #### `1006 - API_TOKEN_EXPIRED`
175
+ The provided API token has expired.
176
+
171
177
  ### Rate Limiting (1100-1199)
172
178
 
173
179
  #### `1101 - RATE_LIMIT_EXCEEDED`
package/index.js CHANGED
@@ -99,4 +99,5 @@ export {
99
99
  ErrorCodes, ErrorMessages, getErrorByCode,
100
100
  createErrorResponse, logError
101
101
  } from "./lib/utils/errors.js";
102
+ export { mbkautheVar } from "./lib/config/index.js";
102
103
  export default router;
@@ -17,7 +17,7 @@ const getEncryptionKey = () => {
17
17
  // Encrypt and sign cookie payload
18
18
  const encryptCookiePayload = (data) => {
19
19
  try {
20
- const iv = crypto.randomBytes(16);
20
+ const iv = crypto.randomBytes(12);
21
21
  const key = getEncryptionKey();
22
22
  const cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
23
23
 
@@ -78,6 +78,26 @@ const generateFingerprint = (req) => {
78
78
  .substring(0, 32);
79
79
  };
80
80
 
81
+ // Encrypt sessionId for cookie storage
82
+ export const encryptSessionId = (sessionId) => {
83
+ if (!sessionId) return null;
84
+ const encrypted = encryptCookiePayload({ sessionId });
85
+ return encrypted ? JSON.stringify(encrypted) : null;
86
+ };
87
+
88
+ // Decrypt sessionId from cookie
89
+ export const decryptSessionId = (encryptedSessionId) => {
90
+ if (!encryptedSessionId) return null;
91
+ try {
92
+ const parsed = JSON.parse(encryptedSessionId);
93
+ const decrypted = decryptCookiePayload(parsed);
94
+ return decrypted?.sessionId || null;
95
+ } catch (error) {
96
+ console.error('[mbkauthe] SessionId decryption error:', error);
97
+ return null;
98
+ }
99
+ };
100
+
81
101
  // Shared cookie options functions
82
102
  const getCookieOptions = () => ({
83
103
  maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
@@ -36,8 +36,6 @@ function validateConfiguration() {
36
36
  if (mbkauthShared && typeof mbkauthShared !== 'object') {
37
37
  console.warn('[mbkauthe] mbkauthShared is not a valid object, ignoring it');
38
38
  mbkauthShared = null;
39
- } else {
40
- console.log('[mbkauthe] mbkauthShared detected and parsed successfully');
41
39
  }
42
40
  }
43
41
  } catch (error) {
@@ -46,6 +44,8 @@ function validateConfiguration() {
46
44
  }
47
45
 
48
46
  // Merge fallback settings: for any key missing or empty in mbkautheVar, check mbkauthShared
47
+ const usedFromShared = [];
48
+ const usedDefaults = [];
49
49
  const applyFallback = (source, sourceName) => {
50
50
  if (!source) return;
51
51
  Object.keys(source).forEach(key => {
@@ -53,7 +53,7 @@ function validateConfiguration() {
53
53
  if ((mbkautheVar[key] === undefined || (typeof mbkautheVar[key] === 'string' && mbkautheVar[key].trim() === '')) &&
54
54
  val !== undefined && !(typeof val === 'string' && val.trim() === '')) {
55
55
  mbkautheVar[key] = val;
56
- console.log(`[mbkauthe] Using ${key} from ${sourceName}`);
56
+ if (sourceName === 'mbkauthShared') usedFromShared.push(key);
57
57
  }
58
58
  });
59
59
  };
@@ -86,10 +86,10 @@ function validateConfiguration() {
86
86
  if (isEmpty) {
87
87
  if (mbkauthShared && mbkauthShared[key] !== undefined && !(typeof mbkauthShared[key] === 'string' && mbkauthShared[key].trim() === '')) {
88
88
  mbkautheVar[key] = mbkauthShared[key];
89
- console.log(`[mbkauthe] Using ${key} from mbkauthShared`);
89
+ if (!usedFromShared.includes(key)) usedFromShared.push(key);
90
90
  } else if (defaults[key] !== undefined) {
91
91
  mbkautheVar[key] = defaults[key];
92
- console.log(`[mbkauthe] Using default value for ${key}`);
92
+ usedDefaults.push(key);
93
93
  }
94
94
  }
95
95
  });
@@ -209,7 +209,16 @@ function validateConfiguration() {
209
209
  throw new Error(`[mbkauthe] Configuration Validation Failed:\n - ${errors.join('\n - ')}`);
210
210
  }
211
211
 
212
- console.log('[mbkauthe] Configuration validation passed successfully');
212
+ // Print consolidated configuration summary
213
+ const configParts = [];
214
+ if (mbkauthShared) {
215
+ configParts.push(`mbkauthShared: ${usedFromShared.length} keys`);
216
+ }
217
+ if (usedDefaults.length > 0) {
218
+ configParts.push(`defaults: ${usedDefaults.length} keys`);
219
+ }
220
+ const configSummary = configParts.length > 0 ? ` (${configParts.join(', ')})` : '';
221
+ console.log(`[mbkauthe] Configuration loaded${configSummary}`);
213
222
  return mbkautheVar;
214
223
  }
215
224
 
@@ -5,4 +5,11 @@ export const hashPassword = (password, username) => {
5
5
  const salt = username;
6
6
  // 128 characters returned
7
7
  return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
8
+ };
9
+
10
+ // Hash an API token for storage
11
+ // Uses SHA-256 for fast, secure non-reversible hashing
12
+ export const hashApiToken = (token) => {
13
+ if (!token) return null;
14
+ return crypto.createHash('sha256').update(token).digest('hex');
8
15
  };
@@ -2,8 +2,103 @@ import { dblogin } from "../database/pool.js";
2
2
  import { mbkautheVar } from "../config/index.js";
3
3
  import { renderError } from "../utils/response.js";
4
4
  import { clearSessionCookies, cachedCookieOptions, readAccountListFromCookie } from "../config/cookies.js";
5
+ import { ErrorCodes, createErrorResponse } from "../utils/errors.js";
6
+ import { hashApiToken } from "../config/security.js";
7
+
8
+ /**
9
+ * Validates a Bearer token (API Token or Session UUID)
10
+ * Returns a user object if valid, or null/error object
11
+ */
12
+ async function validateTokenAuthentication(req) {
13
+ const authHeader = req.headers.authorization;
14
+ if (!authHeader) return null;
15
+
16
+ const parts = authHeader.split(' ');
17
+ if (parts.length !== 2 || parts[0] !== 'Bearer') return null;
18
+ const token = parts[1];
19
+
20
+ // 1. Check for API Token (mbk_)
21
+ if (token.startsWith('mbk_')) {
22
+ const tokenHash = hashApiToken(token);
23
+ const tokenQuery = `
24
+ SELECT t.id, t."UserName", t."ExpiresAt", u.id as uid, u."Active", u."Role", u."AllowedApps", u."FullName"
25
+ FROM "ApiTokens" t
26
+ JOIN "Users" u ON t."UserName" = u."UserName"
27
+ WHERE t."TokenHash" = $1 LIMIT 1
28
+ `;
29
+ const tokenResult = await dblogin.query({ name: 'validate-api-token', text: tokenQuery, values: [tokenHash] });
30
+
31
+ if (tokenResult.rows.length === 0) return { error: 'INVALID_TOKEN' };
32
+
33
+ const row = tokenResult.rows[0];
34
+ if (row.ExpiresAt && new Date(row.ExpiresAt) <= new Date()) return { error: 'TOKEN_EXPIRED' };
35
+
36
+ // Update usage
37
+ dblogin.query({
38
+ text: 'UPDATE "ApiTokens" SET "LastUsed" = NOW() WHERE id = $1',
39
+ values: [row.id]
40
+ }).catch(e => console.error('[mbkauthe] Failed to update token usage:', e));
41
+
42
+ return {
43
+ id: row.uid,
44
+ username: row.UserName,
45
+ fullname: row.FullName,
46
+ role: row.Role,
47
+ sessionId: 'api-token-session',
48
+ allowedApps: row.AllowedApps,
49
+ active: row.Active
50
+ };
51
+ }
52
+
53
+ return null;
54
+ }
5
55
 
6
56
  async function validateSession(req, res, next) {
57
+ // --- Check for API Token Header first ---
58
+ if (req.headers.authorization) {
59
+ try {
60
+ const tokenUser = await validateTokenAuthentication(req);
61
+
62
+ if (tokenUser && !tokenUser.error) {
63
+ if (!tokenUser.active) {
64
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
65
+ }
66
+
67
+ if (tokenUser.role !== "SuperAdmin") {
68
+ const allowedApps = tokenUser.allowedApps;
69
+ const hasAllowedApps = Array.isArray(allowedApps) && allowedApps.length > 0;
70
+ if (!hasAllowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
71
+ return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
72
+ }
73
+ }
74
+
75
+ // Populate session for downstream
76
+ req.session.user = {
77
+ id: tokenUser.id,
78
+ username: tokenUser.username,
79
+ fullname: tokenUser.fullname,
80
+ role: tokenUser.role,
81
+ sessionId: tokenUser.sessionId,
82
+ allowedApps: tokenUser.allowedApps,
83
+ };
84
+ req.userRole = tokenUser.role;
85
+ return next();
86
+ }
87
+
88
+ // Token provided but invalid (or null if format incorrect)
89
+ let errorCode = ErrorCodes.INVALID_AUTH_TOKEN;
90
+ if (tokenUser && tokenUser.error === 'TOKEN_EXPIRED') {
91
+ errorCode = ErrorCodes.API_TOKEN_EXPIRED;
92
+ }
93
+ return res.status(401).json(createErrorResponse(401, errorCode));
94
+
95
+ } catch (err) {
96
+ console.error("[mbkauthe] Token validation error:", err);
97
+ return res.status(500).json(createErrorResponse(500, ErrorCodes.INTERNAL_SERVER_ERROR));
98
+ }
99
+ }
100
+
101
+ // --- Fallback to Cookie Session ---
7
102
  if (!req.session.user) {
8
103
  console.log("[mbkauthe] User not authenticated");
9
104
  console.log("[mbkauthe]: ", req.session.user);
@@ -258,7 +353,7 @@ export async function reloadSessionUser(req, res) {
258
353
  req.session.user.fullname = req.cookies.fullName;
259
354
  } else {
260
355
  try {
261
- const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "USers" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
356
+ const prof = await dblogin.query({ name: 'reload-get-fullname', text: 'SELECT "FullName" FROM "Users" WHERE "UserName" = $1 LIMIT 1', values: [row.UserName] });
262
357
  if (prof.rows.length > 0 && prof.rows[0].FullName) req.session.user.fullname = prof.rows[0].FullName;
263
358
  } catch (profileErr) {
264
359
  console.error('[mbkauthe] Error fetching fullname during reload:', profileErr);
@@ -270,7 +365,7 @@ export async function reloadSessionUser(req, res) {
270
365
 
271
366
  // Sync cookies for client UI (non-httpOnly)
272
367
  try {
273
- res.cookie('username', req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
368
+ res.cookie('username', req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
274
369
  res.cookie('fullName', req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
275
370
  res.cookie('sessionId', req.session.user.sessionId, cachedCookieOptions);
276
371
  } catch (cookieErr) {
@@ -3,7 +3,7 @@ import pgSession from "connect-pg-simple";
3
3
  const PgSession = pgSession(session);
4
4
  import { dblogin } from "../database/pool.js";
5
5
  import { mbkautheVar } from "../config/index.js";
6
- import { cachedCookieOptions } from "../config/cookies.js";
6
+ import { cachedCookieOptions, decryptSessionId, encryptSessionId } from "../config/cookies.js";
7
7
 
8
8
  // Session configuration
9
9
  export const sessionConfig = {
@@ -55,10 +55,11 @@ export function corsMiddleware(req, res, next) {
55
55
  export async function sessionRestorationMiddleware(req, res, next) {
56
56
  // Only restore session if not already present and sessionId cookie exists
57
57
  if (!req.session.user && req.cookies.sessionId) {
58
- const sessionId = req.cookies.sessionId;
58
+ // Decrypt the sessionId from cookie
59
+ const sessionId = decryptSessionId(req.cookies.sessionId);
59
60
 
60
61
  // Early validation to avoid unnecessary processing (expect DB UUID id)
61
- if (typeof sessionId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
62
+ if (!sessionId || typeof sessionId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
62
63
  // Clear invalid cookie to prevent repeated attempts
63
64
  res.clearCookie('sessionId', {
64
65
  domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
@@ -124,12 +125,18 @@ export async function sessionRestorationMiddleware(req, res, next) {
124
125
  // Session cookie sync middleware
125
126
  export function sessionCookieSyncMiddleware(req, res, next) {
126
127
  if (req.session && req.session.user) {
128
+ // Decrypt existing cookie to compare with session
129
+ const currentDecryptedId = decryptSessionId(req.cookies.sessionId);
130
+
127
131
  // Only set cookies if they're missing or different
128
- if (req.cookies.sessionId !== req.session.user.sessionId) {
132
+ if (currentDecryptedId !== req.session.user.sessionId) {
129
133
  res.cookie("username", req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
130
- // Also expose FullName (fallback to username) for display in client-side UI
131
134
  res.cookie("fullName", req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
132
- res.cookie("sessionId", req.session.user.sessionId, cachedCookieOptions);
135
+
136
+ const encryptedSessionId = encryptSessionId(req.session.user.sessionId);
137
+ if (encryptedSessionId) {
138
+ res.cookie("sessionId", encryptedSessionId, cachedCookieOptions);
139
+ }
133
140
  }
134
141
  }
135
142
  next();
@@ -8,10 +8,11 @@ import { mbkautheVar } from "../config/index.js";
8
8
  import {
9
9
  cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies,
10
10
  generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS, hashDeviceToken,
11
- upsertAccountListCookie, readAccountListFromCookie, removeAccountFromCookie, clearAccountListCookie
11
+ upsertAccountListCookie, readAccountListFromCookie, removeAccountFromCookie, clearAccountListCookie,
12
+ encryptSessionId
12
13
  } from "../config/cookies.js";
13
14
  import { packageJson } from "../config/index.js";
14
- import { hashPassword } from "../config/security.js";
15
+ import { hashPassword, hashApiToken } from "../config/security.js";
15
16
  import { ErrorCodes, createErrorResponse, logError } from "../utils/errors.js";
16
17
 
17
18
  const router = express.Router();
@@ -218,13 +219,20 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
218
219
 
219
220
  const expiresAt = new Date(Date.now() + (cachedCookieOptions.maxAge || 0));
220
221
 
221
- // Insert new session record for the user (store username) and return the DB id
222
- const insertRes = await dblogin.query({
223
- name: 'insert-app-session',
224
- text: `INSERT INTO "Sessions" ("UserName", expires_at, meta) VALUES ($1, $2, $3) RETURNING id`,
225
- values: [username, expiresAt, JSON.stringify({ ip: req.ip, ua: req.headers['user-agent'] || null })]
226
- });
227
- const dbSessionId = insertRes.rows[0].id;
222
+ // Insert new session record for the user (generate id explicitly to avoid relying on DB default)
223
+ const newSessionId = crypto.randomUUID();
224
+ let dbSessionId;
225
+ try {
226
+ const insertRes = await dblogin.query({
227
+ name: 'insert-app-session',
228
+ text: `INSERT INTO "Sessions" ("UserName", expires_at, meta) VALUES ($1, $2, $3) RETURNING id`,
229
+ values: [username, expiresAt, JSON.stringify({ ip: req.ip, ua: req.headers['user-agent'] || null })]
230
+ });
231
+ dbSessionId = insertRes.rows[0].id;
232
+ } catch (insertErr) {
233
+ console.error('[mbkauthe] Error inserting app session:', insertErr);
234
+ throw insertErr;
235
+ }
228
236
 
229
237
  // Update last_login timestamp for the user
230
238
  await dblogin.query({
@@ -269,7 +277,11 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
269
277
  }
270
278
 
271
279
  // Expose DB session id and display name to client for UI (fullName falls back to username)
272
- res.cookie("sessionId", dbSessionId, cachedCookieOptions);
280
+ const encryptedSessionId = encryptSessionId(dbSessionId);
281
+ if (encryptedSessionId) {
282
+ res.cookie("sessionId", encryptedSessionId, cachedCookieOptions);
283
+ }
284
+ res.cookie("username", username, { ...cachedCookieOptions, httpOnly: false });
273
285
  res.cookie("fullName", req.session.user.fullname || username, { ...cachedCookieOptions, httpOnly: false });
274
286
 
275
287
  // Remember this account on the device for quick switching (server-trusted list)
@@ -693,7 +705,8 @@ router.post("/api/switch-session", LoginLimit, async (req, res) => {
693
705
  }
694
706
 
695
707
  const storedAccounts = readAccountListFromCookie(req);
696
- if (!storedAccounts.some(acct => acct.sessionId === sessionId)) {
708
+ const acct = storedAccounts.find(a => a.sessionId === sessionId);
709
+ if (!acct) {
697
710
  return res.status(403).json(createErrorResponse(403, ErrorCodes.SESSION_NOT_FOUND, { message: 'Account not available on this device' }));
698
711
  }
699
712
 
@@ -108,6 +108,9 @@ const createOAuthStrategy = async (provider, profile, done) => {
108
108
  }
109
109
  };
110
110
 
111
+ // Configure OAuth strategies and track enabled providers
112
+ const enabledProviders = [];
113
+
111
114
  // Configure GitHub Strategy for login (only if enabled and configured)
112
115
  if ((mbkautheVar.GITHUB_LOGIN_ENABLED || "").toLowerCase() === "true") {
113
116
  if (mbkautheVar.GITHUB_CLIENT_ID && mbkautheVar.GITHUB_CLIENT_SECRET) {
@@ -119,12 +122,10 @@ if ((mbkautheVar.GITHUB_LOGIN_ENABLED || "").toLowerCase() === "true") {
119
122
  }, (accessToken, refreshToken, profile, done) =>
120
123
  createOAuthStrategy('GitHub', profile, done)
121
124
  ));
122
- console.log('[mbkauthe] GitHub OAuth strategy registered');
125
+ enabledProviders.push('GitHub');
123
126
  } else {
124
127
  console.warn('[mbkauthe] GITHUB_LOGIN_ENABLED is true but GITHUB_CLIENT_ID/SECRET missing; skipping GitHub strategy registration');
125
128
  }
126
- } else {
127
- console.log('[mbkauthe] GitHub OAuth not enabled; skipping GitHub strategy registration');
128
129
  }
129
130
 
130
131
  // Configure Google Strategy for login (only if enabled and configured)
@@ -138,12 +139,15 @@ if ((mbkautheVar.GOOGLE_LOGIN_ENABLED || "").toLowerCase() === "true") {
138
139
  }, (accessToken, refreshToken, profile, done) =>
139
140
  createOAuthStrategy('Google', profile, done)
140
141
  ));
141
- console.log('[mbkauthe] Google OAuth strategy registered');
142
+ enabledProviders.push('Google');
142
143
  } else {
143
144
  console.warn('[mbkauthe] GOOGLE_LOGIN_ENABLED is true but GOOGLE_CLIENT_ID/SECRET missing; skipping Google strategy registration');
144
145
  }
145
- } else {
146
- console.log('[mbkauthe] Google OAuth not enabled; skipping Google strategy registration');
146
+ }
147
+
148
+ // Print consolidated OAuth summary
149
+ if (enabledProviders.length > 0) {
150
+ console.log(`[mbkauthe] OAuth providers: ${enabledProviders.join(', ')}`);
147
151
  }
148
152
 
149
153
  // Serialize/Deserialize user for OAuth login
@@ -32,6 +32,8 @@ export const ErrorCodes = {
32
32
  INVALID_USERNAME_FORMAT: 1002,
33
33
  INVALID_PASSWORD_LENGTH: 1003,
34
34
  INVALID_TOKEN_FORMAT: 1004,
35
+ INVALID_AUTH_TOKEN: 1005,
36
+ API_TOKEN_EXPIRED: 1006,
35
37
 
36
38
  // Rate limiting (1100-1199)
37
39
  RATE_LIMIT_EXCEEDED: 1101,
@@ -148,6 +150,16 @@ export const ErrorMessages = {
148
150
  userMessage: "Please enter a valid 6-digit code.",
149
151
  hint: "The code should be 6 numbers from your authenticator app"
150
152
  },
153
+ [ErrorCodes.INVALID_AUTH_TOKEN]: {
154
+ message: "Invalid API token",
155
+ userMessage: "The provided API token is invalid.",
156
+ hint: "Please check your token and try again"
157
+ },
158
+ [ErrorCodes.API_TOKEN_EXPIRED]: {
159
+ message: "API token expired",
160
+ userMessage: "The provided API token has expired.",
161
+ hint: "Please generate a new API token"
162
+ },
151
163
 
152
164
  // Rate Limiting
153
165
  [ErrorCodes.RATE_LIMIT_EXCEEDED]: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "4.1.2",
3
+ "version": "4.1.4",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- <div class="showMessageOverlay" id="messageOverlay" aria-hidden="true">
1
+ <div class="showMessageOverlay" id="messageOverlay" aria-hidden="true" hidden>
2
2
  <div class="messageModal" role="dialog" aria-modal="true" aria-labelledby="messageTitle"
3
3
  aria-describedby="messageContent">
4
4
  <div class="messageHeader">
@@ -115,6 +115,7 @@
115
115
  }
116
116
 
117
117
  // Show Modal
118
+ overlay.removeAttribute("hidden");
118
119
  overlay.classList.add("active");
119
120
  document.body.classList.add("blur-active");
120
121
  overlay.setAttribute("aria-hidden", "false");
@@ -131,6 +132,7 @@
131
132
  overlay.classList.remove("active", "fade-out");
132
133
  document.body.classList.remove("blur-active");
133
134
  overlay.setAttribute("aria-hidden", "true");
135
+ overlay.setAttribute("hidden", "");
134
136
  }, 300);
135
137
  }
136
138