Sign in
Sign in — Technical Spec
app/im2.py, template app/templates/login.html. Interim auth until Slack OAuth credentials exist (Jeff's decision, 2026-08-23).
Routes
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /login | none | Renders login.html with user=None (so base.html renders no header/nav). ?error=1 shows the one generic failure banner. |
| POST | /login | none | Form email, pin. |
| GET | /logout | none | delete_cookie("im2"), 303 → /login. |
| GET | /healthz | none | Unauthenticated: returns {"ok":true,"active_items":N}. |
Everything else depends on current_user.
Credential check
Lookup: select username, pin_hash from app_users where lower(email) = lower(%s) and active.
check_pin()= pbkdf2-hmac-sha256, 120,000 iterations, per-PIN 8-byte hex salt, storedsalt$hexinapp_users.pin_hash, compared withhmac.compare_digest.- No row, inactive user, NULL/malformed
pin_hash, or wrong PIN → 303 →/login?error=1. Same response in every case (no user enumeration). - Success → 303 →
/with the session cookie set.
Session
- Cookie
im2, value =itsdangerous.URLSafeSerializer(IM2_SECRET, "im2-session").dumps(username). Signed, not encrypted, and not timestamped — the username is readable by anyone holding the cookie; the signature is the only integrity control. - Flags:
httponly=True,samesite="lax",max_age=60*60*24*30(30 days, absolute — nothing refreshes it).secureis not set. IM2_SECRETenv var. If unset,secrets.token_hex(32)is generated per process — every restart (and every worker, if there are several) invalidates all cookies.current_user(request): no cookie → 401 "login required";BadSignature→ 401 "bad session"; signature valid butapp_usersrow missing oractive = false→ 401 "unknown user". A DB hit per request, uncached.- Global
HTTPExceptionhandler: 401 → 303 redirect to/login; every other status →JSONResponse({"detail": …}). So a 403 on an API call returns JSON, which is what the screens' fetch handlers display. - Revocation levers: deactivate the user (
app_users.active = false), or rotateIM2_SECRET(logs everybody out).
Known limits / security weaknesses (flagged plainly)
- No lockout and no rate limiting anywhere. There is no failed-attempt counter, no delay, no IP throttle, no captcha. Combined with a 4-digit numeric PIN and a known email pattern (
first.last@solalt.com), the credential space is ~10,000 guesses; pbkdf2 at 120k iterations is the only brake, and it costs the server, not the attacker. This is the single biggest security gap in the app. Cheapest mitigations: a per-email failed-attempt table with a lockout, and a 6-digit minimum PIN. - No failed-login logging. Nothing is written on a bad attempt, so a brute-force attempt would leave no trace in
audit_logat all. secure=Trueis not set on the cookie, so it will be sent over plain HTTP if the app is ever reachable that way (any reverse-proxy misconfiguration downgrades to a clear-text session).URLSafeSerializeris untimestamped — cookie lifetime is enforced only by the browser'smax_age. A copied cookie value is valid forever while the user stays active and the secret is unchanged.URLSafeTimedSerializer+ amax_ageonloads()would fix this.- Setting or regenerating a PIN does not invalidate existing sessions; a compromised session survives the password reset that was meant to close it.
- No CSRF protection on any POST/
/api/*route.samesite="lax"blocks cross-site form posts in current browsers, which is the only reason this is not already exploitable. IM2_SECRETdefaulting to a random per-process value means a multi-worker deployment without the env var set produces random 401 →/loginbounces (each worker rejects the others' cookies) rather than a clean failure.- Only an admin can set a PIN; there is no self-service reset, so a lost PIN is a Slack message to Alex or Dave. Deliberate for now — worth noting as an operational load, not a defect.
- The whole
/docstree (docs.py::install) has nocurrent_userdependency — its docstring says "readable by anyone signed in", but in fact any Operator Help or Technical Spec page, including this one, is readable without a session. The tech pages name tables, columns and route paths. /healthzis unauthenticated and discloses the active item count. Harmless, but it is a public endpoint on this host.
CSRF protection (added 2026-09-06)
same_origin_only, an HTTP middleware in im2.py mounted immediately after the app is created, rejects any non-GET/HEAD/OPTIONS request whose Origin (or, for plain form posts, Referer) host does not equal the request Host, with a 403 and a plain operator message. A request carrying no session cookie and no origin is allowed through, so health probes and the route verifier still work. Blocked attempts are logged with method, path, origin and referer.
Chosen over per-form CSRF tokens deliberately: every write in the app is a POST from one of our own pages, so an origin check gives the same protection without a hidden token in ~40 templates that would drift out of step with them. The session cookie is also samesite="strict" now, so most browsers will not send it on a cross-site request at all. Verified live: a POST with Origin: https://evil.com and a POST carrying a cookie but no origin are both refused; the real login POST still works.