Authentication & Security¶
Detailed documentation of the authentication system, token lifecycle, OAuth flows, partner auth, and authorization model.
Architecture Overview¶
The system uses a hybrid JWT + AES-256 encryption scheme for token security, with Passport.js handling OAuth flows for social login providers.
| Layer | Technology | Purpose |
|---|---|---|
| Token Encryption | JWT + CryptoJS AES | Double-layer token protection |
| OAuth | Passport.js (6 strategies) | Social login/account connection |
| Partner Auth | Plain JWT (issuer/audience) | Third-party API access |
| Session | express-session + Passport | OAuth flow state |
| Authorization | checkuseraccount() |
Account-level access control |
Token Generation & Verification¶
File: helper/tokenGenerator.js
Encryption (encryptData)¶
1. JSON.stringify(payload)
2. CryptoJS.AES.encrypt(jsonString, JWT_SECRET_KEY) → ciphertext
3. jwt.sign({ data: ciphertext }, JWT_SECRET_KEY, { expiresIn }) → token
The result is a JWT whose payload contains AES-encrypted data. This provides: - Tamper detection (JWT signature) - Payload confidentiality (AES encryption)
Decryption (decryptData)¶
1. jwt.verify(token, JWT_SECRET_KEY) → { data: ciphertext }
2. CryptoJS.AES.decrypt(ciphertext, JWT_SECRET_KEY) → jsonString
3. JSON.parse(jsonString) → payload object
Token Payload Structure¶
{
userId: number, // tMember.id
role: number, // tMember.role_type
defaultPaymentId: number, // Payment provider (2 = Paddle)
accountId: number, // Active account context
isPersonnel: boolean // Staff/personnel flag
}
Token Lifetimes¶
| Token Type | Expiry | Use Case |
|---|---|---|
| User login | JWT_EXPIRES_IN env var (e.g., 7d) |
Main API access |
| OAuth callback | 10 minutes | Temporary token during OAuth redirect |
| Partner API | 1 hour | Partner endpoint access |
| Email verification | JWT_EXPIRES_IN |
Account activation links |
Auth Middleware¶
File: middlewares/auth.js
Applied globally to routes/auth.js via router.use(auth).
Flow¶
- Extract token from request header (
TOKEN_HEADER_KEYenv var) - Check in-memory revocation set
- Call
decryptData(token)— verifies JWT signature + decrypts AES payload - On success: set decoded fields on
resobject - On failure: return 401 with plain string
"unauthorized access"(not the standard envelope shape — see error-handling.md §Middleware Errors)
Note: parallel auth path in
methods.js.methods.jsexportsensureToken, an alternative to this middleware. It hardcodes the header name to"authorization"rather than readingTOKEN_HEADER_KEY, returns 403 (not 401), and usessendStatus(no body). The two implementations are independent — neither calls the other. Routes choose which to apply on a per-route basis. This dual-path pattern is itself a finding flagged in enterprise-readiness §4.1.
Fields Set on res¶
| Field | Source | Description |
|---|---|---|
res.userId |
JWT payload | Authenticated user ID |
res.role |
JWT payload | User role type |
res.defaultPaymentId |
JWT payload | Payment provider ID |
res.accountId |
Accountid request header |
Active account context |
res.isPersonnel |
JWT payload | Staff flag |
Important: Auth data is set on res, not req. Route handlers destructure from res.
Token Revocation¶
File: helper/revokeToken.js
- In-memory
Set()checked before token decryption - Populated on logout
- Not persisted (lost on server restart)
- No TTL cleanup
Login Endpoints¶
Standard Login (POST /login)¶
File: routes/routes.js (~line 3024–3086)
- Query
tMemberby email (sid) and password - Generate access token:
{userId}_{timestamp}_{userId} - Return user data with token
⚠ Security note — plaintext login token. The login token is a plaintext concatenation of the user ID and a timestamp, with no AES encryption, no JWT signature, and no integrity check. This is inconsistent with the rest of the auth model (which uses the JWT+AES encryption from § Token Generation), and inconsistent with what the Auth Middleware section describes (which calls
decryptDataexpecting a JWT-AES wrapper).Two implications: - Anyone reading this token in any log, analytics pipeline, or cookie store can extract the user ID and forge a token by guessing a timestamp. - There is an internal inconsistency: if
middlewares/auth.jsrunsdecryptDataon a plaintext token, decryption will fail. Either there is a separate code path that handles login tokens (not yet identified), or login tokens cannot be used with the standard auth middleware. Investigate before relying on this section.Action items: treat as a security finding for the backlog. Reconcile the login-token format with the rest of the auth model — either encrypt login tokens like all others, or document a clear exception for whatever code path consumes them.
Admin Login (POST /Adminsignin → POST /Adminlogin)¶
- Find user by email
- Generate OTP (3 bytes hex random)
- Store OTP in
tMember.access_token - Send OTP via email
- User submits OTP to
/Adminloginfor verification
Registration (POST /register)¶
- Check username uniqueness
- Create
tMemberrecord - Generate encrypted token with
{ userId, defaultPaymentId: 2 } - Send email verification link (separate token with type:
'emailVerification') - Create trial subscription in
tStripe(14-day default)
OAuth Flows (Passport.js)¶
Files: middlewares/passport.js, routes/social.js
Supported Providers¶
| Provider | Strategy | Client ID Env | Callback URL | Scopes |
|---|---|---|---|---|
passport-google-oauth20 |
GOOGLE_CLIENT_ID |
/social/google/callback |
profile, email | |
| GitHub | passport-github2 |
GITHUB_CLIENT_ID |
/social/github/callback |
user:email |
passport-facebook |
FACEBOOK_APP_ID |
/social/facebook/callback |
emails, displayName, picture | |
passport-linkedin-oauth2 |
LINKEDIN_CLIENT_ID |
/social/linkedin/callback |
openid, profile, r_organization_social, w_member_social, etc. | |
@superfaceai/passport-twitter-oauth2 |
TWITTER_CLIENT_ID |
/social/twitter/callback |
tweet.read, users.read, offline.access | |
| TikTok | passport-tiktok-auth |
TIKTOK_CLIENT_ID |
/social/tiktok/callback |
user.info.basic, video.upload, video.publish |
OAuth Flow Pattern¶
1. GET /social/{provider}
→ Passport redirects to provider authorization page
2. User authorizes at provider
3. GET /social/{provider}/callback
→ Passport exchanges code for tokens
→ Extract profile data (name, email, photo, tokens)
→ encryptData(userData, '10m')
→ Redirect to: {APP_URL}/socialAuth?user={encrypted_token}
4. Frontend decrypts token and establishes session
Provider ID Constants (helper/constants.js)¶
| Provider | ID | Used In |
|---|---|---|
| 1 | tMemberAuth.provider_Id |
|
| 2 | tMemberAuth.provider_Id |
|
| 4 | tMemberAuth.provider_Id |
|
| TikTok | 6 | tMemberAuth.provider_Id |
Token Storage for Connected Accounts¶
OAuth access/refresh tokens are stored in tMemberAuth:
- accessToken — current access token
- refresh_token — for token renewal
- token_status — validity state
- Provider-specific JSON fields: FbAccounts, IgAccounts, LnAccounts, TwtAccounts
Partner API Authentication¶
File: routes/partnerAuth.js
Authentication (POST /partnerAuth/partnerAuthentication)¶
Input: { partnerId, secretKey }
- Validate against
tPartnerAuthtable - Generate plain JWT (no AES layer):
- Store token and expiry in
tPartnerAuth.pToken - Return token with 3600s expiry
Partner User Registration (POST /partnerAuth/registerUser)¶
Headers: Authorization: Bearer {partner_jwt}
- Verify partner JWT (issuer/audience claims)
- Generate random 12-char password
- Hash with bcrypt (salt rounds: 10)
- Create
tMember,tAccounts,tAccountMembers,tSubscriptions - Return login URL:
{partner.appUrl}/#login?token={encrypted_user_token}
Single Sign-On (POST /partnerAuth/singleSignOn)¶
Headers: Authorization: Bearer {partner_jwt}
- Verify partner JWT
- Find user, get all active accounts
- Generate encrypted token with
{ userId, role, defaultPaymentId, accountIds[] } - Return login URL
Partner Sign-In (POST /partnerAuth/partnerSignIn)¶
- Find partner by name
- Generate JWT signed with partner's
secretKey(HS256) - Payload:
{ partner, timestamp, user_data, redirect_url } - Return redirect URL with token
Alternative Auth: methods.js¶
Function: ensureToken
Simpler JWT verification used on GET / in routes/routes.js:
- Extracts Bearer token from Authorization header
- Verifies JWT signature only (no AES decryption)
- Returns 403 on invalid token
Authorization Model¶
Account Access Check¶
Function: checkuseraccount(memberId, accountId) in helper/helper.js
Two-tier authorization:
- Regular members: Check
tAccountMemberswherememberId+accountId+isDeleted=0+active=1 - Personnel: If not found above, check
tPersonnelAccountswherepersonnelId+accountId+isDeleted=0+active=1
Returns true if user has access, false otherwise. Called by most authenticated route handlers.
Role Model¶
| Role ID | Name | Context |
|---|---|---|
| 0 | Admin | Full account access |
| 1 | Owner | Equivalent to admin |
| 2 | Manager/Collaborator | Reduced permissions |
| 3 | Contributor | Limited permissions |
| 4 | Designer | Design-specific access (env: DESIGNER) |
| 5 | User Member | Standard member (env: USER_MEMBER) |
| 6 | Account Manager | Management role (env: ACCOUNT_MANAGER) |
| 11 | Disabled/Archived | Excluded from operations |
Role fields:
- tMember.role_type — User's base role
- tAccountMembers.roleType — Role within specific account
Personnel System¶
The isPersonnel flag distinguishes staff from regular users:
- Set in JWT payload during token generation
- Personnel access tracked in tPersonnelAccounts (separate from tAccountMembers)
- Allows cross-account access for support/management
Session Configuration¶
File: server.js
app.use(session({
secret: SESSION_SECRET, // env var, default: "change-me"
resave: false,
saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session());
Passport serialization stores the entire user object in session (simple approach, no database lookup on deserialization).
Security Environment Variables¶
| Variable | Purpose |
|---|---|
JWT_SECRET_KEY |
Signs JWT + encrypts AES payload |
JWT_EXPIRES_IN |
Default token expiry (e.g., 7d) |
TOKEN_HEADER_KEY |
Header name for token extraction |
SESSION_SECRET |
Express session secret |
Request Authentication Flow¶
┌──────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │ │ Auth Middleware│ │Route Handler│
└────┬─────┘ └──────┬───────┘ └──────┬──────┘
│ │ │
│── Request + Token ──▶│ │
│ (Authorization hdr) │ │
│ │── Check revocation ──▶ │
│ │── JWT verify ────────▶ │
│ │── AES decrypt ───────▶ │
│ │ │
│ │── Set res.userId ─────▶│
│ │── Set res.accountId ──▶│
│ │── Set res.role ───────▶│
│ │── next() ────────────▶│
│ │ │
│ │ ┌──┴──┐
│ │ │Check │
│ │ │account│
│ │ │access │
│ │ └──┬──┘
│◀──────────── Response ─────────────────────────│
Known Security Considerations¶
| Issue | Location | Impact |
|---|---|---|
| Plaintext password storage | POST /register |
Passwords readable if DB compromised |
| Plaintext password comparison | POST /login |
No timing-attack protection |
| SQL string interpolation | Multiple route handlers | Injection risk |
| In-memory token revocation | helper/revokeToken.js |
Revocations lost on restart |
| No rate limiting | Login endpoints | Brute-force vulnerable |
| Predictable network token format | POST /login (network) |
{userId}_{timestamp}_{userId} pattern |
Note: Partner auth (routes/partnerAuth.js) correctly uses bcrypt for password hashing — this pattern should be extended to all user registration/login flows.