Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
7e3f6c0
fix(coderd/oauth2provider): reject PKCE code_verifier below RFC 7636 …
BobbyHo Aug 10, 2026
a125238
fix(scripts/oauth2): generate PKCE verifiers at the RFC 7636 floor le…
BobbyHo Aug 10, 2026
eea4094
fix(coderd/oauth2provider): validate code_challenge format at authorize
BobbyHo Aug 10, 2026
e7d78d5
fix(coderd/oauth2provider): return invalid_request for malformed code…
BobbyHo Aug 10, 2026
fb7e90b
fix(coderd/oauth2provider/oauth2providertest): restore e2e coverage o…
BobbyHo Aug 11, 2026
4fe0b1b
fix(coderd/oauth2provider): revoke authorization code on PKCE failure
BobbyHo Aug 11, 2026
663865a
fix: resolve remaining coder-agents-review findings on PKCE hardening
BobbyHo Aug 11, 2026
912ce41
fix(docs/admin): document PKCE length and charset requirement
BobbyHo Aug 11, 2026
9440708
fix: allow bare custom-scheme redirects for public clients
BobbyHo Aug 11, 2026
450d037
fix(codersdk/oauth2_validation): state the real reason for the mailto…
BobbyHo Aug 12, 2026
8c4a1c0
feat: derive OAuth2 client type from token_endpoint_auth_method
BobbyHo Aug 11, 2026
1821ad4
fix: allow bare custom-scheme redirects for public clients (#28041)
BobbyHo Aug 12, 2026
c8b703d
Merge branch 'oauth2-pkce-verifier-length' into oauth2-public-clients…
BobbyHo Aug 12, 2026
39e4beb
fix: pin OAuth2 client type across RFC 7592 updates
BobbyHo Aug 12, 2026
df7135d
Merge remote-tracking branch 'origin/main' into oauth2-public-clients…
BobbyHo Aug 12, 2026
7c124e7
feat(coderd/oauth2provider): register public clients without a secret
BobbyHo Aug 11, 2026
4725dec
Merge branch 'main' into oauth2-public-clients-vocabulary
BobbyHo Aug 13, 2026
577538b
feat: accept PKCE-only token exchange for public clients
BobbyHo Aug 11, 2026
d93704d
docs: describe public client PKCE as proof of possession
BobbyHo Aug 15, 2026
aca591d
test(coderd): restructure public client token exchange and lifecycle …
BobbyHo Aug 15, 2026
240db54
refactor(coderd/oauth2provider): read IsPublic at the branch that use…
BobbyHo Aug 15, 2026
12e1cab
refactor(coderd/oauth2provider): scope the validated client secret to…
BobbyHo Aug 15, 2026
29ec017
docs(coderd/oauth2provider): trim the revocation ownership comments t…
BobbyHo Aug 15, 2026
b5457af
docs(coderd): trim the public-client test comments to what they assert
BobbyHo Aug 15, 2026
96083c0
feat: advertise the none token endpoint auth method in discovery
BobbyHo Aug 17, 2026
6e510b7
Merge branch 'main' into oauth2-public-clients-vocabulary
BobbyHo Aug 17, 2026
9198095
Merge branch 'oauth2-public-clients-vocabulary' into oauth2-public-cl…
BobbyHo Aug 17, 2026
5a0bc6b
Merge branch 'oauth2-public-clients-registration' into oauth2-public-…
BobbyHo Aug 17, 2026
432b2a5
docs: correct the public client token endpoint and redirect URI claims
BobbyHo Aug 17, 2026
16b45cc
refactor(codersdk): derive auth method validity from one list
BobbyHo Aug 17, 2026
a336906
docs(coderd/oauth2provider): describe what the code ownership check b…
BobbyHo Aug 17, 2026
cb8fb61
Merge branch 'oauth2-public-clients-registration' into oauth2-public-…
BobbyHo Aug 17, 2026
ca01e04
docs: drop the caveat that public clients cannot obtain a token
BobbyHo Aug 17, 2026
6acd7bc
docs(coderd/oauth2provider): use plainer wording in the client type c…
BobbyHo Aug 17, 2026
794ed0d
Merge branch 'oauth2-public-clients-vocabulary' into oauth2-public-cl…
BobbyHo Aug 17, 2026
38c3353
Merge branch 'main' into oauth2-public-clients-vocabulary
BobbyHo Aug 17, 2026
21fdfed
Merge branch 'oauth2-public-clients-vocabulary' into oauth2-public-cl…
BobbyHo Aug 17, 2026
b9992c5
Merge branch 'oauth2-public-clients-registration' into oauth2-public-…
BobbyHo Aug 17, 2026
83c02d7
Merge branch 'main' into oauth2-public-clients-vocabulary
BobbyHo Aug 18, 2026
e8cfef2
Merge branch 'main' into oauth2-public-clients-vocabulary
BobbyHo Aug 18, 2026
c347f5c
refactor(coderd/oauth2provider): name the client type change conjuncts
BobbyHo Aug 19, 2026
99e26eb
refactor(codersdk): derive token endpoint auth method Valid from the …
BobbyHo Aug 19, 2026
f3e05bd
Merge remote-tracking branch 'origin/oauth2-public-clients-vocabulary…
BobbyHo Aug 19, 2026
83a6495
Merge remote-tracking branch 'origin/main' into oauth2-public-clients…
BobbyHo Aug 19, 2026
3d40daa
Merge branch 'main' into oauth2-public-clients-registration
BobbyHo Aug 19, 2026
c87521b
Merge branch 'oauth2-public-clients-registration' into oauth2-public-…
BobbyHo Aug 19, 2026
a7e132c
Merge branch 'main' into oauth2-public-clients-token-exchange
BobbyHo Aug 20, 2026
c5b4461
Merge branch 'main' into oauth2-public-clients-token-exchange
BobbyHo Aug 21, 2026
aada1ca
docs(coderd/oauth2provider): name the checks that bind the exchange
BobbyHo Aug 21, 2026
f4516c3
Merge remote-tracking branch 'origin/main' into oauth2-public-clients…
BobbyHo Aug 21, 2026
02a7495
docs(codersdk): state what the token endpoint does with client type
BobbyHo Aug 21, 2026
05c0580
test(coderd/oauth2provider): pin the advertised auth methods
BobbyHo Aug 21, 2026
09a9cbe
docs: drop the public client aside from the code_verifier parameter
BobbyHo Aug 21, 2026
8255649
Merge branch 'main' into oauth2-public-clients-token-exchange
BobbyHo Aug 21, 2026
9f65876
test(coderd): drop positional anchors from public client comments
BobbyHo Aug 22, 2026
f2f0a9c
Merge branch 'main' into oauth2-public-clients-token-exchange
BobbyHo Aug 22, 2026
a938dad
feat: reject secret creation and fix reporting for public clients
BobbyHo Aug 13, 2026
ebb9b8e
test(coderd/oauth2provider): cover reported token_endpoint_auth_method
BobbyHo Aug 19, 2026
9627868
docs: drop the redirect URI matching note
BobbyHo Aug 19, 2026
d32f432
Merge branch 'main' into oauth2-public-clients-token-exchange
BobbyHo Aug 23, 2026
9cb8b0f
test(coderd/oauth2provider): pin the reported client_secret_post method
BobbyHo Aug 23, 2026
effb05b
fix(coderd): name the remedy when rejecting a public client secret
BobbyHo Aug 23, 2026
3990e0c
docs(coderd): trim duplicated public client rationale
BobbyHo Aug 23, 2026
1cf8857
Merge branch 'oauth2-public-clients-token-exchange' into oauth2-publi…
BobbyHo Aug 24, 2026
476ffe2
Merge branch 'main' into oauth2-public-clients-token-exchange
BobbyHo Aug 25, 2026
9c970ec
docs(coderd): drop godoc-style comments from the public client tests
BobbyHo Aug 25, 2026
84dc33c
docs(coderd): match the older RFC 7636 comment to the trimmed one
BobbyHo Aug 25, 2026
56c0c60
docs(coderd): drop the public client lifecycle test header
BobbyHo Aug 25, 2026
8eb0be5
docs(coderd): drop godoc-style comments from the public client guard …
BobbyHo Aug 25, 2026
ff4d0f2
Merge branch 'oauth2-public-clients-token-exchange' into oauth2-publi…
BobbyHo Aug 25, 2026
fe643c3
Merge branch 'main' into oauth2-public-clients-guards
BobbyHo Aug 31, 2026
2f5ec81
Merge branch 'main' into oauth2-public-clients-guards
BobbyHo Aug 31, 2026
8e8eee7
chore(coderd): simplify comments on public client guards
BobbyHo Aug 31, 2026
c85e462
docs(docs/admin/integrations): one sentence per line in new paragraphs
BobbyHo Aug 31, 2026
7e13ffa
Merge branch 'main' into oauth2-public-clients-guards
BobbyHo Aug 31, 2026
0034474
docs(docs/admin/integrations): qualify when the legacy auth method mi…
BobbyHo Aug 31, 2026
ccadf55
docs: document the 400 response for creating a public client secret
BobbyHo Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions coderd/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func (api *API) oAuth2ProviderAppSecrets() http.HandlerFunc {
// @Tags Enterprise
// @Param app path string true "App ID"
// @Success 200 {array} codersdk.OAuth2ProviderAppSecretFull
// @Failure 400 {object} codersdk.Response "Public clients cannot have secrets"
// @Router /api/v2/oauth2-provider/apps/{app}/secrets [post]
func (api *API) postOAuth2ProviderAppSecret() http.HandlerFunc {
return oauth2provider.CreateAppSecret(api.Database, api.Auditor.Load(), api.Logger)
Expand Down
25 changes: 25 additions & 0 deletions coderd/oauth2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,31 @@ func TestOAuth2ProviderAppSecrets(t *testing.T) {
_, err = client.OAuth2ProviderAppSecrets(ctx, apps.Default.ID)
require.Error(t, err)
})

t.Run("RejectsPublicClient", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)

oauth2providertest.EnableDCR(t, client)
app := oauth2providertest.RegisterPublicClient(t, client, "app-secrets-public", "http://localhost:8080/callback")
appID, err := uuid.Parse(app.ClientID)
require.NoError(t, err)

//nolint:gocritic // OAuth2 app management requires owner permission.
_, err = client.PostOAuth2ProviderAppSecret(ctx, appID)
// Check the status and message, since a bare require.Error would also
// pass for a request that never reached the guard.
var sdkError *codersdk.Error
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
require.Equal(t, http.StatusBadRequest, sdkError.StatusCode())
require.Contains(t, sdkError.Message, "public OAuth2 app")

// No partial secret was created.
//nolint:gocritic // OAuth2 app management requires owner permission.
secrets, err := client.OAuth2ProviderAppSecrets(ctx, appID)
require.NoError(t, err)
require.Empty(t, secrets)
})
}

func TestOAuth2ProviderTokenExchange(t *testing.T) {
Expand Down
12 changes: 12 additions & 0 deletions coderd/oauth2provider/app_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ func CreateAppSecret(db database.Store, auditor *audit.Auditor, logger slog.Logg
})
)
defer commitAudit()

// A secret here would do nothing. The token endpoint does not check
// it, and deleting it revokes no tokens, because a public client's
// tokens have a NULL app_secret_id.
if app.IsPublic() {
Comment thread
BobbyHo marked this conversation as resolved.
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Comment thread
BobbyHo marked this conversation as resolved.
Message: "Cannot create a client secret for a public OAuth2 app.",
Detail: "Public clients authenticate with PKCE and have no client secret. The client type is fixed at registration, so register a new confidential client instead.",
})
return
}

secret, err := GenerateSecret()
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Expand Down
25 changes: 22 additions & 3 deletions coderd/oauth2provider/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, audi
SoftwareVersion: app.SoftwareVersion.String,
GrantTypes: slice.StringEnums[codersdk.OAuth2ProviderGrantType](app.GrantTypes),
ResponseTypes: slice.StringEnums[codersdk.OAuth2ProviderResponseType](app.ResponseTypes),
TokenEndpointAuthMethod: codersdk.OAuth2TokenEndpointAuthMethod(app.TokenEndpointAuthMethod.String),
TokenEndpointAuthMethod: reportedAuthMethod(app),
Scope: app.Scope.String,
Contacts: app.Contacts,
RegistrationAccessToken: registrationToken,
Expand Down Expand Up @@ -262,7 +262,7 @@ func GetClientConfiguration(db database.Store) http.HandlerFunc {
SoftwareVersion: app.SoftwareVersion.String,
GrantTypes: slice.StringEnums[codersdk.OAuth2ProviderGrantType](app.GrantTypes),
ResponseTypes: slice.StringEnums[codersdk.OAuth2ProviderResponseType](app.ResponseTypes),
TokenEndpointAuthMethod: codersdk.OAuth2TokenEndpointAuthMethod(app.TokenEndpointAuthMethod.String),
TokenEndpointAuthMethod: reportedAuthMethod(app),
Scope: app.Scope.String,
Contacts: app.Contacts,
RegistrationAccessToken: "", // RFC 7592: Not returned in GET responses for security
Expand Down Expand Up @@ -418,7 +418,7 @@ func UpdateClientConfiguration(db database.Store, auditor *audit.Auditor, logger
SoftwareVersion: updatedApp.SoftwareVersion.String,
GrantTypes: slice.StringEnums[codersdk.OAuth2ProviderGrantType](updatedApp.GrantTypes),
ResponseTypes: slice.StringEnums[codersdk.OAuth2ProviderResponseType](updatedApp.ResponseTypes),
TokenEndpointAuthMethod: codersdk.OAuth2TokenEndpointAuthMethod(updatedApp.TokenEndpointAuthMethod.String),
TokenEndpointAuthMethod: reportedAuthMethod(updatedApp),
Scope: updatedApp.Scope.String,
Contacts: updatedApp.Contacts,
RegistrationAccessToken: "", // RFC 7592: Not returned for security
Expand Down Expand Up @@ -570,6 +570,25 @@ func RequireRegistrationAccessToken(db database.Store) func(http.Handler) http.H

// Helper functions for RFC 7591 Dynamic Client Registration

// reportedAuthMethod returns the token_endpoint_auth_method to report for an
// app: the stored value, unless it disagrees with the client type.
//
// Clients registered before Coder derived the type from the method can be
// stored as confidential with a method of "none". Reporting "none" would tell
// such a client to stop sending the secret its token exchange still requires.
// A client that sends the reported value back on its next update fixes the row.
func reportedAuthMethod(app database.OAuth2ProviderApp) codersdk.OAuth2TokenEndpointAuthMethod {
Comment thread
BobbyHo marked this conversation as resolved.
Comment thread
BobbyHo marked this conversation as resolved.
stored := codersdk.OAuth2TokenEndpointAuthMethod(app.TokenEndpointAuthMethod.String)
if stored.Valid() && (stored == codersdk.OAuth2TokenEndpointAuthMethodNone) == app.IsPublic() {
return stored
}
if app.IsPublic() {
return codersdk.OAuth2TokenEndpointAuthMethodNone
}
// RFC 7591 §2 default for a client with a secret.
return codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic
}

// generateClientCredentials generates a client secret for OAuth2 apps
func generateClientCredentials() (plaintext string, hashed []byte, err error) {
// Use the same pattern as existing OAuth2 app secrets
Expand Down
126 changes: 126 additions & 0 deletions coderd/oauth2provider/registration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -665,3 +665,129 @@ func TestUpdateClientConfiguration_LegacyAuthMethodMismatch(t *testing.T) {
})
}
}

func TestClientConfiguration_ReportedAuthMethod(t *testing.T) {
t.Parallel()

tests := []struct {
name string
clientType string
// An old row reads back empty whether it is NULL or an empty string.
stored sql.NullString
// Sent on the PUT. Must either match the stored method or agree with
// the client type, or the type-change guard rejects the update first.
resend codersdk.OAuth2TokenEndpointAuthMethod
want codersdk.OAuth2TokenEndpointAuthMethod
}{
Comment thread
BobbyHo marked this conversation as resolved.
{
// A client that sends back the stored method, not the reported one.
name: "ConfidentialStoringNoneResendingStored",
clientType: database.OAuth2ProviderAppClientTypeConfidential,
stored: sql.NullString{String: string(codersdk.OAuth2TokenEndpointAuthMethodNone), Valid: true},
resend: codersdk.OAuth2TokenEndpointAuthMethodNone,
want: codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic,
},
{
// A client that sends back what GET reported, which fixes the row.
name: "ConfidentialStoringNoneResendingReported",
clientType: database.OAuth2ProviderAppClientTypeConfidential,
stored: sql.NullString{String: string(codersdk.OAuth2TokenEndpointAuthMethodNone), Valid: true},
resend: codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic,
want: codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic,
},
{
name: "PublicStoringSecretMethodResendingStored",
clientType: database.OAuth2ProviderAppClientTypePublic,
stored: sql.NullString{String: string(codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic), Valid: true},
resend: codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic,
want: codersdk.OAuth2TokenEndpointAuthMethodNone,
},
{
name: "PublicStoringSecretMethodResendingReported",
clientType: database.OAuth2ProviderAppClientTypePublic,
stored: sql.NullString{String: string(codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic), Valid: true},
resend: codersdk.OAuth2TokenEndpointAuthMethodNone,
want: codersdk.OAuth2TokenEndpointAuthMethodNone,
},
{
// RFC 7591 §2 defaults an unset method to client_secret_basic.
name: "ConfidentialStoringNothing",
clientType: database.OAuth2ProviderAppClientTypeConfidential,
stored: sql.NullString{String: "", Valid: true},
resend: codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic,
want: codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic,
},
{
// The stored method agrees with the type but is not the type
// default, so this case fails if the default is returned instead.
name: "ConfidentialStoringClientSecretPost",
clientType: database.OAuth2ProviderAppClientTypeConfidential,
stored: sql.NullString{String: string(codersdk.OAuth2TokenEndpointAuthMethodClientSecretPost), Valid: true},
resend: codersdk.OAuth2TokenEndpointAuthMethodClientSecretPost,
want: codersdk.OAuth2TokenEndpointAuthMethodClientSecretPost,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)

db, _ := dbtestutil.NewDB(t)

// Seeded directly, since registration derives the type from the
// method and can no longer create a disagreeing row.
app := dbgen.OAuth2ProviderApp(t, db, database.OAuth2ProviderApp{
CallbackURL: "https://example.com/callback",
RedirectUris: []string{"https://example.com/callback"},
ClientType: tt.clientType,
TokenEndpointAuthMethod: tt.stored,
DynamicallyRegistered: sql.NullBool{Bool: true, Valid: true},
})

rctx := chi.NewRouteContext()
rctx.URLParams.Add("client_id", app.ID.String())
getReq := httptest.NewRequest(http.MethodGet, "/oauth2/clients/"+app.ID.String(), nil).
WithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx))
getRW := httptest.NewRecorder()

oauth2provider.GetClientConfiguration(db).ServeHTTP(getRW, getReq)
require.Equal(t, http.StatusOK, getRW.Code, "body: %s", getRW.Body.String())

var got codersdk.OAuth2ClientConfiguration
require.NoError(t, json.NewDecoder(getRW.Body).Decode(&got))
require.Equal(t, tt.want, got.TokenEndpointAuthMethod)

logger := slogtest.Make(t, nil)
auditor := audit.NewNop()
handler := tracing.StatusWriterMiddleware(oauth2provider.UpdateClientConfiguration(db, &auditor, logger))

body, err := json.Marshal(codersdk.OAuth2ClientRegistrationRequest{
RedirectURIs: []string{"https://example.com/callback"},
TokenEndpointAuthMethod: tt.resend,
})
require.NoError(t, err)

putRCtx := chi.NewRouteContext()
putRCtx.URLParams.Add("client_id", app.ID.String())
putReq := httptest.NewRequest(http.MethodPut, "/oauth2/clients/"+app.ID.String(),
bytes.NewReader(body)).WithContext(context.WithValue(ctx, chi.RouteCtxKey, putRCtx))
putReq.Header.Set("Content-Type", "application/json")
putRW := httptest.NewRecorder()

handler.ServeHTTP(putRW, putReq)
require.Equal(t, http.StatusOK, putRW.Code, "body: %s", putRW.Body.String())

var updated codersdk.OAuth2ClientConfiguration
require.NoError(t, json.NewDecoder(putRW.Body).Decode(&updated))
require.Equal(t, tt.want, updated.TokenEndpointAuthMethod)

// The row keeps what the client sent, so the response can still
// disagree with it.
stored, err := db.GetOAuth2ProviderAppByClientID(ctx, app.ID)
require.NoError(t, err)
require.Equal(t, string(tt.resend), stored.TokenEndpointAuthMethod.String)
require.Equal(t, tt.clientType, stored.ClientType)
})
}
}
8 changes: 8 additions & 0 deletions docs/admin/integrations/oauth2-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
CODER_EXPERIMENTS=oauth2
```

## Creating OAuth2 Applications

Check warning on line 35 in docs/admin/integrations/oauth2-provider.md

View workflow job for this annotation

GitHub Actions / lint-docs

Coder.GerundHeading

Heading starts with an -ing word ('Creating'); prefer the imperative ('Install') or the noun ('Installation'). See capitalization-and-punctuation.md#no-gerund-leading-headings.

### Method 1: Web UI

Expand Down Expand Up @@ -137,6 +137,14 @@
> also differs by client type. See
> [Callback URL schemes](#callback-url-schemes).

A client's type is fixed when it registers.
An RFC 7592 update that would move a client between public and confidential is rejected with `invalid_client_metadata`, since the client either holds a secret that would stop being required or has none and no way to be issued one.
Switching between `client_secret_basic` and `client_secret_post` is allowed, because both are confidential.
To change type, register a new client.

Clients registered with `token_endpoint_auth_method: none` before Coder honored it are stored as confidential and still require their `client_secret`.
Coder reports `client_secret_basic` for those clients so that what it reports matches what it enforces, and the mismatch clears the next time the client updates its registration using the value Coder reported.

If client authentication fails, the token endpoint returns **HTTP 401** with an OAuth2 `invalid_client` error and a `WWW-Authenticate: Basic realm="coder"` response header.

### Standard OAuth2 Flow
Expand Down Expand Up @@ -335,7 +343,7 @@
This is also how you remove clients that registered themselves while dynamic client registration was enabled.
Turning the setting off stops new registrations; it does not remove the ones already there.

## Testing and Development

Check warning on line 346 in docs/admin/integrations/oauth2-provider.md

View workflow job for this annotation

GitHub Actions / lint-docs

Coder.GerundHeading

Heading starts with an -ing word ('Testing'); prefer the imperative ('Install') or the noun ('Installation'). See capitalization-and-punctuation.md#no-gerund-leading-headings.

Coder provides comprehensive test scripts for OAuth2 development:

Expand Down
7 changes: 4 additions & 3 deletions docs/reference/api/enterprise.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading