Skip to content

Support confidential clients with client_secret_post - #3082

Merged
jeremy merged 28 commits into
oauth-refresh-tokensfrom
oauth-confidential-clients
Oct 27, 2026
Merged

jeremy merged 28 commits into
oauth-refresh-tokensfrom
oauth-confidential-clients

Conversation

@jeremy

@jeremy jeremy commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

Stacked on the refresh-token PR. Third gap-closer for #2296, matching what beta2's bc3 authorization server advertises. Two conformance decisions also land here: D3 (client_secret_basic) and D4 (revocation client authentication). This PR owns client authentication, and both decisions reverse lines that were in this body before. D1 and D2 are stacked above it in #3160 and #3161.

1ca8787 adds confidential client support:

  • Clients carry a token_endpoint_auth_method (none, client_secret_post or client_secret_basic). Confidential clients get a generated client_secret at creation. It is returned once in the DCR response, and .compact keeps it out of public-client responses.
  • The token endpoint authenticates confidential clients on both grants. Public loopback clients are unchanged.
  • Secrets are stored in plaintext and compared in constant time, the way the stack already stores access tokens.

D3: client_secret_basic (d65d011)

RFC 6749 §2.3.1 says the server MUST support HTTP Basic for clients issued a password. This mirrors basecamp/bc3#13678 (Oauth::ClientAuthentication).

  • Oauth::ClientAuthentication reads either transport. It base64-decodes the Basic value strictly, splits on the first colon, and form-urldecodes each half (Appendix B), so %3A becomes : and + becomes a space (bc3). A confidential client's secret verifies whichever way it arrives, because the registered method is advisory.

  • Ambiguity (bc3):

    Request Answer
    Basic plus a non-empty body client_secret (even an identical one) 400 invalid_request: more than one method (§2.3, §5.2)
    Basic plus a body client_id naming the same client, or an empty client_secret= authenticates
    Basic plus a body client_id naming another client 401 invalid_client
    Malformed Basic (bad base64, no colon, empty id, bad %-encoding, invalid UTF-8) 401 invalid_client
    Basic naming a public client 401 invalid_client: a public client holds no password
  • invalid_client is 401 + WWW-Authenticate: Basic realm="<issuer>" only for a failed Basic attempt, and 400 with no challenge for everything else (b6b8329). "Everything else" is a failed client_secret_post, a public client presenting a secret, a secret that authenticates no client, and a body failure beside an Authorization header of another scheme (which until now drew a Basic challenge). Why:

    • A bare 401 violates RFC 9110 §15.5.2, which requires a challenge.
    • A Basic challenge to a request that never tried Basic names a scheme that doesn't apply to it.
    • oauth4webapi and openid-client turn a challenged 401 into a WWWAuthenticateChallengeError instead of invalid_client.
    • A 401 + Basic on a form POST can pop a browser auth dialog.

    This is the policy settled across bc3, Fizzy and HEY after an adversarial review; it supersedes the earlier "every invalid_client is 401" (9b27557). Fizzy has no deployed clients, so nothing depends on the old status. Status, body and header remain identical for an unknown client and a wrong secret.

  • The pilot gate keys on the Basic id. Discovery advertises token_endpoint_auth_methods_supported: [none, client_secret_post, client_secret_basic], and DCR accepts client_secret_basic. bc3 pins DCR to none, but Fizzy's DCR already accepted client_secret_post, so Basic joins it.

D4: revocation client authentication (e80b5b6)

RFC 7009 §2.1: validate the client's credentials, then check that the token was issued to that client. This mirrors bc3's Oauth::RevocationsController, and the Basic branch mirrors #13678.

Request Answer
Confidential client, correct secret (post or Basic), its own token 200, grant revoked
Confidential client, missing or wrong secret in the body 400 invalid_client, no challenge, nothing revoked
Any Basic header that doesn't authenticate (unknown id, wrong secret, public client) 401 invalid_client + WWW-Authenticate: Basic, whatever the token
Public client naming its client_id, its own token 200, grant revoked
Authenticated client presenting another client's token, an unknown token, or a PAT 200, nothing revoked (§2.2)
No client_id, or an unknown one, without Basic or a client secret 200, nothing revoked
A client secret that authenticates no client (no client_id, an unknown one, or a public client, which holds no password) 400 invalid_client (c9375f1, 29a5d11). bc3 answers 200 here; Fizzy treats any presented secret as an authentication attempt, as its token endpoint does
A personal access token, with no client credentials or with credentials that authenticate 200, token revoked
A personal access token, with client credentials that fail to authenticate invalid_client (401 + Basic if via Basic, else 400), nothing revoked

The row for a missing or unknown client_id follows bc3. When no client is identified, the request has failed to identify a client rather than failed authentication, so it gets the non-oracular 200 and not invalid_client. Personal access tokens stay revocable here (98619ab, Jeremy's decision: revocation is uniform across token kinds, and PATs may later be rebuilt on OAuth, see #3164). A PAT has no client, so holding it is the credential. Client credentials presented alongside it must still pass the rules above. Discovery advertises revocation_endpoint_auth_methods_supported: [none, client_secret_post, client_secret_basic].

The old line "Revocation stays unauthenticated by design" is withdrawn. Codex's revocation thread asked for exactly this change.

Tests

test/integration/oauth_client_secret_basic_test.rb and test/integration/oauth_revocation_test.rb are new. I wrote them first: 20 failures and 1 error before the change, and the 3 that passed were guards for behavior that already existed. They cover Basic-only refresh and code exchange, %3A/+ decoding, both-methods → 400, header/body id mismatch, five malformed shapes, Basic naming a public client, unknown client vs wrong secret giving identical answers, a post failure → 400 without the challenge (also beside an Authorization header of another scheme), the pilot gate reading the Basic id, a drift test (every advertised secret method authenticates), DCR, the revocation rows above, including the cross-client revoke, and personal-access-token revocation, which is possession-based but still refused on failed client authentication. A revoked PAT then stops authenticating. Existing tests expect 400 for a non-Basic invalid_client (written first: 17 failures before b6b8329), and revocation requests in them name their client_id.

Locally: bin/rails test test/integration test/models test/controllers passes (1517 tests).

Shipped-client matrix for the invalid_client 401 → 400 change

None of the OAuth endpoints are live yet, since this train is unmerged. No first-party Fizzy client calls /oauth/token, /oauth/revocation or any other /oauth/* or /.well-known/oauth-* path. I checked every tag and every remote branch.

Client (shipped, min supported) Auth today Body-auth failure at token or revocation: 401 → 400 Basic failure (unchanged, 401 + Basic) Branches on 400 vs 401 at /oauth/*
fizzy-ios v1.0-233, no force-update magic link, then the session_token cookie not called, unaffected not called no
fizzy-android 1.1.3-14, no force-update magic link (LoginApi.kt:31-56@1.1.3-14), then the cookie not called, unaffected not called no
fizzy-cli v4.0.1 Bearer PAT (internal/client/client.go:238,648@v4.0.1) not called, unaffected not called no
fizzy-sdk v0.2.4 Bearer or cookie (go/pkg/fizzy/auth_strategy.go:31,51); its AGENTS.md:60 says "no OAuth" not called, unaffected not called no
fizzy-mcp-server, fizzy-popper (main, no releases) Bearer PAT not called, unaffected not called no

The PAT and session flows don't touch these endpoints: PATs go only to API routes as Bearer, and sessions go to /session*. The Fizzy server has no minimum-version gate. The only OAuth-shaped client code was a HEY-copied CalendarAccessTokenUpdater.swift on the fizzy-ios multi-identity-setup branch. It never shipped, was removed in 6920f9f, and treated 400 and 401 alike. The planned hosted-MCP exchange client (fizzy-mcp-server docs/hosted-plan) should expect a 400 invalid_client for a body-auth failure.

Copilot AI balanced review requested due to automatic review settings August 28, 2026 06:16
@jeremy jeremy mentioned this pull request Aug 28, 2026
4 tasks

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds OAuth confidential-client support using client_secret_post, including registration, authentication, discovery metadata, persistence, and tests.

Changes:

  • Generates and authenticates confidential-client secrets.
  • Extends dynamic registration and discovery metadata.
  • Adds database fields, fixtures, and OAuth flow coverage.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
app/controllers/oauth/clients_controller.rb Registers confidential clients and returns secrets.
app/controllers/oauth/metadata_controller.rb Advertises client_secret_post.
app/controllers/oauth/tokens_controller.rb Authenticates confidential token requests.
app/models/oauth/client.rb Adds secret generation and verification.
db/migrate/20260827110000_add_oauth_client_secrets.rb Adds client authentication columns.
db/schema.rb Updates MySQL schema.
db/schema_sqlite.rb Updates SQLite schema.
test/fixtures/oauth/clients.yml Adds a confidential-client fixture.
test/integration/oauth_flow_test.rb Tests registration and token flows.
test/models/oauth/client_test.rb Tests secret behavior and validation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread app/controllers/oauth/tokens_controller.rb Outdated
Comment thread app/controllers/oauth/clients_controller.rb
Comment thread app/controllers/oauth/metadata_controller.rb Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8080b3f2ac

ℹ️ 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".

Comment thread app/controllers/oauth/clients_controller.rb
@jeremy
jeremy force-pushed the oauth-confidential-clients branch 2 times, most recently from e5b198c to 40d32e9 Compare August 28, 2026 06:26

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 40d32e9afb

ℹ️ 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".

Comment thread db/migrate/20260827110000_add_oauth_client_secrets.rb
Comment thread app/models/oauth/client.rb
Comment thread app/controllers/oauth/metadata_controller.rb Outdated
Comment thread app/controllers/oauth/clients_controller.rb
@jeremy
jeremy force-pushed the oauth-confidential-clients branch from 40d32e9 to c1e15ca Compare August 28, 2026 06:40

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0502fd087d

ℹ️ 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".

Comment thread app/controllers/oauth/tokens_controller.rb Outdated
@jeremy
jeremy force-pushed the oauth-confidential-clients branch from 0502fd0 to fe5aab8 Compare August 28, 2026 06:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe5aab8efe

ℹ️ 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".

Comment thread app/controllers/oauth/tokens_controller.rb Outdated
@jeremy
jeremy force-pushed the oauth-confidential-clients branch from fe5aab8 to 92b3eb2 Compare August 31, 2026 19:56
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-26T04:35:47.709073Z 5f9e19b New commits
🔒 Security Review ✅ Completed 2026-10-26T04:36:41.989294Z 5f9e19b New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92b3eb2b2a

ℹ️ 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".

Comment thread app/controllers/oauth/tokens_controller.rb Outdated
@jeremy
jeremy force-pushed the oauth-confidential-clients branch from 92b3eb2 to f2b19f1 Compare September 10, 2026 18:31
@jeremy
jeremy force-pushed the oauth-confidential-clients branch from f2b19f1 to 61d0f7c Compare October 6, 2026 02:30
@jeremy

jeremy commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Rebased onto #2296's token-endpoint conformance fix (comment) and #3081's follow-up.

The client-auth ordering commit now also covers the code grant. authenticate_client runs first, then the shared validate_client_id:

  • A confidential client that omits or misnames client_id gets invalid_client, on either grant.
  • A public client gets invalid_request if client_id is missing, or invalid_grant if it names the wrong client.

For that reason the presence check for client_id lives in validate_client_id instead of the up-front parameter check. A new test covers the confidential code-grant case.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61d0f7ccf9

ℹ️ 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".

Comment thread app/controllers/oauth/tokens_controller.rb Outdated
@jeremy

jeremy commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Review status on head 61d0f7c: CI is green on all four train heads.

One thread is still open. It needs a human decision: authenticate confidential clients at revocation. That is RFC 7009 §2.1, which conflicts with the unauthenticated-revocation posture recorded in this PR's body.

Codex reviewed this head. It has not re-reviewed the rebased heads of #2296, #3080 or #3081 within about 7 minutes of the push.

@jeremy
jeremy force-pushed the oauth-confidential-clients branch from 61d0f7c to 465118e Compare October 6, 2026 02:56
@jeremy

jeremy commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Rebased onto #2296's authorization-endpoint fix and #3081's canonical-scope commit. The only conflict was two helpers side by side in Oauth::BaseController. Content is otherwise unchanged.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 465118ed00

ℹ️ 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".

Comment thread app/controllers/oauth/tokens_controller.rb Outdated
jeremy added 13 commits October 17, 2026 01:08
…ache token errors

Refresh resolved the token before authenticating the client, so a wrong
secret answered invalid_client for a live refresh token and invalid_grant
for a dead one: an oracle for whoever holds a token without the secret.
The token endpoint now authenticates the client the request names, before
any code or refresh token is looked up. A request attempts authentication
when it names a confidential client or carries a client_secret, and must
then succeed; a request naming a public client attempts none, and a
confidential grant presented that way is refused as issued to another
client, the same answer as a dead grant. client_id is required up front
again (invalid_request), so its absence can't depend on the grant either.

prevent_caching ran as an after_action, which a halted callback chain
skips, so every error response from the token and registration endpoints
went out without Cache-Control: no-store. It is now a before_action.
RFC 6749 §2.3.1: the authorization server MUST support HTTP Basic for
clients issued a password. Oauth::ClientAuthentication reads either
transport. It base64-decodes the Basic value strictly, splits on the first
colon and form-urldecodes each half (Appendix B). A confidential client's
secret verifies whichever way it arrives; the registered method is advisory.

Basic plus a non-empty body client_secret is two methods at once, refused
as invalid_request (§2.3, §5.2). A malformed header, a body client_id naming
another client, or Basic naming a public client fails as invalid_client.
Every invalid_client is now a 401 carrying a Basic challenge (§5.2). The
earlier 400 was chosen because no header scheme existed to name in the
challenge, and Basic now is one. The pilot gate keys on the Basic id.

Discovery, DCR and the client model accept client_secret_basic. Mirrors
basecamp/bc3#13678.
RFC 7009 §2.1: the server first validates the client's credentials, then
checks that the token was issued to that client. Before, any holder of a
token could revoke the grant without the client's secret.

A confidential client authenticates with client_secret_post or
client_secret_basic. A public client names its client_id. Each revokes
only tokens issued to it. If the token is unknown, belongs to another
client or is a personal access token, the endpoint still answers 200 and
revokes nothing (§2.2). It also answers 200 when no client is identified,
as with a missing or unknown client_id. Only failed authentication is
refused, with 401 invalid_client: a known confidential client with a
missing or wrong secret, or a Basic header that doesn't authenticate.
That answer depends only on the request, never on whether the token
exists. Discovery advertises the three methods for revocation.

Mirrors basecamp/bc3's Oauth::RevocationsController.
A body client_secret with no client_id, or with an unknown one, is a failed
authentication attempt. Before this it got the 200 that means the token is
dead, so a client with a mistyped client_id would stop using a grant that
was still live. Revocation now uses the same rule as the token endpoint:
Basic, a confidential client, or any secret is an attempt, and an attempt
that fails gets 401. The rule turns on the request alone, so the refusal
says nothing about which clients exist.
A public client holds no password, so a body client_secret beside its
client_id is a failed authentication attempt, the same as a Basic header
naming it. Before this, the request identified the public client and got
200, even when the token belonged to another client and nothing was
revoked.
…eader

RFC 6749 §5.2 requires the WWW-Authenticate challenge on invalid_client when
the client attempted authentication through the Authorization header. A client
that sent its secret in the body never tried the header, so its 401 now carries
no challenge, at the token endpoint and at revocation alike. Any Authorization
header still earns one, since a 401 must name a scheme that applies and Basic
is the only one here.

Status codes are unchanged: invalid_client is 401 either way. This matches
basecamp/bc3 (Oauth::ClientAuthentication#challenge_client_authentication).
Restricting revocation to a client's own tokens also stopped personal access
tokens from being revoked there, since they belong to no client. Revocation
should work the same for every kind of token, and personal access tokens may
later become OAuth tokens themselves, as they are in Basecamp.

A personal access token has no client, so holding it is the credential: whoever
presents one revokes it. Client credentials sent along with it still have to
authenticate under the usual rules. A failed attempt gets 401 invalid_client
and revokes nothing, and Basic beside a body secret is still invalid_request.
OAuth tokens are unchanged: a client revokes only its own.
A request naming a public client makes no authentication attempt. Presenting a
confidential client's code or refresh token in that request got "Grant was not
issued to this client" when the grant was live, and "Invalid refresh token"
when it wasn't. The description was an oracle for whether someone else's
grant was live. Both cases now give the dead-grant answer.
Revocation now authenticates confidential clients: a wrong secret gets 401,
the right one gets 200. Unthrottled, that let anyone who knew a public
client_id guess its secret without limit, around the token endpoint's limit
of 20 per minute. Revocation now has the same per-IP limit (RFC 6749 §2.3.1,
RFC 7009 §5).
A failed Basic authentication stays a 401 challenging Basic: the endpoint
evaluated the Authorization header, so the challenge applies. Every other
invalid_client is now a 400 with no challenge: a failed client_secret_post,
a public client presenting a secret, a secret that authenticates no client
at revocation, or a body failure beside an Authorization header of another
scheme, which used to draw a Basic challenge for a scheme never tried.

- A bare 401 violates RFC 9110 §15.5.2, which requires a challenge.
- A Basic challenge for a request that never used Basic names a scheme
  that doesn't apply to what was tried.
- oauth4webapi and openid-client turn a challenged 401 into a
  WWWAuthenticateChallengeError instead of invalid_client.
- A 401 challenging Basic on a form POST can pop a browser's auth dialog.

Revocation keeps its stricter rule, refusing a presented secret that
authenticates no client, now with 400.
no-store covered the token and registration endpoints. Discovery,
consent, its redirects and revocation answered without it, and every
response sent the app's default Referrer-Policy. Now each OAuth
response, halted callback chains and the sign-in redirect included,
carries Cache-Control: no-store and Referrer-Policy: no-referrer.
"Authorization: Basic" with no credentials didn't match the scheme
pattern, so it counted as no attempt: a public client's exchange went
ahead, and revocation deleted a presented personal access token. Now it
fails as invalid_client with a Basic challenge, like any malformed
Basic header.
@jeremy
jeremy force-pushed the oauth-confidential-clients branch from dc2d791 to 274c566 Compare October 17, 2026 08:22
@jeremy
jeremy requested a balanced review from Copilot October 17, 2026 08:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Security-sensitive changes to client authentication and revocation boundaries warrant final human validation.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@jeremy

jeremy commented Oct 17, 2026

Copy link
Copy Markdown
Member Author

Review threads: 1 resolved (1 fixed: bare Basic scheme).

jeremy added a commit that referenced this pull request Oct 17, 2026
* commit '274c56688b1872aff9b09e10a645e03d54bdd826': (21 commits)
  Read the grant's refresh token before the bare-Basic attempts, not after reloading
  Fail a bare Basic scheme as client authentication, not ignore it
  Keep every authorization server response out of caches and Referers
  Answer invalid_client with 400 unless Basic authentication failed
  Throttle revocation like the token endpoint
  Answer a grant issued to another client exactly as a dead one
  Revoke personal access tokens at the revocation endpoint again
  Name the Basic scheme only to a client that tried the Authorization header
  Refuse a client secret that names a public client at revocation
  Refuse a revocation whose client secret authenticates no client
  Authenticate clients at revocation and revoke only their own tokens
  Accept client_secret_basic at the token endpoint
  Authenticate the named client before resolving the grant, and never cache token errors
  Pin that confidential clients sit behind the availability switches
  Authenticate confidential clients before checking the code's verifier and redirect_uri
  Answer failed client_secret_post authentication with 400, not 401
  Authenticate the client before validating the refresh grant
  Confine client credentials to the request body
  Require client_id with client_secret_post and declare secret expiry
  Reject non-string client secrets and mark credential responses no-store
  ...
…pping partway

* origin/oauth-refresh-tokens:
  Make the OAuth migrations safe to run again after stopping partway
* origin/oauth-refresh-tokens:
  Answer 404 from a dark endpoint however many times it's asked
* tp/oauth-refresh-tokens: (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-refresh-tokens:
  Bring main's tests up to this branch: Ruby 4 and the access token finder
* tp/oauth-refresh-tokens:
  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
… a secret

A confidential client's secret is stored as given, so every literal on oauth_clients is dropped from logs like the other credential tables'.
@jeremy

jeremy commented Oct 26, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 7427c08a50

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

* tp/oauth-refresh-tokens:
  Filter the code parameter by its whole name, so code_challenge stays readable
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.

2 participants