Scope authenticated rate limits by credential - #1635
Conversation
Greptile SummaryThe PR changes the general Rack Attack discriminator so accessible OAuth bearer tokens under the authenticated API namespace are grouped by resource owner while invalid and anonymous requests remain grouped by IP.
Confidence Score: 2/5The PR is not safe to merge because wrong-scope credentials receive authenticated buckets and previously reported credential-isolation failures remain. The current HEAD still maps every accessible OAuth token to its owner's bucket without checking endpoint scope, maps separate tokens for one user to the same bucket and leaves supported query credentials on shared IP buckets despite replies claiming those isolation issues were fixed. Files Needing Attention: config/initializers/rack_attack.rb Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
R[Authenticated API request] --> B{Bearer token present?}
B -- No --> I[IP throttle bucket]
B -- Yes --> A{Token accessible?}
A -- No --> I
A -- Yes --> U[User throttle bucket]
U --> C[Controller scope authorization]
C -- Scope accepted --> O[Serve request]
C -- Scope rejected --> F[Return insufficient-scope response]
Prompt To Fix All With AI### Issue 1
config/initializers/rack_attack.rb:34-35
**Wrong-scope tokens bypass throttling**
When an accessible token lacks the scope required by an authenticated endpoint, this discriminator still assigns it to the resource owner's bucket before the controller rejects it, allowing callers with multiple unauthorized credentials to avoid the shared IP throttle.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (6): Last reviewed commit: "Share OAuth rate limits by user" | Re-trigger Greptile |
| case normalized_path | ||
| when "/api/v1/authenticated/me" | ||
| [ [ :bearer ], false, true, [ "profile" ] ] | ||
| when "/api/v1/authenticated/hours", "/api/v1/authenticated/streak", | ||
| "/api/v1/authenticated/projects", "/api/v1/authenticated/heartbeats/latest" | ||
| [ [ :bearer ], false, true, [ "read" ] ] | ||
| when "/api/v1/authenticated/api_keys" | ||
| [ [ :bearer ], false, true, nil ] | ||
| when %r{\A/api/hackatime/v1/} | ||
| [ %i[bearer basic query], true, false, nil ] | ||
| when "/api/v1/my/heartbeats", "/api/v1/my/heartbeats/most_recent" |
There was a problem hiding this comment.
When clients with distinct valid bearer credentials request an authenticated route such as /api/v1/authenticated/me.json from the same IP, the exact path policy does not recognize the Rails format suffix and falls back to the shared IP bucket, causing one client to exhaust another client's allowance and produce unrelated 429 responses.
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: config/initializers/rack_attack.rb
Line: 52-62
Comment:
**Format suffixes share buckets**
When clients with distinct valid bearer credentials request an authenticated route such as `/api/v1/authenticated/me.json` from the same IP, the exact path policy does not recognize the Rails format suffix and falls back to the shared IP bucket, causing one client to exhaust another client's allowance and produce unrelated 429 responses.
**Knowledge Base Used:**
- [Developer API and authentication](https://app.greptile.com/mahadk/-/custom-context/knowledge-base/hackclub/hackatime/-/docs/developer-api-and-authentication.md)
- [API key management](https://app.greptile.com/mahadk/-/custom-context/knowledge-base/hackclub/hackatime/-/docs/api-key-management.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.|
Narrowed the change after maintainer feedback. The generic credential policy duplicated controller authentication and was too complex for this initializer. This now handles only the observed OAuth integration case under /api/v1/authenticated/; every other request retains the existing IP discriminator. |
|
Updated the primary discriminator to the authenticated Hackatime user, matching GitHub’s user-token model. Multiple OAuth tokens for one user now share the allowance while different users behind the same integration host remain isolated. |
| oauth_token = Doorkeeper::AccessToken.by_token(token) | ||
| "user:#{oauth_token.resource_owner_id}" if oauth_token&.accessible? && oauth_token.resource_owner_id |
There was a problem hiding this comment.
Wrong-scope tokens bypass throttling
When an accessible token lacks the scope required by an authenticated endpoint, this discriminator still assigns it to the resource owner's bucket before the controller rejects it, allowing callers with multiple unauthorized credentials to avoid the shared IP throttle.
Knowledge Base Used: Developer API and authentication
Prompt To Fix With AI
This is a comment left during a code review.
Path: config/initializers/rack_attack.rb
Line: 34-35
Comment:
**Wrong-scope tokens bypass throttling**
When an accessible token lacks the scope required by an authenticated endpoint, this discriminator still assigns it to the resource owner's bucket before the controller rejects it, allowing callers with multiple unauthorized credentials to avoid the shared IP throttle.
**Knowledge Base Used:** [Developer API and authentication](https://app.greptile.com/mahadk/-/custom-context/knowledge-base/hackclub/hackatime/-/docs/developer-api-and-authentication.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
We are intentionally not applying endpoint scopes to the primary rate-limit identity. An accessible OAuth token establishes the user identity; endpoint scope remains an authorisation concern in the controller. All accessible tokens for one resource owner share the same user bucket, so issuing multiple wrong-scope tokens does not multiply that user’s allowance. Different users receiving separate primary buckets is the intended GitHub-style model, with the existing API-wide IP throttle retained as the secondary ceiling.
There was a problem hiding this comment.
You're right — given the intended two-tier model, endpoint scopes should not participate in the primary identity. An accessible token, regardless of its endpoint scopes, maps to its resource owner, so issuing multiple wrong-scope tokens does not increase that user's 300/minute allowance. The separate IP-based API throttle remains the secondary ceiling. This invalidates my concern; I withdraw the finding.
Summary of the problem
OAuth integrations sharing an IP address also share the general rate limit bucket, allowing one integration to rate limit requests made for unrelated Hackatime users.
Describe your changes
Give each authenticated Hackatime user under the OAuth API namespace a general throttle bucket shared across their OAuth tokens. Invalid, anonymous and all other requests remain scoped by IP address.
Screenshots / Media
Not applicable.