ADR-002: Authentication Model
Source: ADR-002-auth-model.md
ADR-002: Authentication Model — No-Auth Participants, PIN-Protected Organizer
Date: 2026-04-09 | Amended: 2026-04-10 (v2.0 Cloudflare adaptation)
Status: Accepted (amended)
Deciders: Platform Engineering Lead, Security Lead, Product
CODITECT Classification: Architecture Decision Record · A6
Context
The scheduling tool must allow external participants — people outside the organizer's organization, without accounts, potentially on personal devices — to respond to a poll via a shared link. At the same time, the organizer needs a protected way to close the poll and export results without those actions being available to any participant who has the link.
We need to define the authentication model for both roles.
Constraints:
- Participant: Must require zero friction. No account creation. No OAuth. No email verification. Must work on mobile browsers without cookies being blocked.
- Organizer: Needs basic protection. Does not need enterprise SSO for v1.0. PIN complexity must be low enough for non-technical organizers.
- Edit flow: Participants should be able to correct their own response without re-entering a password.
- Privacy: No persistent identity — participants are display names only.
Options evaluated for participant edit protection:
| Option | Notes |
| No edit protection | Anyone with the link can overwrite any response — unacceptable |
| Email verification | High friction; contradicts no-account goal |
| Browser localStorage token | Not shared across browsers/devices; acceptable trade-off |
| HttpOnly cookie token | More secure than localStorage; cleared on browser data wipe |
| Short-lived signed URL | Complex to implement; overkill for this use case |
Options evaluated for organizer authentication:
| Option | Notes |
| OAuth (Google/GitHub) | Requires organizer to have that account; excludes some users |
| Email magic link | Better UX but adds email delivery dependency for organizer flow |
| 6-digit PIN, shown once at creation | Simple; no external dependency; acceptable for v1.0 scope |
| No organizer auth | Close/export available to any link holder — unacceptable |
Decision
Participant model: No authentication required to view or submit a response. A crypto.randomUUID() edit token is generated on first submission and stored in an HttpOnly, SameSite=Strict cookie scoped to the poll path. Subsequent edits from the same browser session are permitted via this cookie token. No token = no edit of that response (participant must submit as a new entry, triggering a duplicate name warning).
Organizer model: At poll creation, the system generates a 6-digit numeric PIN, displays it once, and returns the organizer management URL (/manage/{slug}). The PIN is hashed before storage. PIN entry is required to close the poll or export results. After 5 failed PIN attempts on a single poll, further attempts are locked for 15 minutes.
v2.0 amendment: PIN hashing changed from bcrypt (cost 10) to PBKDF2 (100k iterations, SHA-256) via Web Crypto API — bcrypt is unavailable in the Cloudflare Workers runtime. PIN lockout moved from Redis TTL key to a Durable Object (`PinRateLimiter`) for strong consistency across edge POPs. See TDD v2.0 §5.3.
Consequences
Positive:
- Zero-friction participant experience — share link, enter name, click slots, done
- No email delivery dependency for the core participant flow
- Organizer PIN is simple enough for non-technical users
- Edit token is HttpOnly and SameSite=Strict — not accessible to JS, not sent cross-origin
- PBKDF2 at 100k iterations makes PIN brute force computationally expensive even if DB is compromised
Negative:
- Participant edit token is lost if user clears cookies or switches browser/device — they cannot edit their prior response (they can submit a new one, which creates a duplicate name warning but does not block them)
- 6-digit PIN has 1,000,000 possible values — adequate with lockout, but not a high-entropy secret; organizer must not share the management URL publicly
- No multi-organizer support — single PIN per poll; v1.0 constraint accepted
Security notes:
- Edit token stored as PBKDF2 hash + salt in DB — raw token in cookie only, not in DB
- Management URL uses the same 128-bit slug as the participant URL — not guessable
- PIN lockout implemented via Durable Object with sliding-window counter — strong consistency, survives across edge locations
v2.0 amendment: Edit token hashing changed from bcrypt to PBKDF2 with explicit salt column (`edit_token_salt`). PIN lockout moved from Redis to Durable Object (`PinRateLimiter`) — eliminates KV eventual consistency bypass where an attacker could distribute attempts across multiple Cloudflare POPs.
Alternatives Rejected
OAuth for organizer: Adds identity provider dependency and excludes organizers who don't have a Google/GitHub account. Unnecessary complexity for v1.0.
Email magic link for organizer: Better long-term UX but adds email delivery as a dependency for the creation flow. Deferred to v1.1 when organizerEmail becomes required (currently optional).
localStorage for edit token: Accessible to JavaScript (XSS risk). HttpOnly cookie is strictly better with no UX trade-off.
Review Trigger
Revisit when enterprise SSO (SAML/OIDC) is required — likely when CODITECT embeds this tool in the main platform under enterprise accounts. At that point, organizer auth should delegate to the platform identity layer rather than the PIN model.