Repository navigation
Answer API authentication failures with a Bearer challenge - #3158
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Convergence on head edefabb:
Judgment calls to check:
|
2d7c06e to
e17b6d2
Compare
edefabb to
918eb22
Compare
69b77e7 to
d071ad7
Compare
3c9d777 to
2f49f2e
Compare
8947b42 to
bd3a9e9
Compare
2f49f2e to
255eb60
Compare
bd3a9e9 to
f501ced
Compare
255eb60 to
7612d15
Compare
|
Restacked onto #3082's new head (Jeremy's decision f). It had been sitting on an old #3082 head that predated CI is green. @codex review |
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
7612d15 to
fa1f70e
Compare
9cc10ea to
ed4f2cd
Compare
b7d0ea4 to
27cf43f
Compare
…it takes #3158 answers a read-only grant's write with 403 and keeps 401 for personal tokens, telling them apart by whether they have a client. A token of the personal access client has one, so it reads the token's connected app instead.
…it takes #3158 answers a read-only grant's write with 403 and keeps 401 for personal tokens, telling them apart by whether they have a client. A token of the personal access client has one, so it reads the token's connected app instead.
…it takes #3158 answers a read-only grant's write with 403 and keeps 401 for personal tokens, telling them apart by whether they have a client. A token of the personal access client has one, so it reads the token's connected app instead.
…it takes #3158 answers a read-only grant's write with 403 and keeps 401 for personal tokens, telling them apart by whether they have a client. A token of the personal access client has one, so it reads the token's connected app instead.
…it takes #3158 answers a read-only grant's write with 403 and keeps 401 for personal tokens, telling them apart by whether they have a client. A token of the personal access client has one, so it reads the token's connected app instead.
…it takes #3158 answers a read-only grant's write with 403 and keeps 401 for personal tokens, telling them apart by whether they have a client. A token of the personal access client has one, so it reads the token's connected app instead.
* commit '600d347762495665b35528d449e913c94c59b920': Keep shipped clients' answers: 401 for a read-only PAT, sign-in for no credentials Expect the Bearer challenge for unauthenticated device registration in SaaS Answer API authentication failures with a Bearer challenge
…it takes #3158 answers a read-only grant's write with 403 and keeps 401 for personal tokens, telling them apart by whether they have a client. A token of the personal access client has one, so it reads the token's connected app instead.
…it takes #3158 answers a read-only grant's write with 403 and keeps 401 for personal tokens, telling them apart by whether they have a client. A token of the personal access client has one, so it reads the token's connected app instead.
…er stopping partway * origin/oauth-confidential-clients: Make the OAuth migrations safe to run again after stopping partway
* origin/oauth-confidential-clients: Answer 404 from a dark endpoint however many times it's asked
* tp/oauth-confidential-clients: (60 commits) Name v0.2.0 in the pin's comment Pin fizzy-mcp-server v0.2.0 Refuse a join code that was reset while it was being redeemed Filter presigned storage signatures from logs Keep the reviewer's memberships in reach, and print the member audit escaped Import an export that lacks a column this database has only just gained List the reviewer accounts' members when signing the reviewer out Bump thruster from 0.1.26 to 0.1.27 (#3199) Give the asset build a placeholder direct-upload credential Anchor the stubbed storage host pattern Sign direct uploads with a write-only storage credential Lock the reviewer's email address, so a code holder can't move its membership out of revocation's reach Use Ruby's GC defaults and log job memory (#3238) Pin the fizzy_mcp accessory to fizzy-mcp-server v0.1.0 by digest Scope webhook dispatch by account as well as board Test the reviewer audit against the structured log line production writes Revoke on the email alone, and record each use in the request's structured log Remove the reviewer's access tokens and passkeys on sign-out, so a code holder can't keep a way back in Wire the reviewer sign-in into production from Deploy/Fizzy Close the reviewer's live sockets on sign-out, and keep the identity id out of Sentry ...
* tp/oauth-confidential-clients: Bring main's tests up to this branch: Ruby 4 and the access token finder
* tp/oauth-confidential-clients: Pin the load balancers to kamal-proxy v0.10.2, and say why configure-lb-mcp.sh still checks Give the load balancer the real client IP
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b0a9bf7455
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
* tp/oauth-confidential-clients: Redact OAuth client statements from logged SQL, now that a client has a secret
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
* tp/oauth-confidential-clients: Filter the code parameter by its whole name, so code_challenge stays readable
Requirement-by-requirement status for the OAuth train (basecamp#2296, basecamp#3080, basecamp#3081, basecamp#3082) and the PRs above it (basecamp#3158, basecamp#3159): what conforms, which commit fixed what, what the hosted plan schedules, the deliberate postures, and the decisions still open.

Stacked on #3082 (train: #2296 → #3080 → #3081 → #3082 → this → #3160 → …; #3160 was restacked onto this on 2026-10-17, so the line is linear). The gap predates the train, but #3081's hourly expiry makes it load-bearing, so it rides on the tip.
Problem
Every bearer failure got Rails' default
WWW-Authenticate: Token realm="Application", with no error code. JSON requests that sent no credentials were redirected to the sign-in page. RFC 6750 §3 says challenges MUST use theBearerscheme, including when credentials are missing. A read-only token used on a write got the same 401 as a dead token. An OAuth client would then refresh, get another read token (a refresh can't widen scope), and loop.Change
Token realmBearer … error="invalid_token"Token realmBearer … error="insufficient_scope", scope="write"Token realmBearer … error="insufficient_scope", scope="write"(status unchanged for shipped clients)Token realmBearer … error="invalid_request"bearer <token>(lowercase)Basic …Bearer…resource_metadata(RFC 9728 §5.1) is built from the sameroot_url(script_name: nil)as the metadata'sresource. It is included only whileOauth::Availability.acceptance_enabled?, so a dark server never points at a URL that 404s.Identity.find_by_permissable_access_tokenis replaced byIdentity::AccessToken.find_honored, which leaves the method check to the controller. That way a dead token and a token with too little scope get different answers. Expiry and the availability gate still come first: an expired read token on a write isinvalid_token, notinsufficient_scope.AUTH_SCHEME_REGEXRails uses, so the gate and the parser agree. When DPoP lands, it widens this predicate.D6: shipped clients keep their answers (decided 2026-10-17)
The native apps and fizzy-cli rely on two of the old answers, and #3191 pins both: a read-only personal access token writing gets 401, and a JSON request with no credentials gets a 302 to sign in. So only an OAuth grant gets the 403. A credential-less JSON request keeps the 302, because nothing in it tells a shipped client from an OAuth one. That 302 is a recorded deviation from RFC 6750 §3 (rubric DS-3;
docs/oauth/spec_conformance.md,docs/oauth.mdon #3193). Hosted OAuth clients discover Fizzy's authorization server through fizzy-mcp-server's own 401 +resource_metadataand Fizzy's/.well-knownmetadata, so the deviation costs them little. Exit: once the oldest supported native app and fizzy-cli handle a 401 challenge there. It also keeps@rails/request.js(card autosave) and the direct uploader on the redirect they handle.Unchanged
resume_sessionruns first.require_accountredirect for untenanted paths is untouched.Tests
test/controllers/api_test.rbasserts the status and exact challenge for each row above. It also checks that the advertisedresource_metadataURL resolves to metadata whoseresourceis the root URL, and that the parameter is absent while OAuth is dark. A read-only OAuth grant's write gets 403; a read-only personal access token's write keeps its 401 with theinsufficient_scopechallenge; a credential-less JSON request is redirected. #3191's pins pass unchanged on the train. API docs:docs/api/README.md.