Add Reddit OAuth authentication for API access #281

Open
opened 2026-07-28 19:10:43 +01:00 by Smithy-bot · 0 comments
Member

Context

Unauthenticated Reddit .json / listing access is blocked as of late May 2026 (HTTP 403). That breaks the current fetch path in RedditHelper, so both yarn start and the packaged CLI fail with Failed to fetch result from Reddit.

This blocks / supersedes the remaining work on #277 (comment). Cookie bootstrapping via old.reddit.com and swapping got-cjs for native fetch are no longer enough.

Confirmed locally:

  • https://old.reddit.com/... and https://www.reddit.com/.../*.json return 403
  • Reddit's block page tells apps to register developer credentials
  • Official path is authenticated OAuth against https://oauth.reddit.com

Goal

Add a first-class Reddit authentication flow so random-bunny can keep reading public subreddit listings (hot/new/top) without scraping.

Application-only OAuth (client_credentials) with a user-owned script/web app.

Why this fits random-bunny:

  • We only need public read access (subreddit listings + gallery metadata)
  • No logged-in user context is required
  • Script/web apps get a client_secret and can use grant_type=client_credentials
  • ~100 req/min free tier for non-commercial use is plenty for this tool
  • Avoids shipping a shared secret inside npm/binary releases

Token flow

  1. User creates an app at https://www.reddit.com/prefs/apps
    • Type: script (or web)
    • Redirect URI placeholder: http://localhost:8080
  2. App obtains token:
POST https://www.reddit.com/api/v1/access_token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
User-Agent: web:random-bunny:vX.Y.Z (by /u/<reddit-username>)

grant_type=client_credentials
  1. Response (approx 1 hour TTL, no refresh token):
{
  "access_token": "...",
  "token_type": "bearer",
  "expires_in": 3600,
  "scope": "*"
}
  1. Listing request:
GET https://oauth.reddit.com/r/{subreddit}/{sort}?limit={limit}
Authorization: Bearer <access_token>
User-Agent: web:random-bunny:vX.Y.Z (by /u/<reddit-username>)

Response shape should still match today's listing JSON (data.children[].data), so gallery parsing can stay mostly unchanged.

Alternatives considered

Option Pros Cons Verdict
Script/web + client_credentials Simple, official, read-only, no browser login User must create a Reddit app and supply credentials Preferred
Installed app + installed_client No secret; better for distributed binaries Still needs a Reddit app client_id; shared maintainer client_id risks rate-limit/abuse; needs stable device_id Possible CLI enhancement later
Password grant (script) Acts as a user Overkill; needs username/password; higher ToS risk Reject
Authorization code / browser login Good for multi-user web apps Heavy for a one-shot CLI/library Reject for v1
RSS fallback Sometimes still reachable Incomplete metadata vs JSON API; Reddit has flagged RSS as next to restrict; flaky rate limiting Reject as primary path
Third-party Reddit data APIs No Reddit app setup Extra dependency/cost/ToS surface Out of scope

Proposed product design

Library API

Require credentials (breaking change, or optional object with clear error if missing):

await randomBunny("rabbits", "hot", 100, {
  clientId: process.env.REDDIT_CLIENT_ID!,
  clientSecret: process.env.REDDIT_CLIENT_SECRET!,
  userAgent: "web:random-bunny:v2.5.0 (by /u/example)",
});

CLI

Credential sources (priority order):

  1. Flags, e.g. --client-id / --client-secret
  2. Env vars: REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, optional REDDIT_USER_AGENT
  3. Optional local config file (e.g. ~/.config/random-bunny/config.json)

Suggested UX helpers:

  • random-bunny auth / setup docs explaining how to create a Reddit app
  • Clear error when credentials are missing/invalid (distinct from fetch/parse failures)
  • Cache access token in-memory (and optionally on disk for CLI) until near expires_in

Security rules

  • Do not embed a shared client secret in published binaries or npm packages
  • Do not log secrets
  • Document that each consumer should register their own Reddit application
  • Keep User-Agent identifiable and include the Reddit username of the app owner (Reddit requirement)

Implementation outline

  1. Add RedditAuthHelper (or extend RedditHelper) to request/cache bearer tokens
  2. Change listing URL from old.reddit.com/...json to oauth.reddit.com/r/{sub}/{sort}
  3. Thread credentials through library options + CLI options/env/config
  4. New error codes/messages for missing credentials / auth failure / token expiry retry
  5. Update docs (readme.md, docs/cli.md) with Reddit app setup steps
  6. Unit tests with mocked token + listing responses
  7. Manual verification with real credentials for Node CLI and pkg binary

Acceptance criteria

  • With valid credentials, yarn start returns a random image result again
  • Packaged Linux/Windows binaries work the same way
  • Library callers can pass credentials programmatically
  • Missing/invalid credentials produce an actionable error (not generic fetch failure)
  • Token is reused until expiry instead of requesting a new one every call
  • Gallery posts still resolve image URLs correctly via existing GalleryHelper
  • Docs explain Reddit app registration and env/flag/config usage
  • No secrets committed or shipped in release artifacts

Open questions

  1. Treat credentials as a breaking API change (new required arg/options object) vs keep positional args and add an options overload?
  2. Ship a maintainer-owned installed-app client_id as a zero-config CLI fallback, or require BYO credentials always?
  3. Should this land in 2.4.2 (to unblock #277) or 2.5.0 (auth is a user-facing capability change)?
  4. Persist CLI token/cache on disk, or keep it process-local only?

References

## Context Unauthenticated Reddit `.json` / listing access is blocked as of late May 2026 (HTTP 403). That breaks the current fetch path in `RedditHelper`, so both `yarn start` and the packaged CLI fail with `Failed to fetch result from Reddit`. This blocks / supersedes the remaining work on #277 ([comment](https://git.vylpes.xyz/RabbitLabs/random-bunny/issues/277#issuecomment-20449)). Cookie bootstrapping via `old.reddit.com` and swapping `got-cjs` for native `fetch` are no longer enough. Confirmed locally: - `https://old.reddit.com/...` and `https://www.reddit.com/.../*.json` return 403 - Reddit's block page tells apps to register developer credentials - Official path is authenticated OAuth against `https://oauth.reddit.com` ## Goal Add a first-class Reddit authentication flow so random-bunny can keep reading public subreddit listings (hot/new/top) without scraping. ## Recommended approach **Application-only OAuth (`client_credentials`) with a user-owned script/web app.** Why this fits random-bunny: - We only need public read access (subreddit listings + gallery metadata) - No logged-in user context is required - Script/web apps get a `client_secret` and can use `grant_type=client_credentials` - ~100 req/min free tier for non-commercial use is plenty for this tool - Avoids shipping a shared secret inside npm/binary releases ### Token flow 1. User creates an app at https://www.reddit.com/prefs/apps - Type: **script** (or web) - Redirect URI placeholder: `http://localhost:8080` 2. App obtains token: ```http POST https://www.reddit.com/api/v1/access_token Authorization: Basic base64(client_id:client_secret) Content-Type: application/x-www-form-urlencoded User-Agent: web:random-bunny:vX.Y.Z (by /u/<reddit-username>) grant_type=client_credentials ``` 3. Response (approx 1 hour TTL, no refresh token): ```json { "access_token": "...", "token_type": "bearer", "expires_in": 3600, "scope": "*" } ``` 4. Listing request: ```http GET https://oauth.reddit.com/r/{subreddit}/{sort}?limit={limit} Authorization: Bearer <access_token> User-Agent: web:random-bunny:vX.Y.Z (by /u/<reddit-username>) ``` Response shape should still match today's listing JSON (`data.children[].data`), so gallery parsing can stay mostly unchanged. ## Alternatives considered | Option | Pros | Cons | Verdict | | --- | --- | --- | --- | | **Script/web + `client_credentials`** | Simple, official, read-only, no browser login | User must create a Reddit app and supply credentials | **Preferred** | | **Installed app + `installed_client`** | No secret; better for distributed binaries | Still needs a Reddit app `client_id`; shared maintainer client_id risks rate-limit/abuse; needs stable `device_id` | Possible CLI enhancement later | | **Password grant (script)** | Acts as a user | Overkill; needs username/password; higher ToS risk | Reject | | **Authorization code / browser login** | Good for multi-user web apps | Heavy for a one-shot CLI/library | Reject for v1 | | **RSS fallback** | Sometimes still reachable | Incomplete metadata vs JSON API; Reddit has flagged RSS as next to restrict; flaky rate limiting | Reject as primary path | | **Third-party Reddit data APIs** | No Reddit app setup | Extra dependency/cost/ToS surface | Out of scope | ## Proposed product design ### Library API Require credentials (breaking change, or optional object with clear error if missing): ```ts await randomBunny("rabbits", "hot", 100, { clientId: process.env.REDDIT_CLIENT_ID!, clientSecret: process.env.REDDIT_CLIENT_SECRET!, userAgent: "web:random-bunny:v2.5.0 (by /u/example)", }); ``` ### CLI Credential sources (priority order): 1. Flags, e.g. `--client-id` / `--client-secret` 2. Env vars: `REDDIT_CLIENT_ID`, `REDDIT_CLIENT_SECRET`, optional `REDDIT_USER_AGENT` 3. Optional local config file (e.g. `~/.config/random-bunny/config.json`) Suggested UX helpers: - `random-bunny auth` / setup docs explaining how to create a Reddit app - Clear error when credentials are missing/invalid (distinct from fetch/parse failures) - Cache access token in-memory (and optionally on disk for CLI) until near `expires_in` ### Security rules - Do **not** embed a shared client secret in published binaries or npm packages - Do not log secrets - Document that each consumer should register their own Reddit application - Keep User-Agent identifiable and include the Reddit username of the app owner (Reddit requirement) ## Implementation outline 1. Add `RedditAuthHelper` (or extend `RedditHelper`) to request/cache bearer tokens 2. Change listing URL from `old.reddit.com/...json` to `oauth.reddit.com/r/{sub}/{sort}` 3. Thread credentials through library options + CLI options/env/config 4. New error codes/messages for missing credentials / auth failure / token expiry retry 5. Update docs (`readme.md`, `docs/cli.md`) with Reddit app setup steps 6. Unit tests with mocked token + listing responses 7. Manual verification with real credentials for Node CLI and `pkg` binary ## Acceptance criteria - [ ] With valid credentials, `yarn start` returns a random image result again - [ ] Packaged Linux/Windows binaries work the same way - [ ] Library callers can pass credentials programmatically - [ ] Missing/invalid credentials produce an actionable error (not generic fetch failure) - [ ] Token is reused until expiry instead of requesting a new one every call - [ ] Gallery posts still resolve image URLs correctly via existing `GalleryHelper` - [ ] Docs explain Reddit app registration and env/flag/config usage - [ ] No secrets committed or shipped in release artifacts ## Open questions 1. Treat credentials as a **breaking** API change (new required arg/options object) vs keep positional args and add an options overload? 2. Ship a maintainer-owned **installed-app client_id** as a zero-config CLI fallback, or require BYO credentials always? 3. Should this land in **2.4.2** (to unblock #277) or **2.5.0** (auth is a user-facing capability change)? 4. Persist CLI token/cache on disk, or keep it process-local only? ## References - Related bug: #277 - Reddit OAuth2 wiki: https://github.com/reddit-archive/reddit/wiki/OAuth2 (Application Only OAuth) - Reddit app registration: https://www.reddit.com/prefs/apps - Async PRAW auth overview (app types / flows): https://asyncpraw.readthedocs.io/en/stable/getting_started/authentication.html - Reddit May 2026 unauthenticated `.json` deprecation / scraper policy
Smithy-bot added this to the 2.5.0 milestone 2026-07-28 19:10:43 +01:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
RabbitLabs/random-bunny#281
No description provided.