Overview

Ananas GDS has two distinct authentication contexts that use different mechanisms and serve different audiences:

Context Who Uses It Mechanism Expiry
Web app (frontend) Accommodation and TO users logging in via the browser DRF token stored in accessToken cookie, with custom session layer Inactivity-based (30 min or 7 days)
External API (partners) Tour operator integration systems calling the public API Static API token passed in the URL path None — active until manually revoked

The underlying token type for web app sessions is a standard DRF authtoken.Token — an opaque string stored in the database. Despite the frontend file being named jwtInterceptor.js, no JWT tokens are used anywhere in the system. The JWT library is installed but unused.

Login Flow

All web app logins go through a single endpoint. The flow involves several sequential checks before a session is created.

  1. POST to api/auth/token/login/

    Submit credentials as JSON: {"email": "...", "password": "..."}

  2. IP ban check

    The server checks the file-based cache for an active ban on the requesting IP. If a ban is in effect, the request is rejected immediately with a 429 response and an X-IP-Banned header indicating the remaining ban duration.

  3. Account approval check

    The server looks up the user by email and verifies that approval_status == "approved". Pending, rejected, and paused accounts are rejected with a 403 response.

  4. Password verification

    Standard Django password check. A failure increments the IP attempt counter. At 13 failed attempts the IP is banned for 120 seconds.

  5. Concurrent session check

    The server counts active UserSession records for the account. If 2 sessions already exist, the login is rejected with 429 too_many_sessions and a security alert is sent to the account holder.

  6. Session creation

    A DRF Token is created (or retrieved if one exists) and a UserSession record is written with the client IP, parsed user agent, and whether "Remember Me" was selected.

  7. Response returned

    The server responds with {"auth_token": "abc123..."}.

  8. Frontend stores the token

    The Vue app writes the token to the accessToken cookie. The JWT interceptor picks it up on all subsequent requests.

Response Codes

Code Meaning
200 Success — token returned in response body
400 Invalid credentials (wrong email or password)
403 Account not approved, inactive, rejected, or paused
429 IP currently banned due to failed attempts, or concurrent session limit reached

Session Model

Each active login is represented by a UserSession record. This model extends DRF's basic token auth with metadata and expiry controls.

PYTHON
class UserSession(models.Model):
    token        # OneToOneField -> authtoken.Token (CASCADE on delete)
    user         # ForeignKey -> User
    ip           # CharField — client IP address at login time
    device_info  # CharField — parsed user agent string
    is_extended  # BooleanField — True if "Remember Me" was selected
    created_at   # DateTimeField — session creation timestamp
    last_active  # DateTimeField — updated on each request (throttled)

When a Token is deleted (e.g., on logout), its associated UserSession is automatically removed via the CASCADE constraint. There is no orphaned session data.

Session Expiry

Sessions expire based on inactivity, not on absolute time. The inactivity window depends on whether the user selected "Remember Me" at login:

Session Type Inactivity Timeout Condition
Standard 30 minutes is_extended = False
Extended ("Remember Me") 7 days is_extended = True

Inactivity is measured from last_active. Every authenticated request touches this field (see throttling note below). If last_active is older than the allowed window when a request arrives, the session is considered expired: the UserSession and its Token are deleted, and a 401 with session_expired is returned.

Per-Request Authentication

Every API request that requires authentication passes through the custom SessionTokenAuthentication class. This class replaces the standard DRF TokenAuthentication and adds session validation on every call.

  1. Extract the token

    The Authorization: Token <key> header is read from the request. If absent or malformed, the request proceeds as anonymous (and will fail any IsAuthenticated permission check).

  2. Look up the Token record

    The token string is looked up in the authtoken_token table. If not found, a 401 is returned.

  3. Check for a UserSession

    The corresponding UserSession is looked up. If none exists (e.g., the token was created outside the normal login flow), a 401 session_invalid is returned.

  4. Check inactivity window

    The current time is compared against last_active + inactivity_window. If the session has expired, it is deleted and a 401 session_expired is returned.

  5. Touch last_active

    To avoid a database write on every single request, last_active is updated at most once every 5 minutes per session. If the last touch was less than 5 minutes ago, the update is skipped.

Session Heartbeat

The frontend actively keeps sessions alive for users who are working in the app. The heartbeat mechanism prevents the 30-minute inactivity timer from firing while a user has the app open and is actively using it.

Parameter Value
Endpoint GET /api/auth/check-token/
Interval Every 10 minutes
Condition Only fires if the user has been active in the last 15 minutes
Effect Passes through SessionTokenAuthentication, touching last_active

If the user closes the browser or leaves the tab idle for more than 15 minutes, the heartbeat stops firing. The session will then expire naturally after the inactivity window elapses.

Concurrent Session Limits

Each account is limited to 2 active sessions at any one time. This limit is enforced at login time, not during active sessions.

Scenario Outcome
First login (0 existing sessions) Session created normally
Second login (1 existing session) Session created; account owner notified by email and in-app notification
Third login attempt (2 existing sessions) Login rejected with 429 too_many_sessions; security alert sent to account holder

To log in on a new device when at the limit, you must first log out from one of the existing sessions, or wait for an inactive session to expire naturally.

Logout

Logging out destroys the session token server-side. The process is:

  1. POST to api/auth/token/destroy/

    The request must include the Authorization: Token <key> header.

  2. Token deleted

    The DRF Token record is deleted from the database.

  3. Session cascade-deleted

    The associated UserSession is automatically removed via the CASCADE constraint on the token field.

  4. Frontend cleanup

    The Vue app clears the accessToken cookie, resets the auth and account Pinia stores, and redirects to /login.

After logout, the token is invalid immediately. Any in-flight requests that were made with the old token after the DELETE completes will receive a 401.

API Token Authentication (Partners)

External integrations — tour operators and travel agents calling the public API — use a separate token system that is entirely distinct from the web app session tokens.

These static API tokens are:

  • Created by accommodation providers in the Dev Tools > API Tokens panel
  • Scoped to a specific partner contract
  • Passed in the URL path, not in an Authorization header
  • Active indefinitely until manually revoked by the accommodation provider
  • Tracked by the APIStats model for per-token usage analytics
HTTP
# Fetch fact sheet data for a property
GET /api/v1/facts/your-api-token-here/

# Fetch property photos
GET /api/v1/photos/your-api-token-here/

# Fetch stop sale calendar
GET /api/v1/stop-sale/your-api-token-here/

No Authorization header is needed for these endpoints. The token in the URL path is the credential. No session is created or required.

No Session Required for Public API

Public API calls do not go through SessionTokenAuthentication. They are validated by a separate token lookup that checks the API token against the partner contract and property ownership. Session expiry rules and concurrent session limits do not apply.

IP Ban System

To limit brute-force login attempts, the platform implements an IP-level ban using the file-based cache.

Parameter Value
Failed attempts before ban 13
Ban duration 120 seconds
Attempt counter cache key login_attempts_{ip}
Ban cache key ip_ban_{ip}
Ban TTL 120 seconds
Response when banned HTTP 429 with X-IP-Banned header containing remaining seconds

On each failed login attempt the counter increments. When it reaches 13, both the counter and a ban flag are written to the cache. All subsequent login requests from that IP within the 120-second window are rejected immediately without reaching the password check.

File-Based Cache Limitations

The file-based cache used in production means IP bans do not survive a server restart. Additionally, on multi-worker setups each worker process has its own cache file — the effective ban threshold is multiplied by the number of workers. In a 2-worker setup, up to 26 failed attempts may be possible before a ban takes effect.

Security Notes

The following table documents known security characteristics and trade-offs in the current implementation. These are intentional or accepted constraints, not untracked bugs.

Concern Status Notes
Token storage Cookie (JS-readable) The accessToken cookie is not HttpOnly — JavaScript can read it. XSS vulnerabilities could expose the session token. Mitigated by CSP and Vuetify's safe rendering practices, but not eliminated.
CSRF Not required for token auth The Authorization header cannot be set by cross-origin forms (CORS blocks it), so CSRF tokens are not needed for API endpoints that use token auth.
Approval status re-check Login-time only If an account is suspended (paused) after the user has already logged in, their existing session remains valid until it expires naturally. There is no real-time revocation of active sessions on status change.
Concurrent session race Low risk Two simultaneous login requests from different devices could theoretically both pass the concurrent session check before either creates a session record, resulting in 3 active sessions briefly. The window is millisecond-scale and the alert system would fire for the second login regardless.
Token expiry Inactivity-based only There is no absolute maximum session lifetime. A user who sends a request at least once every 30 minutes (or 7 days on extended) can maintain a session indefinitely. There is no forced re-authentication after N days.
IP ban bypass Possible via IP rotation The ban is keyed on IP address. Attackers with access to many IPs (e.g., proxies) can bypass it by rotating. The 13-attempt threshold per IP is the effective rate limit.

DRF Authentication Class Ordering

A known configuration requirement: in DRF settings, TokenAuthentication (or SessionTokenAuthentication) must appear before SessionAuthentication in the DEFAULT_AUTHENTICATION_CLASSES list. Reversing this order causes 401 errors on token-authenticated endpoints because DRF tries the session authenticator first, fails, and does not fall through correctly.

PYTHON
# settings.py — CORRECT ORDER
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'authapp.authentication.SessionTokenAuthentication',  # must be first
        'rest_framework.authentication.SessionAuthentication',
    ],
    ...
}