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):
loginWithGoogleandloginWithMagicLinkcallassertSaasEnvironment()and build WorkOS authorization/session URLs usingWORKOS_API_KEY,WORKOS_PROJECT_ID, andCORE_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 throughoauthClientAppsAdd/Edit/Revoke/Removewithpublicorconfidentialtypes 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
authorizationorerxes-core-tokenheaders is a400, 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 unknownerxes-app-tokengets an explicit401. - WebSocket subscriptions authenticate from the
auth-tokencookie on the upgrade request;connectionParamsare not consulted. - Set
DEBUG_GATEWAY_AUTH=trueon the gateway to get structured JSON logs for every auth decision on/graphql(which credential was present, which check failed, token hash).