Skip to content

feat(files): password, email-OTP, and SSO auth for public file shares#5140

Merged
TheodoreSpeaks merged 11 commits into
stagingfrom
feat/file-share-passwords
Jun 19, 2026
Merged

feat(files): password, email-OTP, and SSO auth for public file shares#5140
TheodoreSpeaks merged 11 commits into
stagingfrom
feat/file-share-passwords

Conversation

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator

Summary

  • Add optional access control to public file shares: Password, Email (OTP), and SSO, on top of Private / Anyone-with-link
  • Unify the gate with the deploy-as-chat machinery — extract validateChatAuth into a shared validateDeploymentAuth(resource, …, cookiePrefix); widen setDeploymentAuthCookie and the OTP store to a 'file' kind; reuse isEmailAllowed, encryptSecret, the global /sso flow, and the mailer/verification table
  • Schema: add auth_type, password (encrypted), allowed_emails to public_share (migrations 0243/0244, additive)
  • Public routes: gate metadata + content on a file_auth_{shareId} cookie (auth_required_* 401 sentinels); add POST /api/files/public/[token] (password), …/otp (request+verify), …/sso (eligibility)
  • ShareModal: access selector follows the deploy-modal AuthSelector precedent (ButtonGroup + conditional password / allowed-emails fields); SSO hidden unless NEXT_PUBLIC_SSO_ENABLED; link is reserved on open (client token, persisted on save) and shown live for non-private modes; removed the post-save link toast
  • OpenGraph: dynamic opengraph-image + generateMetadata for /f/[token] (rich for public shares, generic for password/email/SSO so the filename never leaks pre-auth; always noindex)
  • Reuse cleanups: shared GeneratedPasswordInput (deploy modal + share modal) and PublicFileAuthShell (the three gates)

Type of Change

  • New feature

Testing

  • 56 route/unit tests (password/email/SSO gates, OTP request+verify, eligibility, chat-auth regression)
  • In-process E2E against local DB: gate sentinels, cookie issue/verify, token reservation round-trip
  • bun run lint ✓ · check:api-validation:strict ✓ · check:migrations origin/staging ✓ (backward-compatible) · tsc

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 19, 2026 10:26pm

Request Review

@cursor

cursor Bot commented Jun 19, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches authentication, cookies, OTP, and public file byte serving across chat and files; auth-type binding and POST-only password minting reduce but do not eliminate regression risk in existing chat deploy auth.

Overview
Public file shares can be gated with password, email OTP, or SSO (not only private / anyone-with-link). Chat and file flows share a new validateDeploymentAuth helper with file_auth_{shareId} cookies, OTP storage for the 'file' kind, and validateAuthToken now checks auth type so cookies cannot be reused after a share mode change.

API & data: public_share gains auth_type, encrypted password, and allowed_emails. Metadata and content routes call the shared gate (401 auth_required_*); new routes handle password exchange, OTP send/verify, and SSO eligibility. Password POST on public shares is restricted to password mode so cookies cannot be minted on public links and later satisfy stricter modes.

Product & policy: Share modal uses an access ButtonGroup (password, allow-list, client-reserved token before save). /f/[token] shows auth shells until authorized; metadata/OG stay generic for protected shares so filenames do not leak pre-auth. Enterprise access control can allow-list file share auth types; share upsert validates auth config and returns 400 on ShareValidationError.

Refactors: GeneratedPasswordInput, case-insensitive isEmailAllowed, and broad route/unit test coverage.

Reviewed by Cursor Bugbot for commit ec5de63. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread apps/sim/app/f/[token]/opengraph-image.tsx
@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Password, Email-OTP, and SSO access controls to public file shares, unifying the gate with the existing deploy-as-chat auth machinery by extracting a shared validateDeploymentAuth helper. A migration (0244) adds auth_type, password, and allowed_emails columns to public_share; new routes and UI components wire up the three auth modes on top of the existing "anyone with link" flow.

  • Auth core: validateDeploymentAuth replaces the inline validateChatAuth logic; isEmailAllowed is now case-insensitive (lowercases both sides), fixing SSO session-email casing bugs across all callsites simultaneously.
  • Routes: GET/POST on the metadata endpoint and the content endpoint are gated by validateDeploymentAuth; dedicated /otp (POST request + PUT verify) and /sso (eligibility check) routes handle the email and SSO flows with IP/email rate limits.
  • UI: ShareModal gains a ButtonGroup access selector, conditional password/email fields, and a pre-reserved token so the shareable link is visible before first save; GeneratedPasswordInput is extracted as a shared component used by both the deploy modal and the share modal.

Confidence Score: 5/5

Safe to merge. The auth logic is cleanly shared, the migration is additive, and the file-name-leak issues from earlier review rounds have been addressed.

Cookie generation and validation both use the raw encrypted password from PublicShareRow, so the password-slot invalidation mechanism works as intended. All three new auth modes have IP rate limits on their write paths. The remaining comments are minor validation gaps and a UX inconsistency in the email countdown.

apps/sim/app/api/files/public/[token]/otp/route.ts — the PUT verify endpoint would benefit from an IP rate limit to match the POST request path.

Important Files Changed

Filename Overview
apps/sim/lib/core/security/deployment-auth.ts New shared auth gate for deployed resources; cookie check, rate-limited password comparison, email OTP redirect, and SSO session check are all correct and consistent with the chat implementation they replace.
apps/sim/app/api/files/public/[token]/otp/route.ts IP + email rate limits on the POST request path are solid; the PUT verify path lacks its own IP rate limit, and the OTP body schema allows non-digit 6-char strings.
apps/sim/app/api/files/public/[token]/route.ts GET metadata and POST password-auth endpoints correctly delegate to validateDeploymentAuth; rate limiting and cookie issuance look correct.
apps/sim/app/f/[token]/page.tsx Server-side auth gate correctly covers all four modes; generateMetadata now suppresses the filename for all non-public auth types; request-deduplication via React cache is a nice touch.
apps/sim/lib/core/security/deployment.ts isEmailAllowed is now case-insensitive (lowercases both sides), fixing the SSO session-email casing bug; setDeploymentAuthCookie widened to DeploymentAuthKind.
apps/sim/lib/public-shares/share-manager.ts resolveActiveShareByToken correctly returns PublicShareRow (with the encrypted password field) for auth validation; upsertFileShare handles all four auth modes with proper encryption and conflict update logic.
packages/db/migrations/0244_public_share_auth.sql Additive migration: auth_type (with default 'public'), password (nullable), and allowed_emails (nullable JSON) columns — no breaking schema changes.
apps/sim/app/workspace/[workspaceId]/files/components/share-modal/share-modal.tsx Access selector covers all four modes; token reservation before save is a nice UX touch; dirty state, password-missing, and email-missing guards correctly gate the Save button.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as Client
    participant P as /f/[token] page.tsx
    participant M as GET /api/files/public/[token]
    participant O as POST/PUT /otp
    participant S as POST /sso
    participant A as validateDeploymentAuth

    C->>P: Visit share URL
    P->>P: resolveShare(token)
    P->>P: renderAuthGate(share)

    alt "authType = public"
        P-->>C: PublicFileView
    else "authType = password"
        P-->>C: PublicFileAuth gate
        C->>M: POST password
        M->>A: validateDeploymentAuth
        A-->>M: authorized true
        M-->>C: Set file_auth cookie
        C->>P: router.refresh()
        P-->>C: PublicFileView
    else "authType = email"
        P-->>C: PublicFileEmailAuth
        C->>O: POST email request OTP
        O-->>C: 200 message
        C->>O: PUT email otp verify
        O-->>C: Set file_auth cookie
        C->>P: router.refresh()
        P-->>C: PublicFileView
    else "authType = sso"
        P->>P: getSession + isEmailAllowed
        alt not authorized
            P-->>C: PublicFileSSOAuth
            C->>S: POST email eligibility
            S-->>C: eligible true
            C->>C: redirect to SSO
            C->>P: return from SSO
            P-->>C: PublicFileView
        else authorized
            P-->>C: PublicFileView
        end
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as Client
    participant P as /f/[token] page.tsx
    participant M as GET /api/files/public/[token]
    participant O as POST/PUT /otp
    participant S as POST /sso
    participant A as validateDeploymentAuth

    C->>P: Visit share URL
    P->>P: resolveShare(token)
    P->>P: renderAuthGate(share)

    alt "authType = public"
        P-->>C: PublicFileView
    else "authType = password"
        P-->>C: PublicFileAuth gate
        C->>M: POST password
        M->>A: validateDeploymentAuth
        A-->>M: authorized true
        M-->>C: Set file_auth cookie
        C->>P: router.refresh()
        P-->>C: PublicFileView
    else "authType = email"
        P-->>C: PublicFileEmailAuth
        C->>O: POST email request OTP
        O-->>C: 200 message
        C->>O: PUT email otp verify
        O-->>C: Set file_auth cookie
        C->>P: router.refresh()
        P-->>C: PublicFileView
    else "authType = sso"
        P->>P: getSession + isEmailAllowed
        alt not authorized
            P-->>C: PublicFileSSOAuth
            C->>S: POST email eligibility
            S-->>C: eligible true
            C->>C: redirect to SSO
            C->>P: return from SSO
            P-->>C: PublicFileView
        else authorized
            P-->>C: PublicFileView
        end
    end
Loading

Reviews (4): Last reviewed commit: "fix(security): make isEmailAllowed case-..." | Re-trigger Greptile

Comment thread apps/sim/app/f/[token]/page.tsx
Comment thread apps/sim/app/f/[token]/opengraph-image.tsx Outdated
Comment thread apps/sim/lib/core/security/deployment-auth.ts Outdated
@TheodoreSpeaks TheodoreSpeaks requested a review from a team as a code owner June 19, 2026 20:13
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Addressed the review feedback in 7578702:

  • [Bugbot / Greptile P1 — filename leak in previews] The OG card + generateMetadata only suppressed the filename for password shares; email/sso fell through and leaked file.originalName pre-auth. Both now go generic for any non-public mode (authType !== 'public') — "Protected file" / "Authentication is required to view this file". noindex unchanged.
  • [Greptile P2 — stale JSDoc] Updated the validateDeploymentAuth doc comment: file shares now support all four modes (public/password/email/sso), not just public/password.

Also rebased onto latest staging (resolved the @sim/platform-authz rename + renumbered the migration to 0244_public_share_auth).

@greptile review

Comment thread apps/sim/app/api/files/public/[token]/otp/route.ts
Comment thread apps/sim/lib/core/security/deployment-auth.ts Outdated
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Round 2 of bot feedback addressed:

  • [Bugbot Medium — OTP email casing] The file OTP POST/PUT and SSO eligibility now normalize the email to trim().toLowerCase() before isEmailAllowed / storeOTP / getOTP, matching the lowercase allow-list and the rate-limit key. Added a regression test (mixed-case User@ACME.com → lowercased for matching + storage).
  • [Bugbot Low — "this chat" wording] The shared validateDeploymentAuth SSO-denied message is now generic ("…not authorized to access this resource") since it serves both chat and file shares. Updated the chat test assertion.

@greptile review

Comment thread apps/sim/app/f/[token]/page.tsx
Comment thread apps/sim/app/f/[token]/public-file-email-auth.tsx Outdated
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Round 3 (email casing, root-caused):

  • [Bugbot Medium — SSO session email casing] Root cause was that the shared isEmailAllowed compared raw-cased emails against the lowercase allow-list. Made isEmailAllowed case-insensitive (lowercases both sides) — this fixes the SSO session-email path in page.tsx and the metadata/content gates and chat in one place, instead of normalizing at each callsite.
  • [Bugbot Medium — client sends untrimmed email] The email + SSO gates now send email.trim().toLowerCase() to requestJson, consistent with server-side normalization.

Net: allow-list matching is now case-insensitive everywhere; OTP storage keys off the normalized email on both store and verify.

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Re the two Bugbot comments still showing on the latest commit (SSO session email casing @ page.tsx:87 and OTP email casing @ otp/route.ts): these are the earlier findings rolled forward by GitHub — their location anchors point at pre-82db6c2d0 commits. Both are already resolved on HEAD:

  • isEmailAllowed is now case-insensitive (lowercases the email and every allow-list entry), so the SSO session email (any IdP casing) and OTP allow-list matching work in page.tsx, the metadata/content gates, and chat — fixed centrally, not per-callsite.
  • The OTP route normalizes the email before storeOTP/getOTP (store + verify key consistently); the email/SSO client gates send the normalized email.

Added a direct unit test for isEmailAllowed (mixed-case exact + domain match) in 990bc03d3.

Comment thread apps/sim/lib/core/security/deployment-auth.ts
Comment thread apps/sim/lib/public-shares/share-manager.ts
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

[Bugbot High — "Public cookie bypasses email gate"] Fixed — good catch. Two layers:

  1. Root cause: validateAuthToken ignored the token's auth type, so an empty-slot public cookie satisfied an email/sso gate. It now binds the token to the auth type (storedType !== authType → reject), so a cookie minted under one mode can't authorize another after the share's auth type changes. Updated all 5 callers (file page + content/metadata gate, chat config GET, chat TTS, chat speech) to pass the current auth type.
  2. Direct fix: the password POST /api/files/public/[token] now rejects any non-password share with 400, so it never mints a file_auth cookie for a public share in the first place.

Added a route test (password POST on a public share → 400, no cookie) and updated the chat auth-token assertion.

Also rebased onto latest staging (migration renumbered to 0245_public_share_auth; api-validation baseline → 859).

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2120a46. Configure here.

Comment thread apps/sim/app/api/workspaces/[id]/files/[fileId]/share/route.ts
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

The 'Share upsert validation returns 500' comment is already resolved as of 32afa713b (Bugbot re-anchored the original to the latest commit — its line refs still point at the pre-fix code). On HEAD: upsertFileShare throws a typed ShareValidationError, and the PUT route catches it → 400 (route.ts:146-147). The remaining 500s are genuine server-error fallbacks. Also fixed alongside it: disabling a share no longer runs auth-config validation, so turning sharing off always succeeds.

@TheodoreSpeaks TheodoreSpeaks merged commit 7349bf4 into staging Jun 19, 2026
16 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the feat/file-share-passwords branch June 19, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant