Skip to content

Restrict hosted OAuth to one Reddit account - #2

Merged
jwaldrip merged 3 commits into
feat/hosted-reddit-mcpfrom
fix/hosted-single-user-oauth
Aug 20, 2026
Merged

jwaldrip merged 3 commits into
feat/hosted-reddit-mcpfrom
fix/hosted-single-user-oauth

Conversation

@jwaldrip

@jwaldrip jwaldrip commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Why

Jason is applying for Reddit Data API access for personal, single-account use. The hosted branch leaves dynamic client registration open and, before this change, accepted whichever Reddit identity completed the callback. Open registration without an owner check would contradict that application.

What changed

  • Read REDDIT_ALLOWED_USERNAME at the hosted HTTP entry point and carry it into the Reddit grant.
  • Compare the returned Reddit identity with that configured username before any user grant is stored or local authorization code is minted.
  • Match case-insensitively and ignore a leading u/ or /u/ on the configured value.
  • Return OAuth access_denied for a different account. An unset or empty value also returns access_denied with a message explaining that this is a single-account server with no account configured.
  • Keep the stdio credential path unchanged.
  • Update the README for the single-account boundary and remove the public han plugin connection instructions.

Guard proof

The regression tests were committed before the implementation. Against the unguarded callback, the different-account, unset, and empty cases all failed because the callback returned an authorization code instead of access_denied.

I repeated that check after implementation by temporarily removing grant.assertAllowedUsername(username) and running npm test -- test/oauth/reddit-grant.test.ts. All three refusal tests failed with:

Test Files  1 failed (1)
     Tests  3 failed | 4 passed (7)
  Duration  273ms (transform 33ms, setup 0ms, collect 94ms, tests 9ms, environment 0ms, prepare 32ms)

Restoring the guard returned the file to 7 passing tests.

Verification

npm run lint

> @thebushidocollective/reddit-mcp@1.0.0 lint
> biome check src test

Checked 24 files in 8ms. No fixes applied.

npm run typecheck

> @thebushidocollective/reddit-mcp@1.0.0 typecheck
> tsc --noEmit

npm test

✓ test/oauth/reddit-grant.test.ts (7 tests) 8ms
✓ test/oauth/http-server.test.ts (13 tests) 64ms
✓ test/reddit/client-fetch.test.ts (15 tests) 1010ms

Test Files  11 passed (11)
     Tests  120 passed (120)
  Duration  1.26s (transform 371ms, setup 0ms, collect 939ms, tests 1.11s, environment 1ms, prepare 541ms)

@jwaldrip
jwaldrip marked this pull request as ready for review August 20, 2026 07:44
@jwaldrip
jwaldrip merged commit 30b74bb into feat/hosted-reddit-mcp Aug 20, 2026
1 check passed
jwaldrip added a commit that referenced this pull request Sep 2, 2026
* Reddit MCP as a hosted HTTP server behind a Reddit sign-in

The han reddit plugin ran a public unauthenticated server, so it could
read all of Reddit except the part that belongs to you. The first fix
put an authenticated server inside the han monorepo, over stdio from
npx, which meant a service living in a plugin marketplace and every user
hand-minting a Reddit refresh token onto every machine they use.

This is that server as its own repo, speaking HTTP, with an OAuth 2.1
authorization server in front that bridges to Reddit own grant. An agent
registers itself, the human is sent to Reddit consent screen, and the
refresh token that comes back is sealed at rest with AES-256-GCM and
never leaves the service. No Reddit password ever reaches us.

The Reddit client, its retry and pagination logic and its 56 tests are
carried over unchanged. New here is the transport, the authorization
server, the container and the deploy.

Authorization codes are sealed rather than signed, and single use,
enforced by a Firestore create that fails when the document exists.
Our tokens are stored under a hash of themselves, so a database read
yields fingerprints rather than working credentials.

Terraform for the Cloud Run service lives in jwaldrip/cld beside the
other services in that project. This repo owns the code and which
revision serves.

* Say what bootstrap actually creates

The bring-up order claimed bootstrap.sh creates the deployer this repo
authenticates as. It does not, and its own comments say so: the pool and
provider are bootstrap substrate, while the deployer account and every
role on it are terraform, so they sit beside the rest of what that
account may do. An operator following the old wording would have looked
for an account that does not exist yet.

* Refuse scopes this server cannot deliver

The authorize handler wrote whatever scopes the client asked for into
the transaction. The SDK advertises scopesSupported in metadata but does
not enforce it, so a client could request anything and see it echoed
back in the token response.

That is a lie rather than a privilege, since naming a scope here does
not obtain it from Reddit, and it becomes a real hole the moment a route
gates on scope. The refresh grant only narrows against the original set,
so an inflated original would have propagated for the life of the grant.

Unsupported scopes are now refused at the point they are asked for,
returned to the client as invalid_scope per OAuth 2.1 rather than
rendered, and never forwarded to Reddit.

* Restrict hosted OAuth to one Reddit account (#2)

* Prove hosted OAuth needs an account boundary

* Restrict hosted OAuth to one Reddit account

* Document the hosted account boundary
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