fix(coderd): explain default GitHub app org visibility on login rejection (#27374)

## Problem

On a fresh deployment with no custom GitHub OAuth app, Coder falls back
to the default Coder-managed GitHub app. That app can only see
organization memberships in organizations where it has been installed.
If `CODER_OAUTH2_GITHUB_ALLOWED_ORGS` is set but the app isn't installed
in the allowed organizations, the membership list comes back empty and
every login, including the first admin login, is rejected with a bare
"You aren't a member of the authorized Github organizations!" with no
hint about the actual cause. This leaves fresh deployments in an
apparently broken state.

## Fix

* Append a remediation hint to the login rejection when the default
provider is configured, pointing at the [app installation
page](<https://github.com/apps/coder/installations/select_target>) and
at configuring a custom GitHub OAuth app.
* Log a startup warning when the default provider is combined with
`CODER_OAUTH2_GITHUB_ALLOWED_ORGS`, listing the allowed orgs and the
install URL.
* Document the installation requirement next to the
`CODER_OAUTH2_GITHUB_ALLOWED_ORGS` step in the GitHub auth docs.

Access-control behavior is unchanged; the org check still rejects logins
as before, it just explains why and how to fix it.

## Testing

* New `TestUserOAuth2Github/NotInAllowedOrganizationDefaultProvider`
asserts the hint appears when `DefaultProviderConfigured` is set; the
existing `NotInAllowedOrganization` subtest asserts it does not leak
into the custom-app path.

Fixes coder/coder#17752
This commit is contained in:
Atif Ali
2026-07-21 20:43:10 +05:00
committed by GitHub
parent a9a1dcc65d
commit 48e9bb3391
4 changed files with 101 additions and 2 deletions
+57
View File
@@ -221,6 +221,63 @@ func TestUserOAuth2Github(t *testing.T) {
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
location, err := resp.Location()
require.NoError(t, err)
require.NotContains(t, location.Query().Get("message"), "Coder GitHub app")
})
t.Run("NotInAllowedOrganizationDefaultProvider", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &testutil.OAuth2Config{},
DefaultProviderConfigured: true,
AllowOrganizations: []string{"coder"},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
// The default provider is a GitHub App, so it reports no
// memberships for organizations it isn't installed in.
return []*github.Membership{}, nil
},
},
})
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
// The error must tell the user how to fix the likely cause: the Coder
// GitHub app isn't installed in the allowed organization.
location, err := resp.Location()
require.NoError(t, err)
require.Contains(t, location.Query().Get("message"), "Coder GitHub app")
require.Contains(t, location.Query().Get("message"), "https://github.com/apps/coder")
})
t.Run("NotInAllowedOrganizationDefaultProviderDeviceFlow", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &testutil.OAuth2Config{},
DefaultProviderConfigured: true,
AllowOrganizations: []string{"coder"},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{}, nil
},
DeviceFlowEnabled: true,
ExchangeDeviceCode: func(_ context.Context, _ string) (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: "access_token",
RefreshToken: "refresh_token",
Expiry: time.Now().Add(time.Hour),
}, nil
},
},
})
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
// In the device flow the error is rendered client-side, so the hint
// must arrive in the response body Detail rather than the redirect.
var apiErr codersdk.Response
require.NoError(t, json.NewDecoder(resp.Body).Decode(&apiErr))
require.Contains(t, apiErr.Detail, "Coder GitHub app")
require.Contains(t, apiErr.Detail, "https://github.com/apps/coder")
})
t.Run("NotInAllowedTeam", func(t *testing.T) {
t.Parallel()