POS Gateway - Authentication
Location: api/nextango/apps/pos_gateway/authentication.py
Last Updated: 2026-07-09
Overview
POS Gateway authentication issues an 8-hour JWT session per terminal, backed by a TerminalSession row and a Redis cache entry for fast revalidation. The PIN authentication logic itself is not duplicated here: it lives in users.services.UserAuthenticationService.authenticate_pin_login, and this module calls it directly rather than reimplementing PIN verification.
Four classes make up the authentication surface: PosGatewayAuthenticationClient (session lifecycle), PosGatewayAuthentication (the DRF authenticator used on /pos/* views), DevTokenAuthentication (a DEBUG-only bypass), and SafePosGatewayAuthentication (a non-raising variant used outside /pos/*).
PosGatewayAuthenticationClient
A lightweight client that delegates PIN verification to the users service and owns terminal session state.
authenticate_terminal(employee_id, pin_code, store_id, terminal_id): Calls UserAuthenticationService.authenticate_pin_login(). On success, creates or refreshes the TerminalSession and returns {success: True, token, expires_at, session_id, user, store, employee, session}. On failure, returns whatever failure shape the users service produced, or {success: False, message: "Authentication service unavailable"} if an exception was raised.
validate_session_token(token): Decodes and verifies the JWT (HS256, signed with settings.SECRET_KEY), then checks a session cache before falling back to a database lookup. Returns {valid: True, user_id, store_id, terminal_id, session_id, type: 'pos_session', ...} on success. On a JWT-valid-but-DB-expired session, returns {valid: False, soft_expired: True, ...} with the JWT claims still attached, so a caller such as the WebSocket middleware can refresh the row instead of rejecting outright. On a missing or explicitly revoked session, returns {valid: False, reason: 'Session not found or revoked'} without the soft-expired flag, since a revoked session should hard-fail rather than refresh.
logout_terminal(terminal_id): Sets the TerminalSession status to inactive, records logout_time, and clears the Redis cache entry. Returns True on success, False on any exception. JWTs are stateless and cannot be revoked directly; invalidation works by making the DB and cache lookups fail on the next validation.
PosGatewayAuthentication
The DRF authentication class wired into every protected /pos/* view (authentication_classes = [PosGatewayAuthentication]).
authenticate(request) reads the Authorization: Bearer <token> header, returns None if the header is absent or not a Bearer token (allowing other authenticators to try), and raises AuthenticationFailed if the token fails validate_session_token(). On success it returns (user, session_data), so views can read request.user and request.auth. authenticate_header() returns 'Bearer' for the WWW-Authenticate response header on 401.
DevTokenAuthentication
A DEBUG-only authenticator that recognizes Authorization: Bearer dev-token-* tokens emitted by the POS Electron app's dev-mode PIN screen when no real credentials are supplied. It picks the first active user with an employee or manager role and attaches a synthetic pos_session dict (type: 'pos_session', terminal_id: 'dev-terminal', store_id: 'dev-store') so downstream permission and throttle classes that check request.auth['type'] behave the same as with a real terminal session.
Returns None (falling through to the next authenticator) when settings.DEBUG is False, when the header doesn't match the dev-token- prefix, or when no eligible user exists to attach. Never active in production regardless of header content.
SafePosGatewayAuthentication
A non-raising variant of PosGatewayAuthentication, intended for endpoints outside /pos/* that rely on Django's global DEFAULT_AUTHENTICATION_CLASSES chain rather than an explicit authentication_classes list. Where PosGatewayAuthentication.authenticate() raises AuthenticationFailed on an invalid Bearer token, this class returns None on every failure path (missing header, non-pos_session token, expired or invalid JWT, unresolvable user), so a POS Bearer token doesn't hijack or break authentication on endpoints that also serve other client types. It calls validate_session_token() directly rather than going through PosGatewayAuthentication.authenticate(), which also avoids the per-request error log line the raising path emits on every invalid Bearer attempt.
Authentication Flow
Login: the terminal POSTs to /pos/auth/terminal-login/ with employee_id, pin_code, store_id, terminal_id. PosGatewayAuthenticationClient.authenticate_terminal() verifies the PIN through the users service, issues an 8-hour JWT, and creates or updates the TerminalSession row and its Redis cache entry.
Per-request validation: PosGatewayAuthentication.authenticate() extracts the Bearer token and calls validate_session_token(), which checks Redis first and falls back to the database. A cache hit updates nothing further; a database fallback also calls session.update_activity() to bump last_activity.
Logout: the terminal POSTs to /pos/auth/terminal-logout/ with a valid session token. logout_terminal() marks the session inactive and clears the cache entry.
WebSocket auth follows a parallel but separate path through PosGatewaySessionMiddleware (see Consumers), since a browser WebSocket handshake cannot set a custom Authorization header; the JWT arrives as a ?token= query parameter instead.
Security Notes
The JWT payload carries user_id, store_id, terminal_id, iat, exp, and a type: 'pos_session' discriminator, signed with HMAC-SHA256 using settings.SECRET_KEY. Sessions last 8 hours, matching a typical shift. Because the session is also tracked in the database, logout and revocation work even though the JWT itself is stateless. A valid signature is necessary but not sufficient; the database and cache lookups are what actually gate access after logout.
Related Documentation
- Models: TerminalSession
- Views: login/logout endpoints
- Consumers: WebSocket authentication via
PosGatewaySessionMiddleware - Users Services: PIN authentication logic (
UserAuthenticationService.authenticate_pin_login)