Skip to content

Authentication

This service implements two coexisting auth schemes — one inherited from the someli-api lineage, one added specifically for admin use. Knowing which is which matters when you read a handler.

Scheme A — encrypted-token header (middlewares/auth.js)

This is the scheme used by the admin FE (admin_console_R) for all authenticated requests.

// middlewares/auth.js
const jwt = require("jsonwebtoken");
const revokedTokens = require('../helper/revokeToken');
const { decryptData } = require('../helper/tokenGenerator');

module.exports = (req, res, next) => {
    let tokenHeaderKey = process.env.TOKEN_HEADER_KEY;   // e.g. "Token"
    try {
        const token = req.header(tokenHeaderKey);
        const accountId = req.get('Accountid') ?? null;
        if (revokedTokens.has(token)) {
            return res.status(401).send("unauthorized access");
        }
        const verified = decryptData(token);
        if (verified) {
            res.userId = verified?.userId;
            res.role = verified?.role;
            res.defaultPaymentId = verified?.defaultPaymentId;
            res.accountId = accountId;
            next();
        } else {
            return res.status(401).send("unauthorized access");
        }
    } catch (error) {
        console.log("error", error);
        return res.status(401).send("unauthorized access");
    }
};

Notes:

  1. The token is an AES-encrypted JSON blob, not a JWT. decryptData() in helper/tokenGenerator.js (byte-identical to someli-api's copy) decrypts via crypto-js AES.
  2. The accountId comes from a separate header, not from the token. This means the client picks the active account context per request. A user who is a member of multiple accounts can switch accounts by changing the header without re-logging in. (Admin FE: admin_console_R/src/services/api.ts createAuthHeaders())
  3. res is mutated, not req. This is unusual. res.userId, res.role, res.accountId, res.defaultPaymentId are set; downstream handlers read them from res. This is the same pattern as someli-api and is inherited from the same skeleton.
  4. Token revocation is in-memory only. revokedTokens is a Set populated at logout. Process restart clears it. So a logged-out token becomes valid again on the next deploy. Real revocation needs DB-backed tracking. See security.md.
  5. jsonwebtoken is imported but not used by this middleware — vestigial.

Used by: - routes/auth.js:64router.use(auth) at top of file. Every endpoint in routes/auth.js requires it. - Some handlers in routes/routes.js apply it per-handler.

Scheme B — Bearer JWT (methods.jsensureToken)

// methods.js
const jwt = require('jsonwebtoken');
module.exports.ensureToken = function (req, res, next) {
    var bearerHeader = req.headers["authorization"];
    if (typeof bearerHeader !== 'undefined') {
        const bearer = bearerHeader.split(" ");
        const bearerToken = bearer[1];
        jwt.verify(bearerToken, conf.JWT_SECRET_KEY, (err, result) => {
            if (err) res.sendStatus(403);
            else next();
        });
    } else {
        res.sendStatus(403);
    }
};

This is a plain Authorization: Bearer <jwt> flow, verified against conf.JWT_SECRET_KEY.

Used by: - routes/routes.js:127router.get("/", methods.ensureToken, ...) (health check) - Possibly other handlers (search the file)

The JWT format and the encrypted-token format are different tokens issued by different endpoints:

Endpoint Issues For
POST /authenticate A Bearer JWT (Scheme B) Older / legacy / native integrations
POST /webauthenticate An encrypted-token (Scheme A) The admin FE (admin_console_R)

Both endpoints exist; the FE uses /webauthenticate exclusively (see admin_console_R/src/context/AuthContext.tsx).

Login flow (admin FE perspective)

  1. FE POST /webauthenticate with { email, password } in body + Apptype: <base64> header.
  2. BE verifies via tMember SQL, hashes the password with bcryptjs, returns the encrypted token { token, userId, role, accountId }.
  3. FE stores the token in localStorage as auth_token.
  4. Each subsequent request: headers.Token = <encrypted token>; headers.Accountid = <active account>.
  5. middlewares/auth.js decrypts the token, validates the in-memory revocation set, attaches user context to res, and calls next().

The token has no expiry encoded inside it (verify by reading decryptData and tracing what tokenGenerator.encryptData puts in the JSON). Sessions are therefore long-lived; logout only revokes the specific token.

Logout flow

  1. FE clears localStorage.auth_token and navigates to /login.
  2. (Optionally) FE calls a logout endpoint that adds the token to revokedTokens server-side.
  3. If the process restarts, the in-memory revokedTokens is empty again. This is a server-side correctness gap — see security.md.

Why this matters

Because two schemes coexist:

  • Audit risk: a code reviewer who knows only Scheme A might miss a Scheme-B endpoint that was added casually with ensureToken and a wrong JWT.
  • Operational risk: rotating JWT_SECRET_KEY invalidates all Scheme-B tokens; rotating the AES key in tokenGenerator.js invalidates all Scheme-A tokens. Both rotations must happen as deliberate actions; neither is automated.
  • The role check is done in SQL filters on a per-handler basis, not in middleware. Any new endpoint that forgets to filter by role_type is a privilege-escalation bug waiting to happen.

Recommendations

  • Pick one scheme and migrate. Scheme A is the more powerful (per-account context); deprecate Scheme B.
  • Move revokedTokens to a DB-backed store (tRevokedToken table with expiresAt).
  • Add an expiry to the encrypted token ({ ..., exp: <ms> }); validate in decryptData.
  • Add a middleware (requireRole(...)) that gates endpoints by role_type declaratively. Today the gating is SQL-implicit.