Skip to content

feat(reddit): connect through the dos.me OAuth broker - #54

Merged
JOY (JOY) merged 1 commit into
devfrom
feat/reddit-oauth-broker
Sep 22, 2026
Merged

JOY (JOY) merged 1 commit into
devfrom
feat/reddit-oauth-broker

Conversation

@JOY

Copy link
Copy Markdown

What

Reddit channel connect switches from a direct Reddit OAuth flow (REDDIT_CLIENT_ID + client secret in Crove) to the shared dos.me Reddit OAuth broker (DOS-Me side already live on prod, spec: REDDIT-OAUTH-BROKER.md):

  1. generateAuthUrl redirects the user to {DOS_ME_API_URL}/oauth/reddit/authorize?product=<label>&state=<state> instead of reddit.com/api/v1/authorize. Product label: crove-post-prod / crove-post-beta (auto from FRONTEND_URL, override REDDIT_BROKER_PRODUCT). State bumped from 6 chars to 32 (broker requires >= 128-bit entropy).
  2. The broker redirects back to the existing /integrations/social/reddit returnTo with ?handle=<one-time>&state=<our state> - the state/login Redis contract is untouched.
  3. authenticate exchanges the handle server-side: POST /oauth/reddit/token-delivery/:handle with X-API-Key: DOS_ME_INTERNAL_API_KEY, then fetches /api/v1/me as before and creates the channel identically.

Frontend: the continue page maps the callback's handle param to the connect body's code field. User-denial (?error=...) surfaces as a friendly message via the existing string-error path.

Deliberate non-change

REDDIT_CLIENT_ID/REDDIT_CLIENT_SECRET stay in the env (contrary to the one-line "can remove them"): the broker has no refresh endpoint, and the refresh grant in refreshToken() (used by the post workflow's 401 recovery for any scheduled post older than ~1h) requires them. Verified on the VM: prod env holds the DOS app creds (same app the broker now authorizes through), so refresh keeps working for both old and new channels. Beta never had them - beta refresh was already broken before this change and stays unchanged. A dos.me broker refresh endpoint would be the clean follow-up.

Verification

  • pnpm build (frontend + backend + orchestrator) green; eslint clean on changed files.
  • Env presence verified on the VM (values not printed): DOS_ME_INTERNAL_API_KEY set on beta + prod (88 chars).
  • Beta E2E (connect up to the Reddit authorize screen + channel creation) follows on deployment.

- generateAuthUrl redirects to api.dos.me/oauth/reddit/authorize with a
  per-env product label (crove-post-prod/beta, override REDDIT_BROKER_PRODUCT)
  and a >=128-bit state instead of Reddit directly with REDDIT_CLIENT_ID
- authenticate exchanges the broker's one-time delivery handle for the token
  bundle via POST /oauth/reddit/token-delivery/:handle (X-API-Key =
  DOS_ME_INTERNAL_API_KEY); the state/login Redis contract is unchanged
- REDDIT_CLIENT_ID/SECRET stay in the env: the broker does not cover the
  refresh grant, which 401 recovery for scheduled posts older than an hour
  depends on
- frontend continue page maps the callback's handle param to the connect
  body's code field

@dos dos 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.

⏱️ Code Review completed (3 files · 8,728 chars · 1 PR unit(s))

⏱️ Adversarial Review completed (Model: qwen3.8-27b)

🔍 Verified Adversarial Review Findings

🔴 BLOCKER

  • libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts:180-185: authenticate returns a string on error, violating the method's return type contract.
    • Failure Trace:
      1. User denies Reddit authorization via the dos.me broker.
      2. Broker redirects to frontend with ?error=... and no handle.
      3. Frontend (continue.integration.tsx) extracts code: searchParams.handle || '' (empty string).
      4. Backend controller calls redditProvider.authenticate({ code: '', codeVerifier: '...' }).
      5. authenticate hits if (!params.code) and returns the string "Reddit authorization was denied or expired, please connect again.".
      6. The caller (NestJS controller/service) expects an object conforming to SocialAuthResult (with id, name, accessToken, etc.) or an exception.
      7. Runtime error occurs when the caller attempts to access properties on the returned string (e.g., result.accessToken is undefined, or result.id is undefined), or when the response is serialized incorrectly, leading to a 500 error or broken UI state.
    • Actionable Fix: Throw an error instead of returning a string, allowing the caller to handle it via standard exception handling.
    if (!params.code) {
      // The broker redirects back with only ?error=... when the user denies
      // the authorization (or the flow expires before the callback).
      throw new Error('Reddit authorization was denied or expired, please connect again.');
    }

🛡️ Dismissed Claims

  • CSRF / State Validation Gap: The state parameter is extracted by the frontend and typically validated by the backend controller before calling authenticate (standard OAuth pattern). The diff does not show the controller, but the provider's authenticate signature has never included state in the visible context (it was not in the old implementation either). This is not a new defect introduced by this diff; it is a pre-existing architectural pattern. The claim that this is a new security flaw is unsupported by the diff.
  • Unused codeVerifier: While codeVerifier is now unused in authenticate because the broker handles PKCE, this is a minor code cleanliness issue, not a functional bug or security vulnerability. The parameter is still part of the interface for consistency with other providers. It does not cause a runtime failure.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request integrates the dos.me Reddit OAuth broker for the Reddit connection flow. It updates the frontend to handle the broker's one-time delivery handle and modifies the backend RedditProvider to generate broker-compatible authorization URLs and exchange the delivery handle for the token bundle. A review comment suggests explicitly coercing the expiresIn value to a number using Number() to prevent potential runtime type mismatches if the broker returns it as a string.

return {
accessToken,
refreshToken: body?.refreshToken ?? body?.refresh_token,
expiresIn: body?.expiresIn ?? body?.expires_in ?? 3600,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The expiresIn property returned from the broker might be a string (e.g., if parsed from an external JSON response where it is not strictly typed). Since AuthTokenDetails and downstream token expiration calculations expect a number, it is safer to explicitly coerce this value using Number() to prevent potential runtime type mismatch bugs or NaN calculations.

Suggested change
expiresIn: body?.expiresIn ?? body?.expires_in ?? 3600,
expiresIn: Number(body?.expiresIn ?? body?.expires_in ?? 3600),

@JOY
JOY (JOY) merged commit d1086ff into dev Sep 22, 2026
10 checks passed
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