CODITECT
CODITECT VTR
Visual Test Report
PASSED

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:

OptionNotes
No edit protectionAnyone with the link can overwrite any response — unacceptable
Email verificationHigh friction; contradicts no-account goal
Browser localStorage tokenNot shared across browsers/devices; acceptable trade-off
HttpOnly cookie tokenMore secure than localStorage; cleared on browser data wipe
Short-lived signed URLComplex to implement; overkill for this use case

Options evaluated for organizer authentication:

OptionNotes
OAuth (Google/GitHub)Requires organizer to have that account; excludes some users
Email magic linkBetter UX but adds email delivery dependency for organizer flow
6-digit PIN, shown once at creationSimple; no external dependency; acceptable for v1.0 scope
No organizer authClose/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.