feat(reddit): connect through the dos.me OAuth broker - #54
Conversation
- 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
There was a problem hiding this comment.
⏱️ 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:authenticatereturns a string on error, violating the method's return type contract.- Failure Trace:
- User denies Reddit authorization via the dos.me broker.
- Broker redirects to frontend with
?error=...and nohandle. - Frontend (
continue.integration.tsx) extractscode: searchParams.handle || ''(empty string). - Backend controller calls
redditProvider.authenticate({ code: '', codeVerifier: '...' }). authenticatehitsif (!params.code)and returns the string"Reddit authorization was denied or expired, please connect again.".- The caller (NestJS controller/service) expects an object conforming to
SocialAuthResult(withid,name,accessToken, etc.) or an exception. - Runtime error occurs when the caller attempts to access properties on the returned string (e.g.,
result.accessTokenisundefined, orresult.idisundefined), 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.
- Failure Trace:
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
stateparameter is extracted by the frontend and typically validated by the backend controller before callingauthenticate(standard OAuth pattern). The diff does not show the controller, but the provider'sauthenticatesignature has never includedstatein 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: WhilecodeVerifieris now unused inauthenticatebecause 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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
| expiresIn: body?.expiresIn ?? body?.expires_in ?? 3600, | |
| expiresIn: Number(body?.expiresIn ?? body?.expires_in ?? 3600), |
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):
generateAuthUrlredirects the user to{DOS_ME_API_URL}/oauth/reddit/authorize?product=<label>&state=<state>instead ofreddit.com/api/v1/authorize. Product label:crove-post-prod/crove-post-beta(auto from FRONTEND_URL, overrideREDDIT_BROKER_PRODUCT). State bumped from 6 chars to 32 (broker requires >= 128-bit entropy)./integrations/social/redditreturnTo with?handle=<one-time>&state=<our state>- thestate/loginRedis contract is untouched.authenticateexchanges the handle server-side:POST /oauth/reddit/token-delivery/:handlewithX-API-Key: DOS_ME_INTERNAL_API_KEY, then fetches/api/v1/meas before and creates the channel identically.Frontend: the continue page maps the callback's
handleparam to the connect body'scodefield. User-denial (?error=...) surfaces as a friendly message via the existing string-error path.Deliberate non-change
REDDIT_CLIENT_ID/REDDIT_CLIENT_SECRETstay in the env (contrary to the one-line "can remove them"): the broker has no refresh endpoint, and the refresh grant inrefreshToken()(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.