Authentication & Authorization

How the FaceKom apps authenticate agents/operators (the back-office “OSS” apps) and customers (the customer-facing “CSS” portal apps), and how they authorize via the shared acl (@techteamer/acl) library.

Two app families, two auth models

  • OSS apps (vuer_oss, esign_oss) = back-office. Passport.js strategies (local / Active Directory / SAML / WebAuthn / TOTP / token), role-based ACL via @techteamer/acl + config/roles.json, sessions in Sequelize (connect-session-sequelize).
  • CSS apps (vuer_css, esign_css) = customer portal/kiosk. No Passport, no ACL. Custom session-based customer auth (req.session.customer), sessions in Redis (connect-redis), SMS 2FA + ClientGate/KAÜ eID SSO. Heavy auth logic is proxied over RabbitMQ-RPC to the OSS side.

All committed config/{dev,docker}.json values (web.session.secret, jwt.secret, jwt.keystore[].k, AD/SMTP creds, SAML certs) are template/dev examples, not live secrets. Real deployments override them. See vuer_oss.


1. Mechanism → file map (quick index)

MechanismWhereConcrete source
Passport bootstrap, serialize/deserializevuer_ossserver/web/WebServerAuth.js:69-80,886-888
Local strategy (bcrypt)vuer_oss / esign_ossWebServerAuth.js:953-1079 / esign_oss server/web/WebServerAuth.js:234-302
Active Directory (LDAP)vuer_oss / esign_ossWebServerAuth.js:1081-1186
SAML — agent SSOvuer_ossWebServerAuth.js:285-368,460-508 (@node-saml/passport-saml)
SAML — customer eID (ClientGate/KAÜ)vuer_oss + vuer_cssserver/service/ClientGateService.js (samlify); vuer_css server/web/WebServer.js:288-346, routes/client-gate-login.endpoint.js
FIDO2 / WebAuthnvuer_ossWebServerAuth.js:370-451,510-694 (passport-fido2-webauthn)
TOTP / 2FA (authenticator)vuer_ossWebServerAuth.js:453-458,696-731; server/service/PasswordService.js:329-387
2FA via SMS OTPvuer_ossPasswordService.js:217-281
JWE token strategy (login)vuer_ossWebServerAuth.js:219-250; server/service/JWETokenStrategyService.js
JWT token strategy (login)vuer_ossWebServerAuth.js:252-283; server/service/JWTTokenStrategyService.js
OTL (one-time login) tokenvuer_oss / esign_ossWebServerAuth.js:122-217; server/service/OneTimeLoginService.js; db/model/oneTimeLogin.js
JWT/JWE crypto primitivesallserver/service/TokenService.js (node-jose, A256GCM)
RAB (resource-access-bypass) tokenvuer_ossserver/service/RabService.js; WebServer.js:355-383
Socket.IO auth tokenvuer_ossserver/SocketTokenStorage.js; server/socket/events/auth.js
Session store (OSS)vuer_oss / esign_ossWebServer.js:266-303 (Sequelize); db/model/session.js; server/web/session.js
Session store (CSS)vuer_css / esign_cssvuer_css WebServer.js:275-389 / esign_css web-server.js:100-129 (Redis)
ACL evaluationall OSS@techteamer/aclserver/acl.js, server/auth.js, config/roles.json
ACL library implacl reposrc/ACLService.js, src/ACLManager.js, index.js

2. OSS Passport pipeline (vuer_oss)

Built in server.js:613-655 (StartWebServer). Middleware order is load-bearing:

WebServer:    setupLocale → setupResourceAccessBypass → setupSession → setupFlash
WebServerAuth: setupInteractiveAuthStrategy → setupTokenStrategies → start → setupInteractiveRouting → setupOTLTokenRouting
WebServer:    setupTwig → setupRequestLogger → setupSessionACLHandler → setupRoutes → … → setupErrorHandler

(verbatim chain at server.js:616-639)

WebServerAuth (server/web/WebServerAuth.js) is required at server.js:13 and constructed with webServer.app (server.js:622).

2.1 Passport core (WebServerAuth.js:62-102)

  • passport.serializeUser stores only user.id (:69-71); deserializeUser does dbModels.User.findByPk(id) (:72-79).
  • start() (:886-951) mounts passport.initialize() + passport.session(), then installs the firewall and the role-resolution middleware (see §6).
  • package.json deps: passport@^0.7.0, passport-local@^1.0.0, passport-activedirectory (GitHub tarball TechTeamer/passport-activedirectory), @node-saml/passport-saml@^5.1.0, passport-fido2-webauthn@^0.1.0, passport-totp@^0.0.2, passport-strategy@^1.0.0.

2.2 Strategy selection (setupInteractiveAuthStrategy, :82-102)

Interactive (one of): activeDirectory → AD; else saml → SAML; else (unless disableLocalStrategy) → Local. Additively layered: if security.webAuthn set → WebAuthn; if security.totp set → TOTP. Each pushed strategy is passport.use(strategy.name, strategy).

Token (setupTokenStrategies, :104-120): if jweTokenStrategy.tokenKeyId → JWE token strategy; else if jwtTokenStrategy.tokenKey → JWT token strategy. Always adds the OTL token strategy (named oneTimeLoginToken).

2.3 Local strategy (_setupLocalStrategy, :953-1079)

passport-local LocalStrategy({ passReqToCallback: true }):

  • Looks up User.unscoped().findOne({ where: { username } }).
  • Password compare via passwordService.compare(pass, user.password) (bcrypt, see §8). On unknown user it still runs a dummy bcrypt compare against a hard-coded hash to equalize timing (:982).
  • Rejects with generic { message: 'unknown_user' } for: bad password, no rights, !isEnabled, no getMainRole(). Emits user.auth.* audit events throughout.
  • Branching post-password (the multi-factor gate, :1031-1071):
    • passwordExpiry < now{ message: 'expired_password' }
    • security.twoFactorAuth.via{ message: 'two_factor_auth' }
    • else security.webAuthn{ message: 'need_web_authn' }
    • else security.totp{ message: 'need_totp' }
    • else success → user.login event, ipFilter.releaseIp.
  • The route handler _setupLocalRouting (:733-832) regenerates the session (req.session.regenerate, :739), runs passport.authenticate(strategyNames, …) over all non-webauthn/non-totp strategies, and maps those messages to redirects (/password-change/<token>, /two-factor-auth/<token>, the WebAuthn URL, /totp).

2.4 Active Directory (_setupActiveDirectoryStrategy, :1081-1186)

  • One passport-activedirectory strategy per config.activeDirectory.servers[], named LDAP-SERVER-1..n (:1146-1158).
  • Group→rights mapping via config.activeDirectory.groups[groupDN] → role(s) (_getRights, :1162-1186). Groups come from a live LDAP getGroupMembershipForUser query, or from the user’s memberOf attribute when activeDirectory.useMemberOfProperty is true (:1139-1143).
  • features.roleSwitch off ⇒ user truncated to a single right (:1106-1108).
  • Hooks: ad:userModification before save (:1121). Then same need_web_authn / need_totp layering (:1125-1131).
  • Config schema: docs/config/schemas/activeDirectory.schema.json (servers[].ldap.{url,baseDN,filter,username,password,attributes}, groups, useMemberOfProperty). Schema doc link: docs/features/active-directory/developer-hu.md.

2.5 SAML — agent SSO (_setupSAMLStrategy, :285-368)

  • Library: @node-saml/passport-saml Strategy (:6).
  • Strategy named saml, callback path /login/callback, protocol: 'https://', host: config.hosts.oss (:294-302). Extra IdP options merged from the saml:config override hook (:286).
  • Verify maps profile.nameIDusername/email; rights come from the saml:profile hook (callHooks('saml:profile', …), :342). Empty rights ⇒ user.auth.not_authorized + reject (:345-348). User upserted via _findOrCreateUser keyed on email (case-sensitive) (:337-350).
  • Routing (_setupSAMLRouting, :460-508): GET /saml initiates (encodes RelayState from a redirectTo cookie); POST /login/callback consumes (validates RelayState is a local / path before redirecting to https://hosts.oss…).
  • Cookie sameSite forced to 'Lax' when SAML is on (WebServer.js:296).
  • Config schema: docs/config/schemas/saml.schema.json (entryPoint, issuer, callbackUrl, idpCert, authnContext, identifierFormat, wantAuthnResponseSigned, audience).

2.6 FIDO2 / WebAuthn (:370-451, :510-694)

  • Library: passport-fido2-webauthn (WebAuthnStrategy + SessionChallengeStore, :8,10). Store instance this.webAuthnStore = new SessionChallengeStore() (:371) — challenges live in the session.
  • Verify (login): finds WebAuthnCredential.findOne({ credentialId: id }) → its User by userId; compares the supplied userHandle to the stored handle (base64url) (:389); returns (user, credential.publicKey, { status: 'login' }).
  • Register: finds user by userInfo.name; enforces security.webAuthn.maxCredentialsPerUser (default 100, deletes oldest first, :409-434); creates a WebAuthnCredential row { credentialId, handle, publicKey, userId } (:436-441); emits webAuthnCredential.create/.delete.
  • Routing (_setupWebAuthnRouting, :510-694):
    • POST /web-authn/registration/challenge — requires either an authenticated user with users.change.webAuthnCredentials ACL right, or security.webAuthn.enableSelfRegistration + a valid token via passwordService.validateWebAuthnToken (:514-535).
    • POST /web-authn/login/challenge — validates a WebAuthn JWT, returns challenge + allowCredentialIds.
    • POST /web-authn/loginpassport.authenticate('webauthn', …); on status==='login' calls req.logIn and emits user.login {mode:'webAuthn'}; on status==='register' returns registered/ok.
  • DB model db/model/webAuthnCredential.js: credentialId (unique), handle (nullable), publicKey (TEXT), FK userId. Manual delete API: server/web/api/common/webauthn-credential-delete.js.
  • Config: security.webAuthn.{tokenExpiry='3m', maxCredentialsPerUser=100, enableSelfRegistration, pubKeyCredParams, authenticatorSelection, transports} (security.schema.json:290-312; sample block at WebServerAuth.js:33-60).

2.7 TOTP (:453-458, :696-731) and SMS-2FA (PasswordService.js:217-281)

TOTP (authenticator-app) — passport-totp:

  • Strategy returns (user.key, user.period) (:453-458).
  • Secret generation: PasswordService.generateTotpUrl creates a 40-char key (contactValidation.generateToken(40)), persists a TotpKey row, and builds an otpauth://totp/<label>:<username>?secret=<base32>&period=<period>&issuer=<issuer> URI (PasswordService.js:329-387). base32 via thirty-two.
  • Verify route POST /totp/verify-otp (:696-731): loads User.findByPk(req.session.totpUserId), sets req.user.key/period from the TotpKey, runs passport.authenticate('totp', { failureRedirect: '/totp/failure' }), then passwordService.verifyTotpKey (marks TotpKey.verified=true) and emits user.login {mode:'totp'}.
  • DB model db/model/totpKey.js: key (STRING), verified (BOOL default false), FK userId. Config security.totp.{period=30, label='Facekom', issuer='Facekom'} (PasswordService.js:381-386).

SMS OTP 2FA (separate from authenticator TOTP, gated by security.twoFactorAuth.via):

  • PasswordService.twoFactorAuth(user) (:217-231) issues a JWT-in-JWE token (createTwoFactorToken, tokenExpiry default '5m', :233-238) → URL /two-factor-auth/<token>, and sends an OTP. sendTwoFactorOTP (:254-281) generates an otpLength (default 6) token and sends via SMS (via: 'sms'; other values throw). The OTP is stashed in req.session.otp by the login route (WebServerAuth.js:792-796).
  • Config schema security.twoFactorAuth.{otpLength, tokenExpiry, via} (security.schema.json:276-288).

2.8 Token login strategies (JWE / JWT / OTL)

All three use the custom UniqueTokenStrategy (server/util/UniqueTokenStrategy.ts) — a passport-strategy subclass that pulls a token from body/query/params/cookie/header (:80-106). Named strategies: JWEToken, JWTToken, oneTimeLoginToken.

  • JWE token (_setupJWETokenStrategy, :219-250 + JWETokenStrategyService.js): decrypts the token with tokenService.decrypt(token, jweTokenStrategy.tokenKeyId), runs the tokenStrategy:createUserParams hook, validates {username,rights,firstName,lastName,email?} (regex-checked username/email, non-empty string-array rights), then _findOrCreateUser and updates name/email/rights. Disabled user ⇒ user.auth.disabled. Config: jweTokenStrategy.tokenKeyId (docs/config/schemas/jweTokenStrategy.schema.json).
  • JWT token (_setupJWTTokenStrategy, :252-283 + JWTTokenStrategyService.js): jwt.verify(token, tokenKey) using jsonwebtoken; jwtTokenStrategy.tokenKey may be a single key or an array (tries each, :14-18). Same param validation + upsert as JWE. Config: jwtTokenStrategy.tokenKey.
  • OTL (_setupOTLTokenStrategy, :122-155): gated by config.otl. Consumes a one-time token via oneTimeLogin.useLoginToken(token); can set req.session.role/req.session.rules from the token’s requiredRole/meta.sessionRules. Route GET /otl/:token (setupOTLTokenRouting, :157-217) signs out any current session first, throttles failures via ipFilter.

2.9 OTL token model & service

  • server/service/OneTimeLoginService.js: createLoginToken(userId, redirectTo, comment, expireDurationSec=300, requiredRole, meta) — token = crypto.randomUUID(), retries up to 10× on unique-constraint collision (:26-45). useLoginToken enforces single-use (usedAt) + expiry (expiredAt) (:53-74). deleteExpired purges rows >1 week old (:81-92).
  • Model db/model/oneTimeLogin.js: token (unique), redirectTo, comment, usedAt, expiredAt (required), requiredRole (validated against User.allRights), meta (JSON).
  • CLI: bin/create_rab_token.js documented in vuer_oss uses TokenService + config.jwt.secret.

3. esign_oss Passport (the lighter back-office)

esign_oss/server/web/WebServerAuth.jssubset of vuer_oss:

  • Deps (esign_oss/package.json): passport@^0.7.0, passport-local@^1.0.0, passport-activedirectory@^1.0.4, passport-unique-token@^3.0.0, @techteamer/acl@^2.0.0, bcryptjs, connect-session-sequelize@^8.0.4. No SAML, WebAuthn, TOTP, or samlify deps.
  • Strategies: setupInteractiveAuthStrategy chooses AD or Local (:51-59); setupOTLTokenStrategy adds OTL using the npm passport-unique-token Strategy (:4,65) rather than vuer_oss’s custom .ts one. OTL routing GET /otl/:token mirrors vuer_oss (:91+).
  • ACL roles: config/roles.json = ['admin','operator'] (vs vuer_oss’s ['admin','supervisor','operator','bb_operator']).
  • Session store: Sequelize (server/web/web-server.js:3,177,197).

4. Customer auth — CSS portals (vuer_css / esign_css)

No Passport, no @techteamer/acl here. Customer identity lives in req.session.customer; the real credential/2FA logic is RPC'd to the OSS side over RabbitMQ.

4.1 esign_css

  • Customer API routes (server/web/routes.js:232-253) guarded by resolveSession() + requireSession() middleware: /api/customer/{check-username,check-password,create,get-data,login,logout,2f-auth,agreement-check}. 2f-auth additionally has csrfProtection + auth2factorApiIpFilter (routes.js:247).
  • CustomerService (server/service/CustomerService.js, 61 lines) is a thin RPC proxy: loginCustomer/checkPassword/setCredentials/auth2factor/getCustomerData/logout all forward to serviceContainer.rpcClient.customer.* (esign:rpc-customer, wired server.js:105). auth2factor(session, signatureId, username, password, smsToken, verificationAttemptCount) (:32-33) ⇒ SMS-token 2FA implemented in esign_oss.
  • Session identity check example: server/web/api/sentry.js:5-16 reads req.session.customer.id.

4.2 vuer_css

  • rpc-jwt-auth RPC client (server.js:118, JwtAuthRPCClient) authenticates the customer/portal JWT against the OSS rpc-jwt-auth server.
  • TokenService (server/service/TokenService.js, identical API to OSS — node-jose, A256GCM JWE) loaded at boot (server.js:259,271).
  • Device pairing JWTs in server/socket/events/mobile.js: jwt.sign({ customerId, sessionId }, config.jwt.secret, { expiresIn: config.jwt.expiryPeriod }) (:109,156) and jwt.verify (:141). Janus tokens signed in routes/lobby.endpoint.js:25, system-check.endpoint.js:17.
  • Socket auth token server/SocketTokenStorage.js: jwt.sign({ sessionId }, jwt.secret, { expiresIn: '2m' }) / verify.

5. SAML / IdP details

Two independent SAML stacks in the codebase:

Agent SSOCustomer eID (ClientGate / KAÜ / Ügyfélkapu)
Library@node-saml/passport-saml@^5.1.0samlify@^2.7.1 + @authenio/samlify-node-xmllint@^2.0.0
Codevuer_oss WebServerAuth.js:285-368,460-508vuer_oss server/service/ClientGateService.js; server/queue/rpc_server/ClientGate.js; vuer_css WebServer.js:288-346, routes/client-gate-login.endpoint.js, routes/saml-metadata.endpoint.js, routes/self-service-client-gate.endpoint.js
Config keysaml.*clientGate.*
Whoback-office operatorsend customers (self-service identification)

ClientGateService (ClientGateService.js):

  • init() registers the samlify schema validator and reads customization/assets/saml/AuthnRequestTemplate.xml (:35-39).
  • generateAuthnRequest builds a custom AuthnRequest XML (samlify can’t add the AttributeStatement extension) with urn:eksz.gov.hu attributes (requestData.attributes, e.g. muveletkod=L001, 4t) and requestData.audience (e.g. urn:eksz.gov.hu:1.0:azonositas:kau:2) (:46-77,203-226).
  • handleAuthnResponse calls serviceProvider.parseLoginResponse(idp, 'post', { body: { SAMLResponse } }) (:79-90).
  • Security posture: wantAuthnRequestsSigned, isAssertionEncrypted, wantMessageSigned, wantAssertionsSigned, authnRequestsSigned, messageSigningOrder default ETS (encrypt-then-sign) (:107-179). The SP is re-created per request specifically to avoid keeping the unencrypted private key resident (:127-136). Known validation errors handled: ERR_FAILED_TO_VERIFY_SIGNATURE, ERR_POTENTIAL_WRAPPING_ATTACK, cert-mismatch (:228-240).
  • vuer_css redis hand-off (WebServer.js:288-346): because a cross-origin POST from the IdP carries no session cookie, the SAMLResponse+RelayState are cached in Redis under samlresponse:<uuid> with EX 60, then the browser is GET-redirected to /saml/sso/clientGateLogin?samlResponseCacheId=… to re-attach the session before processing.

6. Firewall, session-ACL, RAB (request gating in OSS)

WebServerAuth.start() (:886-951) installs, in order:

  1. passport.initialize() + passport.session() (:887-888).
  2. Firewall bypass rules (addFirewallBypassRule, :882-884; matched by isRequestBypassable, :865-880): always /login, ^/password-change/.+; conditionally ^/two-factor-auth/.+, ^/otl/.+, /saml + /login/callback, the /web-authn* set, /totp + /totp/verify-otp; plus a functional rule allowing the MNB MSZFH API host’s /external* paths (:914-917).
  3. Firewall (:920-936): if req.rabData present → pass (RAB already authorized); else if !req.user and not bypassable → set a redirectTo cookie and res.redirect('/login').
  4. Role resolution (:938-948): default req.session.role = req.user.getMainRole(); req.rabData.role overrides.

Session-based ACL restrictionWebServer.setupSessionACLHandler (WebServer.js:496-516): if the session carries role+rules, builds a transient ACLService from { [role]: rules } and rejects the request with 403 “Forbidden by session” unless isAllowed(req.path.replace(/\//g,'.'), role). This lets OTL/socket sessions be scoped to a subset of a role’s rights.

RAB (Resource Access Bypass)WebServer.setupResourceAccessBypass (WebServer.js:355-383) + server/service/RabService.js:

  • createRabToken(accessPath, data, expiresIn='1h'): jwt.sign({ urls, data }, config.jwt.secret) then tokenService.encrypt(...) (JWE-wrapped JWT).
  • verifyRabToken(requestPath, token): JWE-decrypt → jwt.verify → path must match one of urls (supports trailing-* prefix match). Token read from ?rab= or the rab cookie. On mismatch ⇒ HTTP 401.

7. Socket.IO authentication (vuer_oss)

  • server/SocketTokenStorage.js: createToken(userId, role, rules=null) = jwt.sign({ userId, role, rules }, config.jwt.secret, { expiresIn: '2m' }); verifyToken returns {userId, role, rules} or undefined.
  • server/socket/events/auth.js (auth:auth handler): verifies the token, loads User.findByPk(userData.userId), blocks non-admins during “disaster” mode (:23-27), sets client.user/role/rules. Builds a per-socket session ACL from client.rules and registers only the event modules the role (and session ACL) allow via accessControlList.isAllowed(moduleRule, role) (:40-51). Emits socket:auth.
  • server/auth.js is the request-side helper: isAuthorized(client) (user is an enabled User model), hasRight(client, right) (global ACL AND session-ACL client.rules), plus doIfAuthorized / doIfHasRight / doIfHasRoom wrappers that reload() the user before invoking the callback.
  • SocketSessionService.js correlates sockets to the Sequelize session via the session cookie (getUserIdFromSessionId, getSessionTokenFromCookie).

8. Passwords (bcrypt) & recovery (vuer_oss)

server/service/PasswordService.js:

  • hash/compare use bcryptjs (@^3.0.2), cost security.password.bcryptCost, optionally offloaded to a worker thread (workers/bcrypt, engines/worker/Runner) (:11-31).
  • Auth-mode predicates: isInternalAuthUsed (local), isRemoteAuthUsed (AD/SAML with disableLocalStrategy), isTokenAuthUsed (JWE/JWT strategy) (:33-43).
  • Password lifecycle: change writes PasswordHistory, enforces passwordHistory depth and passwordExpiry (:77-121); isChangeAvailable throttles by maxChangeFrequency/changeCoolDownPeriod (:45-75); resetPassword issues a temp password emailed/SMSed (:123-173).
  • Recovery: createRecoveryToken = jwt.sign({userId}, jwt.secret, {expiresIn}) then tokenService.encrypt(...) → URL /password-change/<token> (:192-201); validateRecoveryToken decrypts + verifies (:203-215). WebAuthn login token (getWebAuthnLoginUrl/validateWebAuthnToken, :287-321) uses the same JWE-wrapped-JWT pattern.
  • Config schema security.password.* (security.schema.json:124-174): bcryptCost, defaultExpiry, passwordHistory, recoveryEmailExpiry, wrongPasswordLimit, wrongPasswordGracePeriod, wrongTwoFactorLimit, etc.

9. JWT / JWE crypto (TokenService) — all apps

server/service/TokenService.js (same shape in vuer_oss, vuer_css, esign_css):

  • loadKeyStore() builds a node-jose keystore from config.jwt.keystore + config.jwt.customKeystore (:9-15).
  • encrypt(o, keyId='facekom')JWE compact, contentAlg: 'A256GCM' (:17-24). decrypt reverses (:26-30).
  • sign/verifyJWS compact with the same keystore (:32-41).
  • Default key id is 'facekom'. Example dev keystore (vuer_oss config/dev.json:300-307): jwt.secret is a UUID; jwt.keystore = two oct (symmetric) keys kid:'facekom' and kid:'facekomPont'. jwt schema (docs/config/schemas/jwt.schema.json): secret, expiryPeriod, emailExpiryPeriod, deviceChangeExpiryPeriod, keystore/customKeystore (each {use:'sig'|'enc', kty, kid, k}).
  • Note the two JWT mechanisms in OSS: raw jsonwebtoken.sign/verify keyed on the string config.jwt.secret (RAB, OTL recovery, socket, 2FA, device tokens), vs. node-jose JWS/JWE keyed on the keystore (TokenService). They are layered: recovery/RAB/2FA tokens are a jwt.sign(...) string that is then tokenService.encrypt(...)-wrapped in JWE.

10. Session stores

AppStoreSource
vuer_ossSequelize (connect-session-sequelize@^8), table SessionWebServer.js:266-303; db/model/session.js; server/web/session.js
esign_ossSequelize (connect-session-sequelize@^8)server/web/web-server.js:3,177,197
vuer_cssRedis (connect-redis@^9, redis@^5) + separate kiosk sessionWebServer.js:2-4,275-389
esign_cssRedis (connect-redis@^9, redis@^5)server/web/web-server.js:14,24,100-129

OSS Sequelize session specifics (WebServer.js:266-303):

  • SequelizeStore({ db, table:'Session', checkExpirationInterval, expiration }); cookie { httpOnly:true, secure:true, sameSite: saml?'Lax':security.cookie.sameSite, expires:false }; resave:false, saveUninitialized:false, proxy:true.
  • server/web/session.js: listSessions/closeSession parse Session.data JSON to find/kill all sessions for a userId; onExpressSessionExpiry patches clearExpiredSessions to emit user.auth.session_expired. On user.login, all other sessions for that user are destroyed (single-session enforcement, WebServer.js:280-290).

CSS Redis session specifics:

  • vuer_css (WebServer.js:275-389): one redis.createClient(config.web.session.redis) shared for both the SAML-response cache and RedisStore; per-request switch between normal and kiosk session middleware (req.isKiosk).
  • esign_css (web-server.js:100-129): patches sessionStore.generate to drop sameSite to false for iOS-12 Apple-mobile clients (cookie compatibility hack).

The "HA Redis" in vuer_oss ( server/bootstrap/connection/redis.js, gated by HARedis.config) is for shared locks/state, not the session store. vuer_oss sessions are in Postgres via Sequelize. See vuer_oss.


11. ACL model (@techteamer/acl) — shared authorization

Library repo: acl (/Users/levander/coding/facekom-v2-clones/acl). Dependency-free, dual ESM/CJS, @techteamer/acl@2.0.2.

11.1 Rule grammar (src/ACLService.js)

  • A role owns two compiled-regex lists: accept and reject (:52).
  • import(configJSON) (:10-40): keys are roles; values are rule strings. @-prefixed roles/rules are ignored (used for comments / the @index master list). Duplicate rules within a role are logged as errors.
  • createRule (:61-93): a leading ! makes it a reject rule; . is escaped and * compiles to .*; rules become case-insensitive anchored regexes (^…$).
  • Decision isAllowed(access, role) (:112-162): validates the access string, then a rule is allowed iff some accept regex matches AND no reject regex matches — i.e. reject always outranks accept. Results are memoized in _resultCache.
  • areAllowed = all-of (AND), anyAllowed = any-of (OR) (:164-229).
  • _log throws ACLError on error level (:238-240) — malformed roles/rules are fatal, not silent.

11.2 ACLManager (src/ACLManager.js)

  • Holds an ordered stack aclList; import(acl) unshifts so the latest-imported ACL wins for a given role (:32-39). getACLForRole returns the first ACL that defines the role (:50-58). roleList is the flattened unique set across all ACLs (:41-44).

11.3 How OSS wires it

  • server/acl.js: a singleton ACLService that reads config/roles.json at module load and imports it (:5-23). Registered as serviceContainer.service.accessControlList (vuer_oss server.js:133, esign_oss server.js:131), with a logger that suppresses debug and forwards the rest.
  • config/roles.json (vuer_oss): roles @index (the master catalogue of every rule string, alphabetical, 148 rule strings — 157 total array entries including 9 @-comment header lines; the whole role is @-prefixed so it’s import-ignored), then real roles admin (129 rules), supervisor (54), operator (31), bb_operator (22). esign_oss: admin, operator.
  • User.allRights (the legal rights set) = acl.roleList (db/model/user.js:250-254); User.prototype.hasRight/getMainRole (:183-195) drive role selection. The user.rights column is JSON-encoded and validated against allRights on write (:36-66).
  • Request-time checks: server/auth.js:hasRight combines the global ACL with the per-session ACL built from client.rules; socket modules and setupSessionACLHandler use the same primitive (see §6, §7).

Master-list drift risk: config/roles.json's @index block is manually maintained ("IF YOU ADD A NEW RULE ADD IT TO THIS LIST TOO", roles.json:1-11). Because @-rules are import-ignored, @index is documentation only — the effective User.allRights comes from the real roles' rule sets, so a rule used by a role but absent from @index still works. See acl.


12. Cross-cutting auth controls

  • IP throttling / brute-force: security.ipFilter.* (uncheckedIps, suppressAfterAttempts, throttlePeriodMs, coolDownPeriodMs, security.schema.json:107-122). ipFilter.monitorIp/isIpFiltered/releaseIp invoked on every failed/successful auth across local/AD/OTL/WebAuthn (e.g. WebServerAuth.js:959-963,177-183,645). Emits user.auth.throttled.
  • Audit events: a consistent user.auth.* / user.login / user.logout event vocabulary (unknown_user, wrong_password, not_authorized, disabled, no_main_role, expired_password, password_success, throttled, session_expired, session_destroy) feeds server/auditlog.js. See server/listeners/user-listeners.js.
  • CSRF: csurf + security.csrf.cookie.* (security.schema.json:15-33); applied to e.g. esign_css /api/customer/2f-auth (routes.js:247). See vuer_oss.
  • Sensitive config redaction: security.sensitiveConfigFields enumerates secret paths (web.session.secret, jwt.secret, jwt.keystore.*.k, activeDirectory.servers.*.ldap.password, saml.idp, saml.cert, clientGate.*.certContent/keyContent, …) for masking (security.schema.json:175-274).
  • Helmet / CSP / host-header allow-list: WebServer.js:38-46 rejects unknown Host; CSP nonce per request (:48-51); security.helmet.*. See architecture-overview / vuer_oss.

Unverified / gaps

  • I did not open esign_oss’s full _setupLocalStrategy/AD bodies beyond lines 234-302/304-368 (confirmed structure + deps, not every branch). The esign_oss SMS-2FA / auth2factor server-side implementation (the esign:rpc-customer server) was not read.
  • vuer_css’s routes/client-gate-login.endpoint.js and saml-metadata.endpoint.js bodies were not opened (their existence + the Redis hand-off in WebServer.js are verified; the per-endpoint processing is inferred to call OSS ClientGate RPC and was not line-verified).
  • The OSS server/queue/rpc_server/ClientGate.js and rpc-jwt-auth server bodies were not opened (only their names/registration).
  • Exact passport runtime versions are the manifest ranges in package.json, not the locked yarn.lock pins.
  • Whether disableLocalStrategy/saml/activeDirectory/security.webAuthn/security.totp are enabled in any given deployment depends on partner config (not in these repos).

Sources

Read directly (all under /Users/levander/coding/facekom/ unless noted):

  • vuer_oss: server.js (12-13, 128-139, 612-685), server/auth.js, server/acl.js, server/SocketTokenStorage.js, server/web/WebServerAuth.js (full), server/web/WebServer.js (1-130, 240-519), server/web/session.js, server/socket/events/auth.js, server/service/{JWETokenStrategyService,JWTTokenStrategyService,TokenService,RabService,OneTimeLoginService,PasswordService,ClientGateService,SocketSessionService}.js, server/util/UniqueTokenStrategy.ts, server/db/model/{user,webAuthnCredential,totpKey,session,oneTimeLogin}.js, config/roles.json (head + counts), config/dev.json (300-314), docs/config/schemas/{jwt,saml,jweTokenStrategy,jwtTokenStrategy,security,activeDirectory}.schema.json, package.json (deps).
  • esign_oss: server/web/WebServerAuth.js (1-120), server.js (61,105,121,131-132), config/roles.json (keys), package.json; server/web/web-server.js (session refs via grep).
  • vuer_css: server/web/WebServer.js (2-4, 275-389), server.js (118, 246-271), server/service/TokenService.js, server/SocketTokenStorage.js, server/socket/events/mobile.js (grep), package.json.
  • esign_css: server/web/web-server.js (14,24,100-129), server/web/routes.js (232-253), server/web/api/sentry.js, server/service/CustomerService.js, server.js (105,121,148-186), package.json.
  • acl (/Users/levander/coding/facekom-v2-clones/acl): src/ACLService.js, src/ACLManager.js, index.js; prior doc acl for manifest/version.
  • Orientation only (independently re-verified): /Users/levander/levandor_obsidian/projects/facekom-v2/repos/{vuer_oss,acl}.md.