greetings
 
You are here:

bbsengine6.auth to bbsengine6.bank authorization flow

Status: canonical. Updated 2026-09-04.

This document describes how a WebSocket connection authenticates via bbsengine6.auth and is then authorized to perform bank operations via bbsengine6.bank. The two modules cooperate through a shared in-process SessionManager; there is no per-message bearer token on bank operations, and the auth handler binds a session id to a moniker at auth login time so the bank handler can read that binding on every request.

Components

bbsengine6.auth (py/src/bbsengine6/auth/__init__.py)

Pure authorization policy. Exposes a single access() function that returns True/False for the four domain verbs:

Op Meaning
login Issue a fresh bearer token (moniker/password)
reconnect Rebind an existing token to a new WebSocket
refresh Rotate the live session's token
revoke Delete a token from the store

The actual login (password verification, token signing, store write) lives in bed/api/auth.py outside this repo. bbsengine6.auth.access exposes the per-op policy decisions only.

bbsengine6.session.SessionManager (py/src/bbsengine6/session/lib.py)

Process-local mapping:

session_id (int, monotonic) -> { "moniker": str, "is_sysop": bool }

Public methods:

Method Purpose
alloc_session_id() -> int Issued by the transport on connect
register_session(session_id, moniker, is_sysop=False) Called by the auth handler after login / reconnect / refresh
unregister_session(session_id) Called by the transport on disconnect
get_session(session_id) -> dict \| None Read-only lookup used by the bank handler

bbsengine6.bank (py/src/bbsengine6/bank/__init__.py)

Pure authorization policy for bank operations. Defines access() with ops balance, add, remove, history, pending, transfer, approve, reject, list_all. Reads message["claims"] first (HMAC- verified bearer); falls back to session.moniker / session.is_sysop when no claims are present.

bbsengine6.bank.api.handler (py/src/bbsengine6/bank/api/handler.py)

WebSocket dispatch entry point. Maps incoming wire message types to bank ops via OP_MAP, then delegates the per-op authorization decision to bbsengine6.bank.access. The session id is id(websocket) -- the same value the transport hands to SessionManager.alloc_session_id().

End-to-end sequence

  1. Client opens WebSocket to the transport.
  2. Transport calls SessionManager.alloc_session_id() -- returns an integer session id.
  3. Client sends {"type": "auth", "op": "login", "moniker": "alice", "password": "..."}.
  4. bed/api/auth.py (consumer of bbsengine6.auth.access) verifies credentials, asks bbsengine6.auth.access(args, "login", session=None, message=msg), signs a token, and calls SessionManager.register_session(session_id, "alice", is_sysop).
  5. Client sends {"type": "bank_balance", "moniker": "alice"}.
  6. Transport dispatches to BankServiceHandler.handle_message.
  7. Handler calls _check_auth(args, "bank_balance", session_id, message, sessions).
  8. _check_auth looks up session_id in the SessionManager, wraps the dict in SessionState, and calls bank_access(args, "balance", session=..., message=...).
  9. bank_access returns True (alice owns her own account); the handler proceeds with bank_service.get_balance("alice").
  10. On disconnect, transport calls router.unregister_session(session_id) and the bank router removes the SessionManager entry.

Wire-level mapping (bank)

WebSocket message type Op Required session state
bank_balance balance self moniker == target, or sysop
bank_add add self moniker == target, or sysop
bank_remove remove self moniker == target, or sysop
bank_history history self moniker == target, or sysop
bank_pending pending self moniker == target (defaults to caller), or sysop
bank_transfer_request transfer self moniker == from, or sysop
bank_transfer_approve approve valid transfer_id, optional responded_by match
bank_transfer_reject reject valid transfer_id, optional responded_by match
bank_list_all list_all sysop only

On denial the handler returns:

{"type": "error", "code": "forbidden", "message": "not authorized"}

Token-aware path

When bed/api/auth.py verifies a bearer token for a request, it stashes the decoded claims under message["claims"] before calling bank_access. bank_access then prefers claim-derived moniker / is_sysop over the in-memory session attributes because the claims are HMAC-verified. The handler does not need to decode the token itself; only the auth handler does.

Failure modes

Condition Effect on bank handler
WebSocket never logged in SessionManager.get_session returns None; bank_access(..., session=None) returns False; handler returns forbidden
auth login succeeded but register_session not called same as above
Session expired and transport did not unregister stale session may still resolve until explicit logout; the refresh path is the safe way to rebind
Token issued for session_id=A presented to WebSocket B bbsengine6.auth.access("refresh") denies; for bank ops, the in-memory session only reflects the current WebSocket, so a tampered session is invisible to bank_access

Files involved

File Role
py/src/bbsengine6/auth/__init__.py Policy (bbsengine6.auth.access)
py/src/bbsengine6/session/lib.py SessionManager
py/src/bbsengine6/bank/__init__.py Bank policy (bbsengine6.bank.access)
py/src/bbsengine6/bank/api/handler.py WS dispatch + _check_auth
py/src/bbsengine6/net/transport.py Session id lifecycle
py/tests/test_bank_access.py Unit tests for bank_access
py/tests/test_auth_access.py Unit tests for auth.access