Every time someone reads a Dangercorn app and sees the auth setup — one password, stored as an env var, checked on login, session cookie after — they send me some variant of the same message. "You should be using OAuth. You should be using proper password hashing. You should be using JWTs. You should be using per-user accounts."
For a multi-tenant SaaS, all of those are correct. For the apps in our catalog, most of them are the wrong answer. Here's the threat model analysis.
The Assumed Deployment
Almost all Dangercorn apps are deployed in one of three shapes:
- Single-user self-host. The user runs the app on their laptop, Raspberry Pi, or personal VPS. They are the only person who uses it.
- Small-team self-host. A family, a small business, a church. Everyone shares access because they're already sharing the physical space.
- Hosted tier. We run it for them. This one does use real per-user auth.
The single-password pattern serves cases 1 and 2. Case 3 is a different codepath.
What Single-Password Actually Looks Like
On app startup, AUTH_PASSWORD is read from the environment. The app hashes it with bcrypt (12 rounds) and stores the hash in memory. Login form submits a password; we bcrypt-check against the stored hash. On success, we set a signed session cookie.
AUTH_PASSWORD_HASH = bcrypt.hashpw(
os.environ["AUTH_PASSWORD"].encode(),
bcrypt.gensalt(rounds=12)
)
@app.route("/login", methods=["POST"])
def login():
password = request.form["password"].encode()
if bcrypt.checkpw(password, AUTH_PASSWORD_HASH):
session["authenticated"] = True
session.permanent = True
return redirect(url_for("index"))
flash("Wrong password", "error")
return redirect(url_for("login"))
Session cookies are signed with FLASK_SECRET_KEY and marked HttpOnly + SameSite=Lax + Secure (if behind HTTPS). Default session lifetime is 30 days.
The Threat Model
Let's walk through what actually needs defending against in the self-host case.
Network attackers. Someone sniffs traffic between the user and the server. Mitigated by HTTPS (your responsibility as a self-hoster; we recommend Caddy or Cloudflare Tunnel). Session cookies can't be snooped over HTTPS.
Password brute-force. Someone hammers the login endpoint. Mitigated by rate limiting (5 attempts per IP per 15 minutes) and a strong default password policy (minimum 12 chars; we enforce on initial config).
Credential-stealing malware. If the user's machine is compromised, the attacker gets the cookie and can impersonate. True, and not specific to single-password auth — OAuth tokens, JWT bearer tokens, all have the same property.
Compromised third-party services. If the user reuses the password somewhere that gets breached, credential stuffing. Mitigated by advising them to use a unique password; not specific to single-password auth.
Server compromise. If an attacker gets root on the self-host server, they have the env var, the hash, and the data. Defense-in-depth is on the user's infrastructure side.
None of these are worse under single-password than under per-user auth. Some are actively better (one place to rotate the password; no user-enumeration vulnerabilities).
What Single-Password Doesn't Defend Against
Insider abuse. If multiple people have the single password, any one of them can use the tool and everyone sees the same activity log (there's no per-user audit trail, only per-session-cookie if we bother).
For a family using familyhub, this is fine. You're not worried about the 9-year-old forging grocery list additions. For a 10-person team using meetingmind, this starts to be a problem — who logged which decision?
meetingmind's answer is: Hosted Pro tier has per-user accounts. Self-host has single-password. Teams that need individual accountability graduate to the hosted tier. Teams that don't, don't pay for complexity they don't need.
The Tokenized URL Pattern
For apps with a "staff" concept (e.g., shiftfill with hourly workers), we don't even use passwords for staff. We use tokenized URLs.
The manager logs in with the single password. They generate a bookmarkable URL for each staff member. The URL has a token embedded in the query string. That token identifies the staffer read-only (or with specific limited write access, like "claim a swap offer").
def staff_token(staff_id):
payload = {"staff_id": staff_id, "iat": int(time.time())}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
@app.route("/staff/<string:token>")
def staff_page(token):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except jwt.InvalidTokenError:
abort(404)
staffer = Staffer.get(payload["staff_id"])
return render_template("staff_page.html", staffer=staffer)
No login. No password reset. No account creation. The staffer bookmarks the URL on their phone. If they leave, the manager rotates the token on that staff record.
This pattern is under-used in the SaaS world because it doesn't fit the "every user has an account" mental model. For hourly workers, gym members, occasional customers, and family, it's the right friction level.
What Changes on Hosted
Hosted tiers have multiple customers on shared infrastructure, which requires real per-user auth: bcrypt-hashed per-user passwords, password reset flows, email verification, optional TOTP 2FA. Some apps add SSO (SAML, OIDC) on the top tier for enterprise customers.
The code path is different but sits in the same codebase behind a feature flag. Self-host runs with AUTH_MODE=single_password. Hosted runs with AUTH_MODE=multi_user. Most of the app logic is the same either way; authentication is one of the few places that branches.
What I'd Tell a Security Reviewer
If you're auditing a Dangercorn self-host deployment and your checklist says "per-user auth required," the checklist is wrong for this deployment shape. The checklist was written for multi-tenant SaaS.
The real audit checklist for single-user self-host is: HTTPS on the entry point, strong password policy, rate-limited login, bcrypt-hashed secret, signed session cookies, audit logging for sensitive operations, host-level security (OS patching, firewall). All of which we do or document.
The right auth is the auth that matches your deployment. Generalizing 'OAuth everywhere' from multi-tenant consumer apps to single-user self-hosted tools is a category error.
Related
The template walkthrough. Optional Stripe wiring. The self-host pitch. Examples: familyhub (family trust boundary), shiftfill (tokenized URLs for staff), dentaldesk (where single-password stops being appropriate).
What This Doesn't Apply To
Don't generalize this to multi-tenant consumer SaaS, regulated industries with audit requirements, or any product where multiple users need individual accountability. The single-password pattern is specifically for single-user or trust-boundary-defined-by-physical-space deployments. Apply it elsewhere and you'll create real security debt.
The lesson is to match auth to deployment, not to default to whatever the loudest internet voice recommends. Sometimes that's OAuth, sometimes single password, sometimes tokenized URLs. The right answer is context-specific.