Skip to content

feat: derive OAuth2 client type from token_endpoint_auth_method - #28043

Open
BobbyHo wants to merge 19 commits into
mainfrom
oauth2-public-clients-vocabulary
Open

feat: derive OAuth2 client type from token_endpoint_auth_method#28043
BobbyHo wants to merge 19 commits into
mainfrom
oauth2-public-clients-vocabulary

Conversation

@BobbyHo

@BobbyHo BobbyHo commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Adds an OAuth2 client type (public vs confidential, RFC 7591 §2) derived from the requested auth method instead of hardcoded confidential. The type is stored and guarded here, but no endpoint enforces on it yet; public behavior at the token endpoint follows in the next PR in the stack.

  • Client type is derived once and reused by both registration and redirect URI validation, so they can't disagree
  • IsPublic() fails closed: an unrecognized or missing value reads as confidential
  • RFC 7592 update (PUT) now rejects moving a client between public and confidential (400) instead of silently flipping it when the auth method is omitted
  • Discovery still doesn't advertise "none"; follows once the token endpoint honors it

Behavior by client shape

client_type is derived from token_endpoint_auth_method at POST and pinned at PUT. RFC 7592 GET/PUT authenticate with the registration access token, not the client secret, so neither endpoint reads a secret.

Registered with Stored client_type / method GET reports PUT that flips the method
omitted, or client_secret_basic confidential / client_secret_basic client_secret_basic none → 400 invalid_client_metadata
none (new) public / none none client_secret_* → 400 invalid_client_metadata
none (before this PR) confidential / none none either → 200, type stays confidential
  • PUT still replaces every other RFC 7591 field. client_type is the only pinned one; the method may move within a type (client_secret_basicclient_secret_post).
  • Row 3 is the only shape where the two columns disagree. The guard fires only on a method change that crosses the type line, so those clients keep managing themselves instead of being locked out of their own configuration endpoint.
  • The token endpoint does not consult client_type yet, so every client still authenticates with a secret and registration still issues one.

Split out of #27873, second in the stack (on top of #28041).
Refs https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client

…length floor

The token endpoint accepted any non-empty code_verifier, so a client
could authenticate with a one-character verifier. RFC 7636 §4.1 sets a
43 to 128 character floor over the unreserved character set. The
challenge travels in the authorization request URL and the code
travels in the redirect, both of which land in browser history,
referrer headers, and proxy logs, so an attacker holding those
brute-forces the verifier offline at whatever entropy the client
chose, with no server-side rate limit. A one-character verifier is a
one-character password, and the server should refuse it rather than
accept whatever the client picked.

ValidPKCEVerifier enforces the length and charset bounds before the
existing S256 comparison runs. The existing TestOAuth2InvalidPKCE test
already exercises a 14-character verifier end to end and continues to
pass, now rejected on length rather than on hash mismatch.
…ngth

tr -d "=+/" deleted every '+' and '/' character that happened to appear
in the base64 output instead of translating them to the URL-safe
alphabet, so cut -c -43 truncated a string that was often already
short. Roughly 70% of runs produced a verifier below the 43-character
floor coderd/oauth2provider now enforces (#28003), so the manual and
scripted OAuth2 flows these scripts drive failed token exchange
intermittently.

Use tr '+/' '-_' | tr -d '=' instead: translating first and then
stripping the single padding character is deterministic, since 32
random bytes always base64-encode to a fixed length. This always
yields exactly 43 characters, so the cut is no longer needed.
extractAuthorizeParams only checked code_challenge for non-emptiness, so
a malformed value (wrong length, disallowed characters, an arbitrarily
large blob) was persisted verbatim and only surfaced as a failure at
token exchange, with an error that misleadingly names code_verifier
instead of the parameter that was actually invalid.

RFC 7636 gives code_verifier and code_challenge the same ABNF, so reuse
the existing bounds check rather than adding a second one: rename
ValidPKCEVerifier to ValidPKCEFormat and validate code_challenge against
it in extractAuthorizeParams, rejecting a malformed value with
invalid_request at the authorization request per RFC 7636 §4.4.1.

TestExtractAuthorizeParams_Scopes used a 14-character placeholder
code_challenge that the new check now correctly rejects; lengthened it
to a valid value since that test only exercises scope parsing.
…_verifier

A malformed code_verifier (wrong length or disallowed characters) and a
well-formed verifier that simply fails the PKCE hash comparison both
returned the same error: invalid_grant, "The PKCE code verifier is
invalid." A client that sent a too-short verifier had no way to tell
that apart from a genuine hash mismatch, would re-check its SHA-256
computation, find nothing wrong, and retry the same bad verifier
indefinitely since invalid_grant conventionally signals "retry."

RFC 6749 §5.2 assigns a malformed parameter to invalid_request; RFC 7636
§4.6 reserves invalid_grant for the comparison failure specifically. Move
the code_verifier format check out of authorizationCodeGrant and into
extractTokenRequest, which already owns syntax validation for this grant
type, so the two failure modes return distinct, spec-accurate errors.

Several existing tests sent an empty or placeholder code_verifier
incidental to what they were actually testing (client_secret
requirements, scope parsing, malformed-code handling); updated them to
use a valid-length value so they still reach the behavior under test.
…f PKCE hash mismatch

InvalidCodeVerifier ("wrong-verifier", 14 chars) was rejected on length
before VerifyPKCE ever ran, so no test exercised the token endpoint's
hash-comparison branch end to end; TestVerifyPKCE unit-tests the
function, but nothing proved the endpoint still calls it.

Lengthen InvalidCodeVerifier to a well-formed but wrong 43-character
value so it again reaches the hash comparison. Add MalformedCodeVerifier
and a new test asserting the length-rejection path returns
invalid_request, now that the previous commit gives it a distinct error
from the hash-mismatch invalid_grant case.
The code was deleted only inside the success-path transaction, so every
PKCE rejection (errInvalidPKCE) left it live in the database. RFC 6749
§10.5 requires authorization codes to be single-use; without that, an
attacker holding a leaked code (the exact threat PKCE defends against,
since codes and challenges land in browser history, referrer headers, and
proxy logs) could retry the token endpoint with different code_verifier
guesses for the entire 10-minute code lifetime, unthrottled. The
43-character length floor bounds guess format, not entropy.

Add revokeOAuth2CodeOnPKCEFailure, called from both PKCE rejection paths
in authorizationCodeGrant. It deletes the code using the same system
authz context already used for reads in this function; a deletion
failure is noted on the request's log line rather than changing the
response, since surfacing it as a different error would let a caller
distinguish delete success from failure, itself a new oracle.

Added TestOAuth2PKCEFailureConsumesCode to verify the code is
unredeemable, even with the correct verifier, once a PKCE mismatch has
occurred.
Tighten the ValidPKCEFormat doc comment and correct a false claim
(CRF-8, CRF-10). The rationale restated the same threat model across
three separate rhetorical framings, and claimed PKCE is the only
client authentication some clients have, which is false today since
authorizationCodeGrant validates a client secret before PKCE ever
runs; that claim only becomes true once #27873 adds public clients.
Trim the paragraph to a single concrete why and note the caveat.

Delete four boundary-case comments in pkce_test.go (CRF-9). Each one
restated the case name and the strings.Repeat literal beside it; the
RFC provenance already lives on ValidPKCEFormat's doc comment and the
pkceVerifierMinLength/pkceVerifierMaxLength constants, so the comments
carried no information and would drift if either constant changed.

Replace an em-dash with a comma in a comment inside the block this
PR's PKCE-failure handling touches (CRF-2), per the repo's no-emdash
rule. It survived lint because the check scans only changed lines by
default, and this comment was pre-existing context rather than a line
this PR added.

Fix the PKCE example in docs/admin/integrations/oauth2-provider.md
(CRF-7). tr -d "=+/" deleted reserved base64 characters instead of
translating them to the URL-safe alphabet, so the example computed a
code_challenge that failed to verify roughly 74% of the time. Also
strip the newline openssl base64 inserts at its default 64-column
wrap, which the 96-byte verifier example crosses; the prior
cut -c1-128 never merged the wrapped lines back together either.
The PKCE Flow section showed how to generate a code_verifier and
code_challenge but never stated the bound now enforced server-side:
43 to 128 characters from the unreserved set [A-Za-z0-9-._~] (RFC
7636 §4.1). A value outside these bounds returns invalid_request,
at the token endpoint for code_verifier and at the authorization
endpoint for code_challenge.
@BobbyHo BobbyHo changed the title feat(codersdk,coderd/database): add OAuth2ClientType and derive it from auth method feat: add OAuth2ClientType and derive it from auth method Aug 11, 2026
isValidCustomScheme required a literal "." in the scheme for a public
client's redirect URI, so vscode://, jetbrains://, and cursor:// all
400'd while the identical schemes passed for a confidential client
through the separate, more permissive validateScheme. Native and CLI
apps, the population public clients exist for, register those exact
schemes with their OS.

Removed the extra restriction: validateScheme already blocks the
schemes that are actually dangerous in a redirect context, and RFC
8252 section 7.1 only recommends reverse-domain notation rather than
requiring it. PKCE, not the scheme's spelling, is what secures a
public client's redirect.

That removal also stopped rejecting mailto, tel, and sms for public
clients specifically, since validateScheme's dangerous-scheme
blocklist never covered them either. Those three hand off to a mail
client, dialer, or SMS app rather than returning control to the
client, so unlike vscode:// or jetbrains://, none of them can deliver
an authorization code. A public client's redirect URI scheme is its
only mechanism for regaining control, so they are rejected again here,
scoped specifically to public clients rather than folded into
validateScheme's blocklist, since they are harmless for a confidential
client's redirect.
@BobbyHo
BobbyHo force-pushed the oauth2-custom-scheme-fix branch from d95d4f9 to 9440708 Compare August 12, 2026 00:07
@BobbyHo
BobbyHo force-pushed the oauth2-public-clients-vocabulary branch from 0d8a377 to 01ec6b3 Compare August 12, 2026 00:18
…/tel/sms scope, not an invented one

The previous comment claimed mailto, tel, and sms are harmless for a
confidential client's redirect specifically. That is not true: the
client_secret only matters at token exchange, not at redirect
delivery, so nothing about being confidential changes what happens
when the browser is sent to one of these schemes. The actual reason
they are checked only in the isPublicClient branch is that
custom-scheme validation was already scoped there before this PR;
confidential clients were never subject to any scheme-shape check
here, independent of any judgment about these three schemes.
@BobbyHo
BobbyHo force-pushed the oauth2-public-clients-vocabulary branch from 01ec6b3 to 3d4b95e Compare August 12, 2026 01:33
@BobbyHo

BobbyHo commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-08-12 01:33 UTC by @BobbyHo

Review history
  • R1 (2026-08-12): 16 reviewers, 7 Nit, 2 Note, 1 P2, 4 P3, COMMENT. Review

deep-review v0.9.0 | Round 1 | 450d037..3d4b95e

Last posted: Round 1, 14 findings (1 P2, 4 P3, 7 Nit, 2 Note), COMMENT. Review

Finding inventory

Finding inventory: PR #28043

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Open codersdk/oauth2.go:274 AllOAuth2TokenEndpointAuthMethods doc claims discovery derives from it; discovery still hardcoded R1 Netero P3, Mafu-san P2, Mafuuu P3, Gon P2, Leorio P3, Knov P2, Razor P3, Meruem P3, Ryosuke P3, Kite Nit, Zoro Nit, Kurapika Nit, Hisoka Note Yes
CRF-2 P3 Open coderd/oauth2provider/registration.go:324 RFC 7592 PUT silently flips public client to confidential when token_endpoint_auth_method omitted R1 Hisoka P3, Kurapika Note, Razor Note Yes
CRF-3 P3 Open codersdk/oauth2.go:292 OAuth2ClientType doc claims token endpoint reads it; no code does yet R1 Mafu-san P3, Leorio P3 Yes
CRF-4 P3 Open coderd/database/constants.go:28 "do not delete" comment names phantom test TestCreateDynamicClientRegistration_ClientType R1 Leorio P3, Ryosuke P3, Gon P2, Razor Nit, Meruem Nit, Kite Nit, Zoro Nit, Mafuuu Nit, Hisoka Nit, Bisky Nit, Pariston Nit Yes
CRF-5 P3 Open coderd/database/constants.go:14 Comment describes client_type as "nullable column" but it's NOT NULL R1 Gon P2, Knov Nit, Meruem Nit Yes
CRF-6 Nit Open coderd/database/dbgen/dbgen.go:1736, coderd/database/dbauthz/dbauthz_test.go:5828 Test fixtures still write raw "confidential" literal, missed by rename R1 Robin Nit, Mafu-san Nit, Knov Nit, Razor Nit, Meruem Nit Yes
CRF-7 Nit Open coderd/oauth2provider/apps.go:101 Sibling magic string "client_secret_post" untouched next to the constant this PR introduced R1 Ryosuke Nit Yes
CRF-8 Nit Open codersdk/oauth2.go:284 Valid() uses slices.Contains, breaks parity with sibling switch-based Valid() methods and allocates a fresh slice on every call R1 Zoro Nit, Ryosuke Nit Yes
CRF-9 Nit Open codersdk/oauth2.go:572 ClientTypeFor has one caller (DetermineClientType); can inline unless follow-up needs the bare-method form R1 Zoro Note Yes
CRF-10 Nit Open coderd/database/modelmethods_internal_test.go:232 Table-driven cases key on tt.clientType; empty and whitespace cases render as anonymous or ambiguous sub-test names R1 Kite Nit Yes
CRF-11 Nit Open commit 3d4b95e subject Scope (codersdk,coderd/database) excludes touched paths; subject is 84 chars R1 Leorio Nit Yes
CRF-12 Nit Open codersdk/oauth2_test.go:21 // authMethod is the requested token_endpoint_auth_method. restates the field name R1 Gon P2 Yes
CRF-13 Note Open codersdk/oauth2_test.go:14, coderd/database/modelmethods_internal_test.go:229 ApplyDefaults-before-vs-after invariant for DetermineClientType is not pinned by a test that runs ApplyDefaults() with token_endpoint_auth_method="none" R1 Meruem Note Yes
CRF-14 Note Open codersdk/oauth2.go:300, coderd/database/modelmethods.go:690 OAuth2ClientType.Valid() and OAuth2ProviderApp.IsPublic() have no production caller in this PR (deferred to next stack PR) R1 Netero Note, Razor Note Yes

Round log

Round 1

Panel + Netero. 1 P2, 4 P3, 7 Nit, 2 Note. Reviewed against 450d037..3d4b95e. Panel: Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Gon, Leorio, Kurapika, Knov, Razor, Meruem, Ryosuke, Ging-Go, Robin, Kite, Zoro. Wildcards: Kite, Zoro.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot 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.

Vocabulary split at the right seam. codersdk.OAuth2ClientType + ClientTypeFor gives one owner of the auth-method-to-client-type mapping; Validate() reusing DetermineClientType() collapses the last drift between the type that decides RFC 8252 rules and the type stored on the app; IsPublic() fails closed on unrecognized values and is pinned by a whitespace/case/empty table test. constants.go explicitly bridges the SDK enum into the DB layer so the value the writer writes and the value IsPublic reads back cannot differ, and the pinning-test note (once its ghost reference is fixed) is exactly the shape long-lived docs should carry. Deferrals to the next PR in the stack are anchored in the commit body and Linear ENG-3029.

Severity summary: 1 P2, 4 P3, 7 Nit, 2 Note.

Two threads to pull before this merges:

  1. Every doc that names an invariant a follow-up PR enforces should say so in this PR's tense. AllOAuth2TokenEndpointAuthMethods (Both Valid() and the discovery metadata are derived from it) and OAuth2ClientType (the token endpoint reads it to decide whether a client secret is required) both describe the endpoint of the stack. Today registration accepts token_endpoint_auth_method=none and /.well-known/oauth-authorization-server does not advertise none, so the drift the first comment says cannot happen is present. Either wire metadata.go:39 to AllOAuth2TokenEndpointAuthMethods() and drop IsPublic()-based enforcement to the next PR only, or scale each doc to Valid() derives from this; discovery/token endpoint will in the follow-up. Gon's constants.go review nails the pattern: any sentence of the form 'X and Y derive from Z, so they cannot drift' must trace to code in this PR that makes the drift impossible.

  2. UpdateClientConfiguration at registration.go:324 now derives client_type on every PUT. Combined with ApplyDefaults() at line 288, a public client that PUTs any subset of its registration without re-asserting token_endpoint_auth_method: "none" silently rewrites its stored row to client_type=confidential and token_endpoint_auth_method=client_secret_basic. client_type is ActionTrack in enterprise/audit/table.go:320, so an audit entry gets written recording a change the client did not request. Nothing in this PR reads IsPublic() at runtime, so the flip is inert today; once the follow-up wires IsPublic() into the token endpoint's secret check, the same PUT locks the client into needing a secret it was designed not to hold. DetermineClientType's own docstring already names this shape ("an omitted field compares as "" and looks like a change the client did not request") but points at Validate(); the actual write is here.

From Hisoka: "DetermineClientType's own docstring warns exactly this shape [...] but it points at the wrong caller: Validate isn't the one that records the drift, UpdateClientConfiguration is."

Process: commit subject feat(codersdk,coderd/database): ... is 84 chars and its scope excludes coderd/oauth2provider/ and site/; either omit the scope (change is cross-cutting) or extend it to every touched path. See CRF-11.


coderd/database/dbgen/dbgen.go:1736

Nit [CRF-6] Test fixture still writes the raw literal "confidential" instead of the new database.OAuth2ProviderAppClientTypeConfidential. (Robin, Mafu-san, Knov, Razor, Meruem)

This PR's own constants.go comment says the constants exist so "the value registration writes and the value OAuth2ProviderApp.IsPublic reads back cannot disagree." coderd/oauth2provider/apps.go:95 and both registration.go sites were migrated to the constant/derived value; dbgen.go:1736 and coderd/database/dbauthz/dbauthz_test.go:5828 still spell the literal. Every seeded app that omits ClientType lands via the raw string, which is exactly the drift the constant was introduced to eliminate. Change to takeFirst(seed.ClientType, database.OAuth2ProviderAppClientTypeConfidential) (and the matching swap in dbauthz_test.go).

🤖

coderd/oauth2provider/apps.go:101

Nit [CRF-7] Sibling magic string left in place next to the one this PR just fixed. (Ryosuke)

Line 95 replaces the hardcoded "confidential" with database.OAuth2ProviderAppClientTypeConfidential, the correct move. Six lines down, TokenEndpointAuthMethod: sql.NullString{String: "client_secret_post", Valid: true} still spells the wire value inline, even though codersdk.OAuth2TokenEndpointAuthMethodClientSecretPost exists for exactly this. Same class of bug (writer-side wire literal), same file, same struct literal. Use string(codersdk.OAuth2TokenEndpointAuthMethodClientSecretPost) here so the "spell it once in codersdk" pattern this PR establishes covers both fields the admin-create path writes.

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread codersdk/oauth2.go Outdated
Comment thread coderd/oauth2provider/registration.go Outdated
Comment thread codersdk/oauth2.go Outdated
Comment thread coderd/database/constants.go Outdated
Comment thread coderd/database/constants.go
Comment thread coderd/database/modelmethods_internal_test.go
Comment thread coderd/database/modelmethods.go
Comment thread codersdk/oauth2_test.go
Comment thread codersdk/oauth2_test.go
Comment thread coderd/database/modelmethods.go
Split out of #27873 to make that PR smaller to review. Second in the
stack; adds the vocabulary the rest of the public-client work is built
on, with no behavioral change beyond what it stores.

RFC 7591 §2 / OAuth 2.1 §2.1 define two client types: a confidential
client authenticates with a secret, a public client authenticates with
PKCE alone. DetermineClientType() previously hardcoded "confidential"
regardless of the requested token_endpoint_auth_method. It now derives
the type via the new ClientTypeFor() mapping, which is the single owner
of the auth-method-to-client-type relationship: registration derives
the stored client_type from it, and redirect URI validation uses it to
pick which RFC 8252 rules apply, so the two cannot disagree about what
"public" means.

OAuth2ProviderApp.IsPublic() is the reader for the stored client_type
column, added alongside matching database constants so the value
registration writes and the value IsPublic reads back cannot drift.
An unset or unrecognized client type reads as confidential, so an app
can never skip client authentication by accident.

AllOAuth2TokenEndpointAuthMethods() is the single source Valid() reads
from, so what registration accepts is defined in one place. Discovery
metadata does not yet derive from it and still hardcodes its own list
without "none"; a follow-up PR wires the token endpoint to honor
"none", and only then should discovery advertise it too.

registration.go and app registration itself do not yet skip secret
issuance for a public client; that follows in the next PR in the
stack.
@BobbyHo
BobbyHo force-pushed the oauth2-public-clients-vocabulary branch from 3d4b95e to 8c4a1c0 Compare August 12, 2026 04:54
@BobbyHo BobbyHo changed the title feat: add OAuth2ClientType and derive it from auth method feat: derive OAuth2 client type from token_endpoint_auth_method Aug 12, 2026
Split out of #27873 to make that PR smaller to review. First in the
stack; the rest of the public-client work builds on this.

`isValidCustomScheme` required a literal `.` in the scheme for a public
client's redirect URI, so `vscode://`, `jetbrains://`, and `cursor://`
all 400'd while the identical schemes passed for a confidential client
through the separate, more permissive `validateScheme`. Native and CLI
apps, the population public clients exist for, register those exact
schemes with their OS.

Removed the extra restriction: `validateScheme` already blocks the
schemes that are actually dangerous in a redirect context, and RFC 8252
section 7.1 only recommends reverse-domain notation rather than
requiring it. PKCE, not the scheme's spelling, is what secures a public
client's redirect.

That removal also stopped rejecting `mailto`, `tel`, and `sms` for
public clients specifically, since `validateScheme`'s dangerous-scheme
blocklist never covered them either. Those three hand off to a mail
client, dialer, or SMS app rather than returning control to the
application that started the flow, so a public client registered with
one of them could never actually complete authorization. They are
rejected again here, scoped to public clients only because that is how
custom-scheme validation was already scoped before this change, not
because they are known to be safe for a confidential client's redirect;
confidential clients were never subject to any scheme-shape check beyond
`validateScheme` and remain so here.

Refs
https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client
Base automatically changed from oauth2-custom-scheme-fix to oauth2-pkce-verifier-length August 12, 2026 14:36
BobbyHo and others added 2 commits August 12, 2026 07:52
UpdateClientConfiguration wrote ClientType: string(req.DetermineClientType())
on every PUT, recomputed from the request instead of read from storage.
ApplyDefaults() fills an omitted token_endpoint_auth_method with
client_secret_basic, so a public client's PUT that only touched an
unrelated field (e.g. redirect_uris) silently converted it to
confidential, since DetermineClientType() can now return "public" where
it previously always returned "confidential".

A client's type is fixed at registration; RFC 7592 §2.2 permits
rejecting metadata the server will not accept. UpdateClientConfiguration
now rejects a PUT that would move a client between public and
confidential with 400 invalid_client_metadata, and carries the stored
client_type through verbatim rather than re-deriving it. A legacy row
whose stored client_type and token_endpoint_auth_method already
disagree can still manage itself, as long as the update does not also
ask to change the auth method.

ClientTypeFor(), extracted as its own function in the previous commit,
had exactly one caller and no second one materialized, so it is
inlined back into DetermineClientType().
@BobbyHo
BobbyHo marked this pull request as ready for review August 12, 2026 20:30
Base automatically changed from oauth2-pkce-verifier-length to main August 12, 2026 20:36
…-vocabulary

Resolves a conflict in coderd/oauth2provider/tokens.go: #28003 hardened
revokeOAuth2CodeOnPKCEFailure on main (detaches the delete from the
request context, treats sql.ErrNoRows as non-error) after this branch's
own copy of that function predated the hardening. Took main's version
in full; this branch made no independent edits to it.
@BobbyHo
BobbyHo requested a review from Emyrk August 13, 2026 14:53
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