Authentication & Permissions

How erxes identifies team members, portal visitors, and machine apps, and where each credential is checked.

For the user model, see Organizations, Users & Permissions. For portal-specific flows, see Client Portal.

Credential types

All of these are checked by userMiddleware in backend/gateway, before the request reaches Apollo Router.

Credential How issued Transport Identifies Where checked
Team-member session login mutation (or WorkOS SSO); JWT { user: { _id, isOwner } }, 1-day expiry, registered in Redis user_token_<id>_<token> for 24h Authorization: Bearer <jwt> or auth-token httpOnly cookie A Users document Gateway: JWT signature + user lookup + Redis key must exist
OAuth access token /oauth/token (device-code flow) Same as team member, JWT carries typ: "oauth_access", clientId, scope A user acting through an OAuthClientApps client Gateway; scopes land on req.user.oauthScopes
App token appsAdd mutation (requires appsManage); random string on the app_tokens collection erxes-app-token header A machine principal app:<id>; isOwner when allowAllPermission Gateway: Apps.findOne({ token, status: "active" }); 401 on miss
CORS-bypass app token Same app_tokens record x-app-api-token header Origin-check bypass only Gateway CORS middleware; result cached in Redis 1h
Client Portal token clientPortalAdd / clientPortalChangeToken; JWT { clientPortalId } signed with JWT_TOKEN_SECRET, no expiry x-app-token header A ClientPortals record Gateway: JWT verify + portal lookup → req.clientPortal
Portal-user session clientPortalUser* auth mutations; JWT { userId, clientPortalId }, 1d access / 7d refresh defaults client-auth-token header or httpOnly cookie A CPUsers record bound to that portal Gateway, only after a valid x-app-token
POS config token Sales POS setup; posChooseConfig sets the cookie erxes-pos-token header or pos-config-token cookie A posclient Configs record posclient_api config middleware
POS user session posLogin pos-auth-token cookie A posclient PosUsers record posclient_api user middleware
Website check erxes-hosted verification erxes-core-token + erxes-core-website-url headers Synthetic user limited to showIntegrations, showKnowledgeBase, showScripts Gateway: POST to https://erxes.io/check-website must answer ok

Check order in userMiddleware: website check → app token → client portal (x-app-token, then client-auth-token) → Bearer/auth-token user JWT. All credential types can ride the same request, for example an app token plus a portal token.

Login example

mutation Login($email: String!, $password: String!) {
  login(email: $email, password: $password)
}

login returns the access token as a String and also sets the auth-token cookie (httpOnly; secure/sameSite depend on the request). Internally Users.createTokens (Users.ts) also mints a 7-day refresh JWT; logout clears the cookie and drops the Redis key, which revokes the token immediately rather than at JWT expiry.

curl --fail https://<gateway>/graphql \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer <token>" \
  --data '{"query":"query { currentUser { _id email } }"}'

SSO and OAuth

  • WorkOS SSO (SaaS only): loginWithGoogle and loginWithMagicLink call assertSaasEnvironment() and build WorkOS authorization/session URLs using WORKOS_API_KEY, WORKOS_PROJECT_ID, and CORE_DOMAIN. Magic-link mail goes out through SendGrid.
  • OAuth 2.0 device flow: Core API mounts /oauth/device/code, /oauth/device/approve, /oauth/device/deny, /oauth/token, and /oauth/revoke (oauth/routes.ts; device-code grant with per-IP rate limits). Client apps are managed through oauthClientAppsAdd/Edit/Revoke/Remove with public or confidential types and per-client token lifetimes.

What the gateway forwards

Subgraphs never see your raw credentials. The middleware rewrites them into signed-by-position headers read by erxes-api-shared context builders:

Header Contents
user Base64 JSON of the compacted user document (emailSignatures, customFieldsData, propertiesData, links, loginToken stripped)
userid The user _id (also set for app:<id> principals)
clientportal Base64 JSON of the resolved portal
cpuser Base64 JSON of the portal user
sessioncode Passed through to req.user.sessionCode

Never trust these headers at the edge

user, clientportal, and cpuser are unsigned; they are trustworthy only because the gateway sets them. Expose only the gateway publicly: direct access to a subgraph port bypasses authentication. JWT_TOKEN_SECRET must be identical across gateway, Core API, and plugins, and must be set explicitly in production. For portal JWTs the shared code throws in NODE_ENV=production rather than falling back to the 'SECRET' default.

Permissions

Team-member authorization runs through checkPermission / wrapPermission in erxes-api-shared/core-modules/permissions. Wrapped resolvers throw Login required (UNAUTHORIZED) with no context.user, then check the action list the plugin declares in meta/permissions.ts (canGroup → permission-group actions, isOwner bypass, checkOAuthScope for oauth_access tokens). Portal resolvers instead use wrapPublicResolver with forClientPortal/cpUserRequired flags, throwing Client portal required / Client portal user required. cp* operations and widget resolvers skip the team-member wrapper (wrapperConfig.skipPermission) and scope by portal/integration context; the cp prefix does not itself imply published-only or read-only data.

Edge cases

  • Sending two authorization or erxes-core-token headers is a 400, not a pick-one.
  • An expired, malformed, or Redis-evicted user token is ignored, not rejected: the request continues anonymous and wrapped resolvers fail with Login required. Only an unknown erxes-app-token gets an explicit 401.
  • WebSocket subscriptions authenticate from the auth-token cookie on the upgrade request; connectionParams are not consulted.
  • Set DEBUG_GATEWAY_AUTH=true on the gateway to get structured JSON logs for every auth decision on /graphql (which credential was present, which check failed, token hash).

Operate on one system.

Talk to our team about consolidating your stack onto erxes. 30-minute walkthrough, custom to your org.