Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Identity and access

Explanation

Authentication and sessions

How tenant users sign in, how access and refresh tokens are issued, carried, rotated, and revoked, and which settings control them.

Cluster ERP signs tenant users in with a user name or company e-mail and a password, then issues a short-lived access token and a rotating refresh token. The web client receives both tokens as HttpOnly cookies; the desktop client and scripts receive them in the response body and send a Bearer header. This page explains the sign-in routes in modules/auth/login/routes.py, the token helpers in modules/auth/jwt_utils.py, the cookie transport in modules/auth/session_cookies.py, password hashing in modules/auth/password.py, revocation through auth_token_blacklist, and the permission memory in modules/auth/auth_cache.py. The operator console has its own sign-in; see operator console.

In short

  • Passwords are hashed with Argon2 through argon2.PasswordHasher, and a hash is upgraded when its parameters are outdated (src/cluster_erp/modules/auth/password.py:3-21, src/cluster_erp/modules/auth/users/services.py:298-308).
  • Access and refresh tokens are signed with the secret_key setting; each carries a unique jti and a type claim that verify_token checks (src/cluster_erp/modules/auth/jwt_utils.py:10-60, src/cluster_erp/modules/auth/jwt_utils.py:80-91).
  • The web client asks for cookie transport with the X-Token-Transport header; cookie-carried writes must also carry the X-Requested-With header (src/cluster_erp/modules/auth/session_cookies.py:31-39, src/cluster_erp/modules/auth/session_cookies.py:112-121).
  • Every refresh spends the presented refresh token and issues a new pair; the session end is fixed at sign-in and survives rotation (src/cluster_erp/modules/auth/login/routes.py:283-354).
  • Sign-out, a password change, and an anonymisation stamp last_logout_at, which invalidates every token issued earlier (src/cluster_erp/modules/auth/login/routes.py:423-424, src/cluster_erp/modules/auth/users/services.py:273-276, src/cluster_erp/modules/auth/privacy_handlers.py:96, src/cluster_erp/modules/auth/guards.py:153-165).

How it works

Sign-in and refresh flow

The sequence shows a web sign-in, a later request, and a refresh.

sequenceDiagram
 participant C as web client
 participant L as login route
 participant U as UserService
 participant G as get_current_user
 participant R as refresh route
 C->>L: POST /api/v1/auth/login with X-Token-Transport
 L->>L: _check_attempt_budget
 L->>U: get_by_identifier, authenticate
 U-->>L: AuthUser or None
 L-->>C: user and permissions, tokens in HttpOnly cookies
 C->>G: request with access cookie and X-Requested-With
 G->>G: verify_token, jti blacklist, last_logout_at
 C->>R: POST /api/v1/auth/refresh with refresh cookie
 R->>R: spend old jti, check last_logout_at
 R-->>C: new access and refresh cookies

In prose:

  1. The login route first checks the failed-attempt budget for the pair of client address and user name (src/cluster_erp/modules/auth/login/routes.py:129-131).
  2. get_by_identifier resolves the user name. When nothing matches and the identifier holds @, it tries the company e-mail, then the part before @ as a user name (src/cluster_erp/modules/auth/users/services.py:92-119).
  3. A locked account is refused before the password is checked (src/cluster_erp/modules/auth/login/routes.py:135-147).
  4. authenticate refuses inactive users, verifies the password, and re-hashes it when needed (src/cluster_erp/modules/auth/users/services.py:298-308).
  5. On success, the route clears the failure counter, stamps last_login_at at most once a minute, and loads role and permission codes (src/cluster_erp/modules/auth/login/routes.py:181-201).
  6. It issues the tokens. In multi-tenant mode both tokens carry the tenant key resolved for the request (src/cluster_erp/modules/auth/login/routes.py:203-228).
  7. A cookie client gets empty token fields in the body and the tokens in cookies; other clients get the tokens in the body (src/cluster_erp/modules/auth/login/routes.py:230-257).
  8. Every protected request runs get_current_user. It reads the credential, applies the CSRF rule, verifies the access token, requires a jti, checks the blacklist, loads the live user with roles, and compares iat with last_logout_at (src/cluster_erp/modules/auth/guards.py:85-167).
  9. The refresh route verifies the refresh token, blacklists its jti, refuses a second use after a short grace window, refuses tokens issued before the last sign-out, and issues a new pair with the same absolute session end (src/cluster_erp/modules/auth/login/routes.py:260-370).

Token contents

Token Claims Lifetime setting Source
Access sub, roles, type set to access, jti, iat, exp, optional tenant jwt_access_expire_minutes src/cluster_erp/modules/auth/jwt_utils.py:10-30
Refresh sub, type set to refresh, jti, iat, exp, sexp (absolute session end), persist, optional tenant jwt_refresh_expire_days, or jwt_session_hours without "remember me" src/cluster_erp/modules/auth/jwt_utils.py:33-60, src/cluster_erp/modules/auth/login/routes.py:215-228

read_tenant_claim reads the tenant claim of a signature-verified token and ignores expiry. It only routes a request to its tenant database; authentication still runs verify_token (src/cluster_erp/modules/auth/jwt_utils.py:63-77). See multi-tenancy.

set_session_cookies writes two cookies. Both are HttpOnly, SameSite=Strict, and Secure over HTTPS. The access cookie uses path / and takes the __Host- prefix over HTTPS; the refresh cookie is limited to the path /api/v1/auth (src/cluster_erp/modules/auth/session_cookies.py:46-54, src/cluster_erp/modules/auth/session_cookies.py:124-153). Without "remember me" (Keep me signed in on this device, webapp/i18n/en.json key auth.remember_me), the cookies carry no Max-Age and the refresh chain ends after jwt_session_hours (src/cluster_erp/modules/auth/login/routes.py:215-220). A well-formed Bearer header takes precedence over the access cookie (src/cluster_erp/modules/auth/session_cookies.py:63-75).

Revocation and sign-out

  • POST /api/v1/auth/logout blacklists the access token jti, and for a cookie session also the refresh token jti. It clears both cookies and stamps last_logout_at (src/cluster_erp/modules/auth/login/routes.py:373-431).
  • The blacklist table auth_token_blacklist holds token_jti (unique) and expires_at (src/cluster_erp/modules/auth/token_blacklist/models.py:11-23). The daily log retention pass deletes expired rows (src/cluster_erp/server/app.py:1168-1192, src/cluster_erp/server/app.py:1281).
  • POST /api/v1/auth/users/me/change-password verifies the current password and stamps last_logout_at (src/cluster_erp/modules/auth/users/routes.py:162-174, src/cluster_erp/modules/auth/users/services.py:255-278).

Failed attempts

Two mechanisms limit password guessing:

  • A failure budget per client address and user name, counted in the table auth_login_budget so that every worker process shares it. The limit is the setting login_rate_limit_per_minute; above it the route answers HTTP 429 with AUTH_TOO_MANY_ATTEMPTS (src/cluster_erp/modules/auth/login/routes.py:53-120). Successful sign-ins do not consume the budget (src/cluster_erp/modules/auth/login/routes.py:56-58).
  • An account lockout after a fixed number of consecutive failures, for a fixed period (_MAX_FAILED_ATTEMPTS, _LOCKOUT_MINUTES), stored in failed_login_count and locked_until (src/cluster_erp/modules/auth/login/routes.py:48-49, src/cluster_erp/modules/auth/login/routes.py:150-168, src/cluster_erp/modules/auth/users/models.py:67-76).

Permission memory

auth_cache keeps each user's permission codes and each permission's active policies in process memory for auth_cache_seconds seconds, keyed by database name (src/cluster_erp/modules/auth/auth_cache.py:41-55, src/cluster_erp/modules/auth/auth_cache.py:97-135, src/cluster_erp/server/app.py:1310). A version stamp from core_cache_versions, moved by database triggers, makes a change in one worker visible in all workers on the next request (src/cluster_erp/modules/auth/auth_cache.py:63-89). An after_flush listener clears the memory when roles, grants, user roles, permissions, or policies change (src/cluster_erp/modules/auth/auth_cache.py:153-184). The user row itself is not cached (src/cluster_erp/modules/auth/auth_cache.py:13-14).

First administrator

At start-up, _seed_permissions_from_metadata ensures the admin role and then calls ensure_bootstrap_admin, which creates one administrator only when the database has no user (src/cluster_erp/server/app.py:862-900, src/cluster_erp/modules/auth/users/services.py:412-453). The account takes its name, e-mail, and password from bootstrap_admin_username, bootstrap_admin_email, and bootstrap_admin_password or bootstrap_admin_password_hash (src/cluster_erp/config/settings.py:195-201). When no password is configured, a random one is generated and written to an owner-only file named by bootstrap_password_file; the log carries only the file path (src/cluster_erp/server/app.py:881, src/cluster_erp/server/app.py:914-923, src/cluster_erp/server/app.py:591-609). assign_admin_to_unassigned gives the admin role to role-less users only while no active administrator exists (src/cluster_erp/modules/auth/users/services.py:455-499); tests/test_admin_bootstrap.py pins this.

Why it is built this way

  • The module docstring of session_cookies.py gives the reasons for cookie transport: no browser storage protects a token from injected script, so the web client never sees a token, and the custom header adds a second CSRF barrier beside SameSite=Strict (src/cluster_erp/modules/auth/session_cookies.py:1-20).
  • The rotation comment states that the reuse grace window lets two frames of the same origin refresh at once (src/cluster_erp/modules/auth/login/routes.py:283-286). The absolute session end prevents rotation from extending a session (src/cluster_erp/modules/auth/jwt_utils.py:41-45).
  • The failure budget lives in the database because an in-process counter gave each worker its own budget (src/cluster_erp/modules/auth/login/routes.py:53-59).
  • The failed-attempt counter is committed before the error response because the session dependency rolls back on any exception (src/cluster_erp/modules/auth/login/routes.py:162-168).

Rules and boundaries

  • Other modules use only modules/auth/guards.py from the auth module; the comment names tests/test_architecture_boundaries.py as the test that holds the line (src/cluster_erp/modules/auth/guards.py:22-27).
  • The actor of change history comes from the same access token, read again by _resolve_actor_id in the session dependency (src/cluster_erp/server/dependencies.py:82-101). See database and sessions.
  • Token lifetimes, the session length, the reuse grace window, the failure budget, and the cache lifetime are settings: jwt_access_expire_minutes, jwt_refresh_expire_days, jwt_session_hours, jwt_refresh_reuse_grace_seconds, login_rate_limit_per_minute, and auth_cache_seconds (src/cluster_erp/config/settings.py:307-332). Their types and defaults are in the settings reference.

Interaction with other parts

  • The auth module answers GetUserById and GetUserPermissions on the EventBus for other modules (src/cluster_erp/modules/auth/module.py:81-82, src/cluster_erp/modules/auth/module.py:110-144).
  • In multi-tenant mode, the session dependency resolves the tenant before the login route runs, and the route stamps the tenant key into both tokens (src/cluster_erp/server/dependencies.py:104-157, src/cluster_erp/modules/auth/login/routes.py:203-213).
  • The web client side of sign-in is documented in boot, session, and API.

Limits and open points

  • Tenant users have no second factor in the code at this commit: verify_totp is called only from src/cluster_erp/server/operators.py:162. See second factor and secret storage.
  • A password policy beyond length is not determined from code. The schemas set lengths of 8 to 128 characters, and the setting password_min_length (src/cluster_erp/config/settings.py:327) is not read by any code at this commit (src/cluster_erp/modules/auth/users/schemas.py:12, src/cluster_erp/modules/auth/users/schemas.py:54-60).

Terms introduced

Term Code identifier Definition Turkish term Source
access token create_access_token A short-lived signed token that authenticates one user's requests. erişim belirteci src/cluster_erp/modules/auth/jwt_utils.py:10
refresh token create_refresh_token A signed token that is spent once to obtain a new token pair. yenileme belirteci src/cluster_erp/modules/auth/jwt_utils.py:33
token blacklist TokenBlacklist The table of revoked token ids that are refused until they expire. belirteç kara listesi src/cluster_erp/modules/auth/token_blacklist/models.py:11
cookie transport wants_cookie_transport The web sign-in mode in which the tokens travel only as HttpOnly cookies instead of in the response body. çerez taşıması src/cluster_erp/modules/auth/session_cookies.py:42
sign-in failure budget _check_attempt_budget The per-client and per-name limit on failed sign-ins within one minute. giriş hata bütçesi src/cluster_erp/modules/auth/login/routes.py:71

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13