Skip to content

Require verified sign-in and queue verification delivery - #67

Merged
Eoic merged 2 commits into
developmentfrom
agent/issue-22
Nov 6, 2026
Merged

Eoic merged 2 commits into
developmentfrom
agent/issue-22

Conversation

@Eoic

@Eoic Eoic commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Registration now commits a pending account and a durable verification email job without creating a session. Password login, Google exchange, refresh, shared API authentication and explicit deletion recovery require evidence for the current primary email. Existing verified sign-in retains the AuthTokens response and refresh rotation.

Closes #22. Implements TASK-AUTH-02 for FR-1_1, FR-1_2 and FR-1_3, following the accepted TASK-AUTH-01 contract merged in PapyrusReader/docs#93. Client integration remains PapyrusReader/client#60.

Behavior

  • Registration returns HTTP 201 PendingVerificationResponse. Missing SMTP/landing configuration or a failed queue transaction returns VERIFICATION_UNAVAILABLE with Retry-After: 60 and rolls back pending account data.
  • Email links target the configured app base path at /verify-email. Tokens are hashed for lookup, bound to the exact email, single-use and valid for 24 hours. Confirming one link consumes all outstanding links for that address without signing in or cancelling deletion.
  • Registration, resend and automatic Google mail share an atomic PostgreSQL recipient cooldown. Resend responses stay identical for unknown, verified, disabled and pending accounts. Public auth endpoint limits are capped at five attempts per minute per IP. Auth responses use no-store.
  • The separate verification outbox commits with the token, leases work across workers, retries SMTP failures with backoff, and removes secrets after delivery, expiry, verification or deletion. It does not alter lifecycle-notice ordering. Migration removes inherited/default non-owner grants from the new queue tables, including the PowerSync setup's default SELECT grants. SQL parameters and OAuth callback queries are excluded from logs.
  • Google evidence requires a Gmail address or email_verified=true with a nonempty signed hd. The subject remains the identity key. Primary-email matching, existing-account conflicts and explicit linking remain enforced. Pending handoffs are consumed with HTTP 403 even if verification completes before exchange. Verified Google sign-in works without SMTP.
  • A legacy unverified refresh revokes all account sessions and commits that revocation before returning 403. Shared API guards block old JWTs for media, sync and PowerSync renewal. Logout remains available. Recovery retains its authorization and deletion state when verification is missing. Owner-before-credential locking covers refresh, verification, recovery and code consumption.

Database model

The account stores primary_email, email_verified_at and email_verification_source. The public email_verified boolean is derived, with a database constraint requiring complete, valid evidence. There is no second account email, persisted verification boolean or duplicated provider subject. Google subjects and validated hosted-domain claims remain on the linked identity records.

Existing-email changes use one owner-locked transaction to clear evidence, invalidate email tokens and OAuth/recovery codes, cancel queued verification mail, and revoke sessions. Normalized no-op changes retain access. Rollback preserves the original state, and changing away and back cannot revive old links. Credential revocation is shared with deletion/recovery.

Verification delivery rows store only the token reference, deliverable secret and retry state. Owner, recipient and expiry come from the token row. Token address binding, the shared recipient cooldown, and the separate lifecycle-notice queue remain necessary for their distinct security and ordering rules.

Migration and activation audit

d1e2f3456789 follows c0d1e2f34567 and adds verification evidence, token address binding, hosted-domain claims, cooldowns and the outbox. The follow-up d2f3456789ab removes the redundant fields and adds the evidence constraint. The prior migration remains intact so databases that already applied it can upgrade safely. Both preserve user IDs, password hashes, linked identities, libraries, reading positions and pending deliveries.

The exact predicates retained as evidence are password-only Papyrus verification or a matching retained Google Gmail identity. The combined eligibility predicate is:

primary_email_verified = true AND primary_email IS NOT NULL
AND (
  (
    EXISTS (SELECT 1 FROM password_credentials p WHERE p.user_id = users.user_id)
    AND NOT EXISTS (SELECT 1 FROM user_identities i WHERE i.user_id = users.user_id)
  )
  OR EXISTS (
    SELECT 1 FROM user_identities i WHERE i.user_id = users.user_id
    AND i.provider = 'google' AND i.provider_subject <> ''
    AND i.email_at_provider = users.primary_email
    AND i.email_at_provider LIKE '%@gmail.com'
  )
)

Google subjects remain on linked identity records, and account evidence refers to the current primary email. Workspace authority is never inferred from old provider booleans or domain strings. Previously true accounts outside this predicate require fresh proof. Their boolean is cleared and their sessions are revoked. Existing unverified sessions are also revoked, and old unbound verification tokens are consumed. The migration logs the affected previously-verified account count without account identifiers. The seeded migration regression reports 2 affected accounts out of 5, preserves the password-only and attributable Gmail verified accounts/sessions and all credentials, identities and library state. A fresh isolated migration database reports 0. These are local validation counts, not a production audit. Before activation, record the actual deployment count using SELECT count(*) FROM users WHERE primary_email_verified = true AND NOT (<predicate above>), and retain the migration log. Downgrade is deliberately refused because rollback must preserve enforcement and evidence.

Validation

  • Workspace tools/papyrus check server passed: Ruff lint, formatting and strict Mypy. The CLI functions were directed at the isolated server worktree without changing workspace component paths.
  • Full backend suite: 587 passed, 2 provider smoke tests deselected on the distinct local papyrus_auth22_test database. Database suites ran serially under the workspace test lock.
  • The full run includes the specified auth route/service tests, queue ACL and populated migration regressions, and new email-change rollback, concurrency, credential invalidation and evidence-constraint regressions.
  • The original clean migration bootstrap passed. Upgrading an existing d1e2f3456789 database to d2f3456789ab and alembic check passed with no schema drift. The populated regression preserves verification timestamps, queued secrets/expiry/retry state and durable library data, clears stale evidence, and checks queue access restrictions.
  • Deterministic OpenAPI export and --check passed. CI now publishes server-openapi-<sha> using the existing exporter. The coordinated docs change should consume this artifact and pin .github/workflows/ci.yml's server revision to the accepted server commit. No manual/requirements/static documentation was changed here.
  • The developer sandbox registration handlers now show pending guidance without storing tokens or starting sync. Their TypeScript check and Vite build remain blocked by missing base-revision modules src/lib/{api,config,dom,jwt,html,storage}. Only feedback.ts is tracked there. This pre-existing developer-tool build gap is separate from backend validation.
  • Real Google-provider, SMTP-provider, client/device, staging, deployment and live PowerSync acceptance were not run. Hosted CI is separate from the local results above.

Deployment order and client #60

  1. Release the compatible client first. It must parse pending registration and 403 email/cooldown details, reject unverified token responses before persistence, retain account/server-scoped offline data on denied refresh, and only switch profiles after eligible sign-in.
  2. Deploy the signed-out verification landing route before server cutover. GET must not consume tokens. Explicit confirmation POSTs to the issuing API, removes the token from visible history and prevents referrer leakage. Confirm the public app URL/base path, SMTP transport and EMAIL_VERIFICATION_TOKEN_EXPIRE_MINUTES=1440.
  3. Record the deployment audit count, stop old API instances for cutover, apply migrations through d2f3456789ab and deploy the enforcing server through the normal development-to-master release process. Do not leave older instances granting unverified sessions. Keep queue-table access restricted to the application/migration owner and privileged operators.
  4. Validate mail delivery/retries, callback/profile isolation and recovery with client Synchronize active reading timers through PowerSync #60. Previously issued PowerSync JWTs remain valid until their actual configured expiry, five minutes by default. Deny renewal and wait that lifetime before claiming live sync access is removed.
  5. Update the docs OpenAPI snapshot and its source revision together after server acceptance. Roll back by preserving enforcement/data and fixing forward, using retryable auth unavailability if necessary.

No merge or deployment is part of this PR preparation.

Copilot AI balanced review requested due to automatic review settings November 6, 2026 20:16

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T20:19:13.719810Z 2f2a6f3 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Eoic
Eoic merged commit c4adb0e into development Nov 6, 2026
2 checks passed
@Eoic
Eoic deleted the agent/issue-22 branch November 6, 2026 22:33
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.

[TASK-AUTH-02] Enforce verified sign-in in the authentication service

2 participants