Skip to content

feat: register public clients without a secret - #28046

Draft
BobbyHo wants to merge 1 commit into
oauth2-public-clients-vocabularyfrom
oauth2-public-clients-registration
Draft

feat: register public clients without a secret#28046
BobbyHo wants to merge 1 commit into
oauth2-public-clients-vocabularyfrom
oauth2-public-clients-registration

Conversation

@BobbyHo

@BobbyHo BobbyHo commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Split out of #27873 to make that PR smaller to review. Third in the stack (on top of #28043); this is the point where dynamic client registration actually produces a public client.

An RFC 7591 registration requesting token_endpoint_auth_method: "none" now skips secret generation entirely: no secret is minted, and the app is persisted with the client_type the previous PR in the stack derives from that auth method. Discovery advertises "none" as a supported method so a client can find out Coder will accept it.

Registration now writes the app and its secret in one transaction. They were two independently committed inserts, so a failure of the second left a permanently committed app that can never authenticate while still holding a registration access token. Pre-existing, but making a public client's "no secret row" a legitimate state removes the ability to spot the orphaned confidential case by inspection later, so it's fixed here alongside the rest of this change.

registration_client_uri now uses url.JoinPath instead of fmt.Sprintf, fixing a latent bug where an access URL configured with a trailing slash would mint //oauth2/clients/{id} as the client's management endpoint.

The token endpoint does not yet accept a public client's PKCE-only exchange; that follows in the next PR in the stack, so a client registered here cannot yet obtain a token. Dynamic client registration itself is off by default (GetOAuth2DCREnabled), so this is not user-visible until the next PR lands.

Refs https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client

@github-actions

Copy link
Copy Markdown

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

@BobbyHo BobbyHo changed the title feat(coderd/oauth2provider): register public clients without a secret feat: register public clients without a secret Aug 11, 2026
@BobbyHo
BobbyHo force-pushed the oauth2-public-clients-vocabulary branch from 0d8a377 to 01ec6b3 Compare August 12, 2026 00:18
@BobbyHo
BobbyHo force-pushed the oauth2-public-clients-registration branch from 872b5e3 to 7c8d3e5 Compare August 12, 2026 00:18
Split out of #27873 to make that PR smaller to review. Third in the
stack; this is the point where dynamic client registration actually
produces a public client.

An RFC 7591 registration requesting token_endpoint_auth_method: "none"
now skips secret generation entirely: no secret is minted, and the app
is persisted with the client_type the previous PR in the stack derives
from that auth method. Discovery advertises "none" as a supported
method so a client can find out Coder will accept it.

Registration now writes the app and its secret in one transaction. They
were two independently committed inserts, so a failure of the second
left a permanently committed app that can never authenticate while
still holding a registration access token. Pre-existing, but making a
public client's "no secret row" a legitimate state removes the ability
to spot the orphaned confidential case by inspection later, so it is
fixed here alongside the rest of this change.

The registration_client_uri now uses url.JoinPath instead of
fmt.Sprintf, fixing a latent bug where an access URL configured with a
trailing slash would mint "//oauth2/clients/{id}" as the client's
management endpoint.

The token endpoint does not yet accept a public client's PKCE-only
exchange; that follows in the next PR in the stack, so a client
registered here cannot yet obtain a token.
@BobbyHo
BobbyHo force-pushed the oauth2-public-clients-vocabulary branch from 01ec6b3 to 3d4b95e Compare August 12, 2026 01:33
@BobbyHo
BobbyHo force-pushed the oauth2-public-clients-registration branch from 7c8d3e5 to 800fda7 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): 20 reviewers, 5 Nit, 5 Note, 2 P2, 4 P3, COMMENT. Review

deep-review v0.9.0 | Round 1 | 3d4b95e..800fda7

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

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Open coderd/oauth2provider/registration.go:346 UpdateClientConfiguration flips client_type without reconciling secret rows R1 Knov P1, Chopper P2, Hisoka P2, Pariston P2, Mafuuu P2, Melody P2, Ryosuke P3, Razor P3, Kurapika P3, Meruem P3, Mafu-san P4 Yes
CRF-2 P2 Open docs/admin/integrations/oauth2-provider.md:130 Redirect-URI callout contradicts validateRedirectURIs (omits https, wrong loopback set, wrong confidential rule) R1 Zoro P2, Mafu-san P2, Mafuuu P3, Razor P3, Chopper Nit Yes
CRF-3 P3 Open coderd/oauth2provider/metadata.go:44 Discovery advertises "none" while /oauth2/tokens rejects any exchange without client_secret R1 Knov P2, Kurapika P3, Meruem P3, Melody P3, Mafuuu Note, Mafu-san Note, Pariston Note, Kite Note, Luffy Note Yes
CRF-4 P3 Open codersdk/oauth2.go:622 client_secret_expires_at has omitempty; RFC 7591 §3.2.1 requires the field when client_secret is issued R1 Chopper P3 Yes
CRF-5 P3 Open coderd/oauth2provider/registration.go:139 JoinPath trailing-slash fix has no regression test; revert to fmt.Sprintf would pass suite R1 Mafu-san P3, Chopper Nit, Meruem Nit, Ryosuke Note Yes
CRF-6 P3 Open coderd/oauth2provider/registration.go:149 "Create client secret - parse the formatted secret to get components" mislocates the operation (block parses to extract prefix; the secret was minted at line 82) R1 Gon P2 Yes
CRF-7 Nit Open coderd/oauth2provider/registration.go:109 Em-dash cleanup applied to two touched //nolint:gocritic lines but not to 10 sibling instances (registration.go 227,313,338,421,445,497; tokens.go 261,289,451,488) R1 Kite Nit Yes
CRF-8 Note Open coderd/oauth2provider/metadata_test.go:47 Contains-only assertion; ElementsMatch against AllOAuth2TokenEndpointAuthMethods would pin the drift invariant R1 Bisky Note Yes
CRF-9 Nit Open docs/admin/integrations/oauth2-provider.md:131 "the out-of-band URN" not spelled as urn:ietf:wg:oauth:2.0:oob R1 Leorio Nit Yes
CRF-10 Note Open coderd/oauth2provider/oauth2providertest/helpers.go:85 RegisterPublicClient has no in-tree caller + ctx-parameter signature drift from surrounding helpers R1 Netero Note, Chopper Note, Gon Note, Hisoka Nit, Luffy Note, Mafu-san Note, Razor Note, Zoro Note+Nit Yes
CRF-11 Nit Open coderd/oauth2provider/metadata.go:39 5-line comment duplicates godoc on codersdk.AllOAuth2TokenEndpointAuthMethods; trim to the why-not-what unique to this site R1 Gon P2 Yes
CRF-12 Nit Open coderd/oauth2provider/registration_test.go:159 Trailing "The docs promise the field is absent" sentence restates the opening two lines R1 Gon P2 Yes
CRF-13 Nit Open coderd/oauth2provider/registration.go:76 "// Generate client credentials." narrates the mechanism of the two lines that follow R1 Gon Nit Yes
CRF-14 Note Open coderd/oauth2provider/registration_test.go:249 Mock InsertOAuth2ProviderApp hardcodes ClientType=Confidential regardless of params; will drift silently if scaffolding is reused for a public request R1 Ryosuke Note Yes
CRF-15 Note Open coderd/oauth2provider/registration.go:107 now and uuid.New() captured outside InTx closure; latent footgun if anyone later wraps InTx with retry semantics R1 Pariston Note Yes
CRF-16 Note Open coderd/oauth2provider/registration_test.go:301 PublicClientSkipsSecretInsert overlaps NoneIsPublicWithNoSecret (mechanism vs outcome); decide whether both should live R1 Luffy Note, Zoro Note Yes

Round log

Round 1

Panel. Base 3d4b95e..800fda7. Netero first-pass (no P0-P2 findings; one Note on unused RegisterPublicClient), then 19-reviewer panel: Bisky, Chopper, Ging-Go, Gon, Hisoka, Kite, Knov, Komugi, Kurapika, Leorio, Mafu-san, Mafuuu, Melody, Meruem, Pariston, Razor, Robin, Ryosuke, plus wildcards Zoro and Luffy. 16 findings written to inventory: 2 P2, 5 P3, 4 Nit, 5 Note. Dominant convergent finding is CRF-1 (11 reviewers): the RFC 7592 PUT handler at registration.go:346 flips client_type without reconciling oauth2_provider_app_secrets, so the "public means no secret row" invariant this PR's InTx establishes at registration is broken by the sibling update path.

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.

The change stands up cleanly on the create side. Wrapping the app and secret inserts in a single InTx closes the pre-existing orphan-confidential race and does it with a two-mock-store test setup (mDB outer, mTx closure) that fails an insert issued off-transaction as an unexpected call, so the InTx contract is enforced by construction rather than by convention. Raw-body assertion on absent client_secret pins the RFC 7591 §3.2.1 wire contract rather than the decoded struct, which cannot distinguish absent from empty. Deriving discovery and registration from a single AllOAuth2TokenEndpointAuthMethods() list is the right single-owner narrowing for the advertised-vs-accepted pair. The JoinPath swap fixes a real latent double-slash bug.

From Hisoka on the dominant finding: "Bungee Gum. Pull the new InTx thread and it moves POST. It does not move PUT."

Severity: 2 P2, 5 P3, 4 Nit, 5 Note (16 findings).

Two P2 items need attention before this merges:

  • [CRF-1] (11 reviewers converged, one at P1) The RFC 7592 PUT handler at registration.go:346 is the sibling of the invariant this PR just installed on the create side. UpdateClientConfiguration recomputes client_type from token_endpoint_auth_method and writes it straight to the app row with no touch of oauth2_provider_app_secrets. Both directions reproduce broken states: public→confidential leaves an app with client_type='confidential' and zero secret rows (the exact permanently-uninhabitable state the create-side transaction exists to prevent), and confidential→public leaves an orphan secret row against a nominally public client. This PR is what makes those transitions load-bearing. Treating the PR as if no follow-up will ever touch this code, this needs a decision here: reject client_type changes on PUT (RFC 7592 does not require supporting them), or wire the same InTx pattern with matching secret insert/delete on the transition. If it must ship deferred, please file a linked issue rather than leaving it silent, because it cannot be agent-accepted as permanent.
  • [CRF-2] The new IMPORTANT callout at docs/admin/integrations/oauth2-provider.md:130 disagrees with validateRedirectURIs on three independently checkable points: it omits https:// to arbitrary hosts (the RFC 8252 §7.2 preferred method for native apps, which the code accepts), it lists only 127.0.0.1 for loopback while the validator also accepts localhost and [::1], and its "http to any other host" carve-out for confidential clients is wrong (confidential rejects non-loopback http too).

Two P3 items are pure regression-guard gaps that pin invariants your PR description names as goals: [CRF-5] no test sets a trailing-slash accessURL (a revert of url.JoinPath back to fmt.Sprintf passes the suite), and [CRF-8] TokenEndpointAuthMethodsSupported is asserted with require.Contains instead of require.ElementsMatch against codersdk.AllOAuth2TokenEndpointAuthMethods(). Both are one-line changes.

Stack-context observation: [CRF-3] (discovery advertises none while /oauth2/tokens rejects any exchange without client_secret) and [CRF-10] (RegisterPublicClient has no in-tree caller) both trace to the next PR in the stack. Reviewing this PR standalone, they need to be answered even if the eventual answer is "these ship together in a merge queue." Either hold "none" out of the advertised list until the token endpoint accepts it, or explicitly gate merging this PR on the follow-up landing in the same train. Same for the helper: fold into the PR that first uses it, or add a minimal exercise here.

Bundle observation: the InTx wrap and the JoinPath swap are two independent fixes riding along with the public-client feature. Each is defensible on its own reasoning and the PR description acknowledges the bundling. Called out only so the review record shows the bundle was noticed rather than missed.


coderd/oauth2provider/registration.go:346

P2 [CRF-1] The RFC 7592 PUT handler flips client_type without reconciling oauth2_provider_app_secrets, reintroducing the exact orphaned-confidential state this PR's InTx was written to prevent on the create side. (Knov P1, Hisoka P2, Chopper P2, Pariston P2, Mafuuu P2, Melody P2, Ryosuke P3, Razor P3, Kurapika P3, Meruem P3, Mafu-san P4)

From Hisoka: "Pull the new InTx thread and it moves POST. It does not move PUT. The invariant this PR just installed, 'public means no secret row', starts life legitimate on the POST side and is left undefended on the PUT side."

From Knov, the direct sequence: "1. Register a confidential client (POST); receives a client_secret and a secret row. 2. PUT the same client with token_endpoint_auth_method: \"none\". The row now has client_type = public, but the secret row still exists. Or the reverse: PUT a public client with token_endpoint_auth_method: \"client_secret_basic\". The row now has client_type = confidential, with no secret row, the exact state POST was just rewritten to prevent."

From Mafuuu on the security half of confidential→public: "Once the next PR in the stack teaches the token endpoint to accept a public client's PKCE-only exchange, that residual row is a stealth credential valid for a client that should authenticate with PKCE alone. Anyone who ever saw the original client_secret (support ticket, log, backup) can present it against a client the operator now considers public."

The PR description grounds the create-side transaction in exactly this invariant ("making a public client's 'no secret row' a legitimate state removes the ability to spot the orphaned confidential case by inspection later, so it's fixed here alongside the rest of this change"). This PR is what makes the class-of-bug reachable on the PUT side; before this PR, no dynamically-registered client was public in practice. The pattern-inheritance argument ("the update path already worked this way") does not carry: the precondition that made the pattern safe (client_type effectively constant) is exactly what this PR removes.

Narrowing options, in order of narrowness:

  • Reject a PUT whose req.DetermineClientType() differs from existingApp.ClientType with invalid_client_metadata (RFC 7592 §2 does not require the type to be mutable).
  • If transitions must be supported, do them inside InTx matching the create-side shape: delete secret rows on confidential→public, mint and insert a fresh secret on public→confidential. The response type OAuth2ClientConfiguration has no client_secret field, so the second direction cannot return the new secret without a wider surface change; that is a design signal in favor of the reject option.

Human decision needed: fix in this PR, file a linked issue that names the two reachable states, or explicitly document why the transition is safe. A silent defer is not one of the options.

🤖

codersdk/oauth2.go:622

P3 [CRF-4] client_secret_expires_at is int64 with omitempty, so 0 (the RFC 7591 wire value for "never expires") disappears from the JSON registration response for every confidential client. (Chopper P3)

From Chopper (verified by marshalling OAuth2ClientRegistrationResponse{ClientSecret: "x", ClientSecretExpiresAt: 0}: client_secret present, client_secret_expires_at absent): "RFC 7591 §3.2.1 says client_secret_expires_at is REQUIRED when client_secret is issued, and that 0 is the on-the-wire value for 'never expires.' The recipient is any integrator whose registration client validates against the RFC schema: they see a required key missing and either fall back to a wrong default or reject the response outright."

Pre-existing but on the exact signal this PR is fixing (the registration response's RFC 7591 conformance), so it belongs in scope here. Drop omitempty from ClientSecretExpiresAt; if you need to omit the field for public clients whose response has no client_secret, do it structurally (custom MarshalJSON, or set the whole field only when ClientSecret != "").

🤖

🤖 This review was automatically generated with Coder Agents.

If you use Dynamic Client Registration (RFC 7591) and omit `token_endpoint_auth_method`, clients default to `client_secret_basic`. To request `client_secret_post`, set `token_endpoint_auth_method` to `client_secret_post` in the registration request. To register a public client, set it to `none`: Coder issues no `client_secret`, and the registration response omits that field entirely.

> [!IMPORTANT]
> Public clients must use a loopback redirect (`http://127.0.0.1:{port}/...`),

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.

P2 [CRF-2] The public-client redirect-URI callout contradicts validateRedirectURIs on three independently checkable points. (Zoro P2, Mafu-san P2, Mafuuu P3, Razor P3, Chopper Nit)

From Zoro:

validateRedirectURIs in codersdk/oauth2_validation.go:127 accepts https:// for both client types (line 155 falls through with no further check when the scheme is https), yet the docs omit https entirely and say the only allowed shapes for a public client are loopback http, custom scheme, and urn:...:oob. A user reading this will assume a native app that redirects to a claimed https:// URL (the RFC 8252 §7.2 pattern) is unregisterable, which is not what the code enforces.

isLoopbackAddress accepts localhost, 127.0.0.1, and ::1 (line 311). The docs give only http://127.0.0.1:{port}/... as the loopback form.

The last sentence, "http redirects to any other host are rejected for public clients and are usable only by confidential ones," is wrong for the confidential side too. Line 165 rejects any confidential-client http:// URI whose host is not in isLocalhost (that adds .localhost). Non-loopback http:// is rejected for confidential clients as well.

From Mafuuu: TestCreateDynamicClientRegistration_ClientType/NoneIsPublicWithNoSecret in this PR registers a public client with RedirectURIs: []string{"https://example.com/callback"} and receives 201 Created, so the code allows exactly the shape the docs steer readers away from.

Replace the block with what the code actually enforces. Zoro proposed:

> [!IMPORTANT]
> Public clients must use one of:
> - `https://` to any host,
> - `http://` with a loopback host (`localhost`, `127.0.0.1`, `[::1]`),
> - a custom URI scheme (`myapp://callback`, `vscode://callback`), or
> - the `urn:ietf:wg:oauth:2.0:oob` out-of-band URN.
>
> `http://` to a non-loopback host is rejected. Confidential clients have
> the same restriction, except they also accept `.localhost` subdomains.

🤖

// Not gated on dcrEnabled: disabling registration stops new public
// clients being created but does not stop existing ones exchanging
// tokens, so the method remains supported.
TokenEndpointAuthMethodsSupported: codersdk.AllOAuth2TokenEndpointAuthMethods(),

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.

P3 [CRF-3] Discovery advertises "none" in token_endpoint_auth_methods_supported, but the token endpoint at tokens.go:96-101 still appends client_secret is required and cannot be empty for every authorization_code grant with an empty secret and never inspects client_type. (Knov P2, Kurapika P3, Meruem P3, Melody P3, Mafuuu Note, Mafu-san Note, Pariston Note, Kite Note, Luffy Note)

From Kurapika: "A conforming client that trusts .well-known/oauth-authorization-server, registers with token_endpoint_auth_method=none, gets no secret in the response (correct per RFC 7591 §3.2.1), and then attempts the token exchange will hit client_secret is required at parameter parsing, before authorizationCodeGrant even runs."

From Knov on the in-file comment's rationale ("disabling registration stops new public clients being created but does not stop existing ones exchanging tokens, so the method remains supported"): "No existing public client can exchange tokens today, so that rationale is aspirational, not a description of the tree."

The follow-up PR named in the description will make this coherent, but reviewing this PR standalone, discovery is telling a conforming client something the server will not do. DCR is off by default (GetOAuth2DCREnabled), but discovery is served unconditionally.

Options:

  • Hold "none" out of AllOAuth2TokenEndpointAuthMethods() until the token endpoint accepts it. Registration then rejects "none" (consistent with the token endpoint's current behavior) and this becomes a coherent no-op until the follow-up.
  • Introduce a separate AdvertisedTokenEndpointAuthMethods() filtered by what the token endpoint executes.
  • Land the discovery advertisement in the same PR as the token-endpoint change so the pair agrees at merge.

Also: revise the comment at metadata.go:39-42. As written, it justifies the choice with a state the tree does not currently produce.

🤖

// JoinPath, not Sprintf: an access URL configured with a
// trailing slash would otherwise mint "//oauth2/clients/{id}"
// and hand it to the client as its management endpoint.
RegistrationClientUri: sql.NullString{String: accessURL.JoinPath("/oauth2/clients", clientID.String()).String(), Valid: true},

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.

P3 [CRF-5] The fmt.SprintfaccessURL.JoinPath fix has no regression test. (Mafu-san P3, Chopper Nit, Meruem Nit, Ryosuke Note)

From Mafu-san: "None of the three new tests uses a trailing-slash accessURL... Revert accessURL.JoinPath(\"/oauth2/clients\", clientID.String()) to the old fmt.Sprintf and every test still passes. The fix is real (verified against the code) but the regression guard is not."

The PR description names this fix explicitly ("a latent bug where an access URL configured with a trailing slash would mint //oauth2/clients/{id} as the client's management endpoint"), so it is worth pinning. Add one subtest row that sets accessURL to https://example.com/ (trailing slash) and asserts the response's RegistrationClientURI contains exactly one slash between authority and /oauth2/clients/. Same pattern would fail against path.Join as well.

🤖

return nil
}

// Create client secret - parse the formatted secret to get components

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.

P3 [CRF-6] // Create client secret - parse the formatted secret to get components mislocates the operation. (Gon P2)

From Gon: "'Create client secret' is wrong at this location. The secret was already generated on line ~82 by generateClientCredentials(). The block that follows parses that string to pull the prefix out, then inserts a DB row for it. It creates a row, not a secret. 'parse the formatted secret to get components' is what the function name ParseFormattedSecret says."

A future maintainer reading this hunts for a secret-minting call that is not there. Delete or replace with one line naming what the parse is for (extracting Prefix for InsertOAuth2ProviderAppSecretParams.SecretPrefix).

🤖

var app database.OAuth2ProviderApp
err = db.InTx(func(tx database.Store) error {
var err error
//nolint:gocritic // OAuth2 system context, dynamic registration is a public endpoint

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.

Nit [CRF-7] The em-dash → comma cleanup is applied to the two //nolint:gocritic comments this change touched but not to the ten sibling instances in the same file and package. (Kite Nit)

From Kite: "Six identical // OAuth2 system context — RFC 7592 client configuration endpoint comments remain in registration.go at lines 227, 313, 338, 421, 445, 497, and four more in tokens.go at lines 261, 289, 451, 488. scripts/check_emdash.sh only scans added lines in the diff, so these siblings sit under the linter's radar but violate the same rule the change is enforcing on the lines it touched."

One-character change each. Doing them now stops the class re-appearing in a future diff against those lines.

🤖

auditor := audit.NewNop()
handler := tracing.StatusWriterMiddleware(oauth2provider.CreateDynamicClientRegistration(db, accessURL, &auditor, logger))

body, err := json.Marshal(tt.req)

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.

Nit [CRF-12] The trailing sentence "The docs promise the field is absent, so that is what to pin." restates the opening "client_secret is omitted entirely for a client that was not issued one." (Gon P2)

From Gon: "Both sentences deliver the same fact (RFC 7591 §3.2.1 requires absence, not empty). Drop the last sentence; keep the middle two, which teach the actual why (decoded struct can't distinguish absent from zero)."

🤖

// Generate client credentials
clientType := req.DetermineClientType()
isPublic := clientType == codersdk.OAuth2ClientTypePublic

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.

Nit [CRF-13] // Generate client credentials. narrates the mechanism of the two lines that follow it. (Gon Nit)

From Gon: "The payload of the block is the second sentence ('Public clients authenticate with PKCE alone and never receive a secret'). Drop the leading section-header sentence and keep the rationale:

// Public clients skip secret generation and authenticate with PKCE
// alone (RFC 7591 §2, OAuth 2.1 §2.1).
```"

> 🤖

// outer store is distinguishable from one made on tx.
mTx := dbmock.NewMockStore(ctrl)

mDB.EXPECT().GetOAuth2DCREnabled(gomock.Any()).Return(true, nil).Times(1)

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.

Note [CRF-14] The transaction test's mock InsertOAuth2ProviderApp return hardcodes ClientType: OAuth2ProviderAppClientTypeConfidential regardless of what the handler asked for. (Ryosuke Note)

From Ryosuke: "Not a bug in this PR, it happens to line up with the confidential request the test sends, but it means the mock is not honoring the parameters. If a later change reuses this test scaffolding for a public request without noticing, the assertion drifts silently. Echoing back params.ClientType in the DoAndReturn would keep the mock and the handler in step."

🤖

// write would commit an app that can never authenticate, and which
// still holds a registration access token.
var app database.OAuth2ProviderApp
err = db.InTx(func(tx database.Store) error {

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.

Note [CRF-15] now := dbtime.Now() and uuid.New() for the secret ID are captured outside the InTx closure. (Pariston Note)

From Pariston: "Not currently a bug: InTx(fn, nil) runs at the driver's default isolation and does not retry on serialization failure, so the captured values are used exactly once. Worth knowing because if anyone later passes a TxOptions with retry semantics, or wraps this in a retry loop, the same secret ID would be reused across attempts and the second attempt would fail on uniqueness. Moving the uuid.New() and dbtime.Now() calls inside the closure would remove that footgun for the cost of two lines."

🤖

}
}

// TestCreateDynamicClientRegistration_PublicClientSkipsSecretInsert verifies

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.

Note [CRF-16] TestCreateDynamicClientRegistration_PublicClientSkipsSecretInsert overlaps TestCreateDynamicClientRegistration_ClientType/NoneIsPublicWithNoSecret on the same behavior. (Luffy Note, Zoro Note)

From Zoro: "The real-DB test at line 137 already asserts require.Empty(t, secrets) after registering with none, which fails if the handler calls InsertOAuth2ProviderAppSecret at all (an insert with the empty prefix/hash still produces a row that GetOAuth2ProviderAppSecretsByAppID returns). The mock test adds one increment: it fails at the call site rather than at the read side."

From Luffy: "The mock test pins the mechanism (no call was made). The parametric test pins the outcome (no row exists). Users see the outcome. Drop the mock test, one behavior test is enough."

Call out whether the two tests are meant to hold different invariants. If not, drop the mock one; if yes, name the invariant in the mock test's comment so a later reader does not merge them.

🤖

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