diff --git a/cli/server.go b/cli/server.go index 4805bf4b64d22..933ab64ab267a 100644 --- a/cli/server.go +++ b/cli/server.go @@ -688,24 +688,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } } - if vals.OAuth2.Github.ClientSecret != "" || vals.OAuth2.Github.DeviceFlow.Value() { - options.GithubOAuth2Config, err = configureGithubOAuth2( - oauthInstrument, - vals.AccessURL.Value(), - vals.OAuth2.Github.ClientID.String(), - vals.OAuth2.Github.ClientSecret.String(), - vals.OAuth2.Github.DeviceFlow.Value(), - vals.OAuth2.Github.AllowSignups.Value(), - vals.OAuth2.Github.AllowEveryone.Value(), - vals.OAuth2.Github.AllowedOrgs, - vals.OAuth2.Github.AllowedTeams, - vals.OAuth2.Github.EnterpriseBaseURL.String(), - ) - if err != nil { - return xerrors.Errorf("configure github oauth2: %w", err) - } - } - // As OIDC clients can be confidential or public, // we should only check for a client id being set. // The underlying library handles the case of no @@ -793,6 +775,20 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("set deployment id: %w", err) } + githubOAuth2ConfigParams, err := getGithubOAuth2ConfigParams(ctx, options.Database, vals) + if err != nil { + return xerrors.Errorf("get github oauth2 config params: %w", err) + } + if githubOAuth2ConfigParams != nil { + options.GithubOAuth2Config, err = configureGithubOAuth2( + oauthInstrument, + githubOAuth2ConfigParams, + ) + if err != nil { + return xerrors.Errorf("configure github oauth2: %w", err) + } + } + options.RuntimeConfig = runtimeconfig.NewManager() // This should be output before the logs start streaming. @@ -1843,25 +1839,101 @@ func configureCAPool(tlsClientCAFile string, tlsConfig *tls.Config) error { return nil } -// TODO: convert the argument list to a struct, it's easy to mix up the order of the arguments -// +const ( + // Client ID for https://github.com/apps/coder + GithubOAuth2DefaultProviderClientID = "Iv1.6a2b4b4aec4f4fe7" + GithubOAuth2DefaultProviderAllowEveryone = true + GithubOAuth2DefaultProviderDeviceFlow = true +) + +type githubOAuth2ConfigParams struct { + accessURL *url.URL + clientID string + clientSecret string + deviceFlow bool + allowSignups bool + allowEveryone bool + allowOrgs []string + rawTeams []string + enterpriseBaseURL string +} + +func getGithubOAuth2ConfigParams(ctx context.Context, db database.Store, vals *codersdk.DeploymentValues) (*githubOAuth2ConfigParams, error) { + params := githubOAuth2ConfigParams{ + accessURL: vals.AccessURL.Value(), + clientID: vals.OAuth2.Github.ClientID.String(), + clientSecret: vals.OAuth2.Github.ClientSecret.String(), + deviceFlow: vals.OAuth2.Github.DeviceFlow.Value(), + allowSignups: vals.OAuth2.Github.AllowSignups.Value(), + allowEveryone: vals.OAuth2.Github.AllowEveryone.Value(), + allowOrgs: vals.OAuth2.Github.AllowedOrgs.Value(), + rawTeams: vals.OAuth2.Github.AllowedTeams.Value(), + enterpriseBaseURL: vals.OAuth2.Github.EnterpriseBaseURL.String(), + } + + // If the user manually configured the GitHub OAuth2 provider, + // we won't add the default configuration. + if params.clientID != "" || params.clientSecret != "" || params.enterpriseBaseURL != "" { + return ¶ms, nil + } + + // Check if the user manually disabled the default GitHub OAuth2 provider. + if !vals.OAuth2.Github.DefaultProviderEnable.Value() { + return nil, nil //nolint:nilnil + } + + // Check if the deployment is eligible for the default GitHub OAuth2 provider. + // We want to enable it only for new deployments, and avoid enabling it + // if a deployment was upgraded from an older version. + // nolint:gocritic // Requires system privileges + defaultEligible, err := db.GetOAuth2GithubDefaultEligible(dbauthz.AsSystemRestricted(ctx)) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get github default eligible: %w", err) + } + defaultEligibleNotSet := errors.Is(err, sql.ErrNoRows) + + if defaultEligibleNotSet { + // nolint:gocritic // User count requires system privileges + userCount, err := db.GetUserCount(dbauthz.AsSystemRestricted(ctx)) + if err != nil { + return nil, xerrors.Errorf("get user count: %w", err) + } + // We check if a deployment is new by checking if it has any users. + defaultEligible = userCount == 0 + // nolint:gocritic // Requires system privileges + if err := db.UpsertOAuth2GithubDefaultEligible(dbauthz.AsSystemRestricted(ctx), defaultEligible); err != nil { + return nil, xerrors.Errorf("upsert github default eligible: %w", err) + } + } + + if !defaultEligible { + return nil, nil //nolint:nilnil + } + + params.clientID = GithubOAuth2DefaultProviderClientID + params.allowEveryone = GithubOAuth2DefaultProviderAllowEveryone + params.deviceFlow = GithubOAuth2DefaultProviderDeviceFlow + + return ¶ms, nil +} + //nolint:revive // Ignore flag-parameter: parameter 'allowEveryone' seems to be a control flag, avoid control coupling (revive) -func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, clientID, clientSecret string, deviceFlow, allowSignups, allowEveryone bool, allowOrgs []string, rawTeams []string, enterpriseBaseURL string) (*coderd.GithubOAuth2Config, error) { - redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback") +func configureGithubOAuth2(instrument *promoauth.Factory, params *githubOAuth2ConfigParams) (*coderd.GithubOAuth2Config, error) { + redirectURL, err := params.accessURL.Parse("/api/v2/users/oauth2/github/callback") if err != nil { return nil, xerrors.Errorf("parse github oauth callback url: %w", err) } - if allowEveryone && len(allowOrgs) > 0 { + if params.allowEveryone && len(params.allowOrgs) > 0 { return nil, xerrors.New("allow everyone and allowed orgs cannot be used together") } - if allowEveryone && len(rawTeams) > 0 { + if params.allowEveryone && len(params.rawTeams) > 0 { return nil, xerrors.New("allow everyone and allowed teams cannot be used together") } - if !allowEveryone && len(allowOrgs) == 0 { + if !params.allowEveryone && len(params.allowOrgs) == 0 { return nil, xerrors.New("allowed orgs is empty: must specify at least one org or allow everyone") } - allowTeams := make([]coderd.GithubOAuth2Team, 0, len(rawTeams)) - for _, rawTeam := range rawTeams { + allowTeams := make([]coderd.GithubOAuth2Team, 0, len(params.rawTeams)) + for _, rawTeam := range params.rawTeams { parts := strings.SplitN(rawTeam, "/", 2) if len(parts) != 2 { return nil, xerrors.Errorf("github team allowlist is formatted incorrectly. got %s; wanted /", rawTeam) @@ -1873,8 +1945,8 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl } endpoint := xgithub.Endpoint - if enterpriseBaseURL != "" { - enterpriseURL, err := url.Parse(enterpriseBaseURL) + if params.enterpriseBaseURL != "" { + enterpriseURL, err := url.Parse(params.enterpriseBaseURL) if err != nil { return nil, xerrors.Errorf("parse enterprise base url: %w", err) } @@ -1893,8 +1965,8 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl } instrumentedOauth := instrument.NewGithub("github-login", &oauth2.Config{ - ClientID: clientID, - ClientSecret: clientSecret, + ClientID: params.clientID, + ClientSecret: params.clientSecret, Endpoint: endpoint, RedirectURL: redirectURL.String(), Scopes: []string{ @@ -1906,17 +1978,17 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl createClient := func(client *http.Client, source promoauth.Oauth2Source) (*github.Client, error) { client = instrumentedOauth.InstrumentHTTPClient(client, source) - if enterpriseBaseURL != "" { - return github.NewEnterpriseClient(enterpriseBaseURL, "", client) + if params.enterpriseBaseURL != "" { + return github.NewEnterpriseClient(params.enterpriseBaseURL, "", client) } return github.NewClient(client), nil } var deviceAuth *externalauth.DeviceAuth - if deviceFlow { + if params.deviceFlow { deviceAuth = &externalauth.DeviceAuth{ Config: instrumentedOauth, - ClientID: clientID, + ClientID: params.clientID, TokenURL: endpoint.TokenURL, Scopes: []string{"read:user", "read:org", "user:email"}, CodeURL: endpoint.DeviceAuthURL, @@ -1925,9 +1997,9 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl return &coderd.GithubOAuth2Config{ OAuth2Config: instrumentedOauth, - AllowSignups: allowSignups, - AllowEveryone: allowEveryone, - AllowOrganizations: allowOrgs, + AllowSignups: params.allowSignups, + AllowEveryone: params.allowEveryone, + AllowOrganizations: params.allowOrgs, AllowTeams: allowTeams, AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) { api, err := createClient(client, promoauth.SourceGitAPIAuthUser) @@ -1966,19 +2038,20 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl team, _, err := api.Teams.GetTeamMembershipBySlug(ctx, org, teamSlug, username) return team, err }, - DeviceFlowEnabled: deviceFlow, + DeviceFlowEnabled: params.deviceFlow, ExchangeDeviceCode: func(ctx context.Context, deviceCode string) (*oauth2.Token, error) { - if !deviceFlow { + if !params.deviceFlow { return nil, xerrors.New("device flow is not enabled") } return deviceAuth.ExchangeDeviceCode(ctx, deviceCode) }, AuthorizeDevice: func(ctx context.Context) (*codersdk.ExternalAuthDevice, error) { - if !deviceFlow { + if !params.deviceFlow { return nil, xerrors.New("device flow is not enabled") } return deviceAuth.AuthorizeDevice(ctx) }, + DefaultProviderConfigured: params.clientID == GithubOAuth2DefaultProviderClientID, }, nil } diff --git a/cli/server_test.go b/cli/server_test.go index d9716377501cb..d4031faf94fbe 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -45,6 +45,8 @@ import ( "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/cli/config" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/migrations" "github.com/coder/coder/v2/coderd/httpapi" @@ -306,6 +308,145 @@ func TestServer(t *testing.T) { require.Less(t, numLines, 20) }) + t.Run("OAuth2GitHubDefaultProvider", func(t *testing.T) { + type testCase struct { + name string + githubDefaultProviderEnabled string + githubClientID string + githubClientSecret string + expectGithubEnabled bool + expectGithubDefaultProviderConfigured bool + createUserPreStart bool + createUserPostRestart bool + } + + runGitHubProviderTest := func(t *testing.T, tc testCase) { + t.Parallel() + if !dbtestutil.WillUsePostgres() { + t.Skip("test requires postgres") + } + + ctx, cancelFunc := context.WithCancel(testutil.Context(t, testutil.WaitLong)) + defer cancelFunc() + + dbURL, err := dbtestutil.Open(t) + require.NoError(t, err) + db, _ := dbtestutil.NewDB(t, dbtestutil.WithURL(dbURL)) + + if tc.createUserPreStart { + _ = dbgen.User(t, db, database.User{}) + } + + args := []string{ + "server", + "--postgres-url", dbURL, + "--http-address", ":0", + "--access-url", "https://example.com", + } + if tc.githubClientID != "" { + args = append(args, fmt.Sprintf("--oauth2-github-client-id=%s", tc.githubClientID)) + } + if tc.githubClientSecret != "" { + args = append(args, fmt.Sprintf("--oauth2-github-client-secret=%s", tc.githubClientSecret)) + } + if tc.githubClientID != "" || tc.githubClientSecret != "" { + args = append(args, "--oauth2-github-allow-everyone") + } + if tc.githubDefaultProviderEnabled != "" { + args = append(args, fmt.Sprintf("--oauth2-github-default-provider-enable=%s", tc.githubDefaultProviderEnabled)) + } + + inv, cfg := clitest.New(t, args...) + errChan := make(chan error, 1) + go func() { + errChan <- inv.WithContext(ctx).Run() + }() + accessURLChan := make(chan *url.URL, 1) + go func() { + accessURLChan <- waitAccessURL(t, cfg) + }() + + var accessURL *url.URL + select { + case err := <-errChan: + require.NoError(t, err) + case accessURL = <-accessURLChan: + require.NotNil(t, accessURL) + } + + client := codersdk.New(accessURL) + + authMethods, err := client.AuthMethods(ctx) + require.NoError(t, err) + require.Equal(t, tc.expectGithubEnabled, authMethods.Github.Enabled) + require.Equal(t, tc.expectGithubDefaultProviderConfigured, authMethods.Github.DefaultProviderConfigured) + + cancelFunc() + select { + case err := <-errChan: + require.NoError(t, err) + case <-time.After(testutil.WaitLong): + t.Fatal("server did not exit") + } + + if tc.createUserPostRestart { + _ = dbgen.User(t, db, database.User{}) + } + + // Ensure that it stays at that setting after the server restarts. + inv, cfg = clitest.New(t, args...) + clitest.Start(t, inv) + accessURL = waitAccessURL(t, cfg) + client = codersdk.New(accessURL) + + ctx = testutil.Context(t, testutil.WaitLong) + authMethods, err = client.AuthMethods(ctx) + require.NoError(t, err) + require.Equal(t, tc.expectGithubEnabled, authMethods.Github.Enabled) + require.Equal(t, tc.expectGithubDefaultProviderConfigured, authMethods.Github.DefaultProviderConfigured) + } + + for _, tc := range []testCase{ + { + name: "NewDeployment", + expectGithubEnabled: true, + expectGithubDefaultProviderConfigured: true, + createUserPreStart: false, + createUserPostRestart: true, + }, + { + name: "ExistingDeployment", + expectGithubEnabled: false, + expectGithubDefaultProviderConfigured: false, + createUserPreStart: true, + createUserPostRestart: false, + }, + { + name: "ManuallyDisabled", + githubDefaultProviderEnabled: "false", + expectGithubEnabled: false, + expectGithubDefaultProviderConfigured: false, + }, + { + name: "ConfiguredClientID", + githubClientID: "123", + expectGithubEnabled: true, + expectGithubDefaultProviderConfigured: false, + }, + { + name: "ConfiguredClientSecret", + githubClientSecret: "456", + expectGithubEnabled: true, + expectGithubDefaultProviderConfigured: false, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + runGitHubProviderTest(t, tc) + }) + } + }) + // Validate that a warning is printed that it may not be externally // reachable. t.Run("LocalAccessURL", func(t *testing.T) { diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 73ada6a92445d..df1f982bc52fe 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -498,6 +498,9 @@ OAUTH2 / GITHUB OPTIONS: --oauth2-github-client-secret string, $CODER_OAUTH2_GITHUB_CLIENT_SECRET Client secret for Login with GitHub. + --oauth2-github-default-provider-enable bool, $CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE (default: true) + Enable the default GitHub OAuth2 provider managed by Coder. + --oauth2-github-device-flow bool, $CODER_OAUTH2_GITHUB_DEVICE_FLOW (default: false) Enable device flow for Login with GitHub. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 1a45d664db1f8..cffaf65cd3cef 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -265,6 +265,9 @@ oauth2: # Enable device flow for Login with GitHub. # (default: false, type: bool) deviceFlow: false + # Enable the default GitHub OAuth2 provider managed by Coder. + # (default: true, type: bool) + defaultProviderEnable: true # Organizations the user must be a member of to Login with GitHub. # (default: , type: string-array) allowedOrgs: [] diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 69d421b2998e8..d7e9408eb677f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10331,7 +10331,7 @@ const docTemplate = `{ "type": "object", "properties": { "github": { - "$ref": "#/definitions/codersdk.AuthMethod" + "$ref": "#/definitions/codersdk.GithubAuthMethod" }, "oidc": { "$ref": "#/definitions/codersdk.OIDCAuthMethod" @@ -11857,6 +11857,17 @@ const docTemplate = `{ } } }, + "codersdk.GithubAuthMethod": { + "type": "object", + "properties": { + "default_provider_configured": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + } + } + }, "codersdk.Group": { "type": "object", "properties": { @@ -12519,6 +12530,9 @@ const docTemplate = `{ "client_secret": { "type": "string" }, + "default_provider_enable": { + "type": "boolean" + }, "device_flow": { "type": "boolean" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 2a407061512f8..ff714e416c5ce 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9189,7 +9189,7 @@ "type": "object", "properties": { "github": { - "$ref": "#/definitions/codersdk.AuthMethod" + "$ref": "#/definitions/codersdk.GithubAuthMethod" }, "oidc": { "$ref": "#/definitions/codersdk.OIDCAuthMethod" @@ -10642,6 +10642,17 @@ } } }, + "codersdk.GithubAuthMethod": { + "type": "object", + "properties": { + "default_provider_configured": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + } + } + }, "codersdk.Group": { "type": "object", "properties": { @@ -11255,6 +11266,9 @@ "client_secret": { "type": "string" }, + "default_provider_enable": { + "type": "boolean" + }, "device_flow": { "type": "boolean" }, diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 689a6c9322420..fdc9f6504d95d 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1845,6 +1845,13 @@ func (q *querier) GetNotificationsSettings(ctx context.Context) (string, error) return q.db.GetNotificationsSettings(ctx) } +func (q *querier) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { + return false, err + } + return q.db.GetOAuth2GithubDefaultEligible(ctx) +} + func (q *querier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2App); err != nil { return database.OAuth2ProviderApp{}, err @@ -4435,6 +4442,13 @@ func (q *querier) UpsertNotificationsSettings(ctx context.Context, value string) return q.db.UpsertNotificationsSettings(ctx, value) } +func (q *querier) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { + return err + } + return q.db.UpsertOAuth2GithubDefaultEligible(ctx, eligible) +} + func (q *querier) UpsertOAuthSigningKey(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index db4e68721538d..108a8166d19fb 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4405,6 +4405,12 @@ func (s *MethodTestSuite) TestSystemFunctions() { Value: "value", }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) + s.Run("GetOAuth2GithubDefaultEligible", s.Subtest(func(db database.Store, check *expects) { + check.Args().Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead).Errors(sql.ErrNoRows) + })) + s.Run("UpsertOAuth2GithubDefaultEligible", s.Subtest(func(db database.Store, check *expects) { + check.Args(true).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) + })) } func (s *MethodTestSuite) TestNotifications() { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 9488577edca17..058aed631887e 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -254,6 +254,7 @@ type data struct { announcementBanners []byte healthSettings []byte notificationsSettings []byte + oauth2GithubDefaultEligible *bool applicationName string logoURL string appSecurityKey string @@ -3515,6 +3516,16 @@ func (q *FakeQuerier) GetNotificationsSettings(_ context.Context) (string, error return string(q.notificationsSettings), nil } +func (q *FakeQuerier) GetOAuth2GithubDefaultEligible(_ context.Context) (bool, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + if q.oauth2GithubDefaultEligible == nil { + return false, sql.ErrNoRows + } + return *q.oauth2GithubDefaultEligible, nil +} + func (q *FakeQuerier) GetOAuth2ProviderAppByID(_ context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -11154,6 +11165,14 @@ func (q *FakeQuerier) UpsertNotificationsSettings(_ context.Context, data string return nil } +func (q *FakeQuerier) UpsertOAuth2GithubDefaultEligible(_ context.Context, eligible bool) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + q.oauth2GithubDefaultEligible = &eligible + return nil +} + func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 90ea140d0505c..31fbcced1b7f2 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -871,6 +871,13 @@ func (m queryMetricsStore) GetNotificationsSettings(ctx context.Context) (string return r0, r1 } +func (m queryMetricsStore) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) { + start := time.Now() + r0, r1 := m.s.GetOAuth2GithubDefaultEligible(ctx) + m.queryLatencies.WithLabelValues("GetOAuth2GithubDefaultEligible").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { start := time.Now() r0, r1 := m.s.GetOAuth2ProviderAppByID(ctx, id) @@ -2817,6 +2824,13 @@ func (m queryMetricsStore) UpsertNotificationsSettings(ctx context.Context, valu return r0 } +func (m queryMetricsStore) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error { + start := time.Now() + r0 := m.s.UpsertOAuth2GithubDefaultEligible(ctx, eligible) + m.queryLatencies.WithLabelValues("UpsertOAuth2GithubDefaultEligible").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpsertOAuthSigningKey(ctx context.Context, value string) error { start := time.Now() r0 := m.s.UpsertOAuthSigningKey(ctx, value) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 38ee52aa76bbd..f92bbf13246d7 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1762,6 +1762,21 @@ func (mr *MockStoreMockRecorder) GetNotificationsSettings(ctx any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationsSettings", reflect.TypeOf((*MockStore)(nil).GetNotificationsSettings), ctx) } +// GetOAuth2GithubDefaultEligible mocks base method. +func (m *MockStore) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOAuth2GithubDefaultEligible", ctx) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOAuth2GithubDefaultEligible indicates an expected call of GetOAuth2GithubDefaultEligible. +func (mr *MockStoreMockRecorder) GetOAuth2GithubDefaultEligible(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuth2GithubDefaultEligible", reflect.TypeOf((*MockStore)(nil).GetOAuth2GithubDefaultEligible), ctx) +} + // GetOAuth2ProviderAppByID mocks base method. func (m *MockStore) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { m.ctrl.T.Helper() @@ -5936,6 +5951,20 @@ func (mr *MockStoreMockRecorder) UpsertNotificationsSettings(ctx, value any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertNotificationsSettings", reflect.TypeOf((*MockStore)(nil).UpsertNotificationsSettings), ctx, value) } +// UpsertOAuth2GithubDefaultEligible mocks base method. +func (m *MockStore) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertOAuth2GithubDefaultEligible", ctx, eligible) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertOAuth2GithubDefaultEligible indicates an expected call of UpsertOAuth2GithubDefaultEligible. +func (mr *MockStoreMockRecorder) UpsertOAuth2GithubDefaultEligible(ctx, eligible any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertOAuth2GithubDefaultEligible", reflect.TypeOf((*MockStore)(nil).UpsertOAuth2GithubDefaultEligible), ctx, eligible) +} + // UpsertOAuthSigningKey mocks base method. func (m *MockStore) UpsertOAuthSigningKey(ctx context.Context, value string) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index a5cedde6c4a73..527ee955819d8 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -185,6 +185,7 @@ type sqlcQuerier interface { GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) GetNotificationsSettings(ctx context.Context) (string, error) + GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error) GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error) @@ -553,6 +554,7 @@ type sqlcQuerier interface { // Insert or update notification report generator logs with recent activity. UpsertNotificationReportGeneratorLog(ctx context.Context, arg UpsertNotificationReportGeneratorLogParams) error UpsertNotificationsSettings(ctx context.Context, value string) error + UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error UpsertOAuthSigningKey(ctx context.Context, value string) error UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) UpsertRuntimeConfig(ctx context.Context, arg UpsertRuntimeConfigParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ea4124d8fca94..0e2bc0e37f375 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8100,6 +8100,23 @@ func (q *sqlQuerier) GetNotificationsSettings(ctx context.Context) (string, erro return notifications_settings, err } +const getOAuth2GithubDefaultEligible = `-- name: GetOAuth2GithubDefaultEligible :one +SELECT + CASE + WHEN value = 'true' THEN TRUE + ELSE FALSE + END +FROM site_configs +WHERE key = 'oauth2_github_default_eligible' +` + +func (q *sqlQuerier) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) { + row := q.db.QueryRowContext(ctx, getOAuth2GithubDefaultEligible) + var column_1 bool + err := row.Scan(&column_1) + return column_1, err +} + const getOAuthSigningKey = `-- name: GetOAuthSigningKey :one SELECT value FROM site_configs WHERE key = 'oauth_signing_key' ` @@ -8243,6 +8260,28 @@ func (q *sqlQuerier) UpsertNotificationsSettings(ctx context.Context, value stri return err } +const upsertOAuth2GithubDefaultEligible = `-- name: UpsertOAuth2GithubDefaultEligible :exec +INSERT INTO site_configs (key, value) +VALUES ( + 'oauth2_github_default_eligible', + CASE + WHEN $1::bool THEN 'true' + ELSE 'false' + END +) +ON CONFLICT (key) DO UPDATE +SET value = CASE + WHEN $1::bool THEN 'true' + ELSE 'false' +END +WHERE site_configs.key = 'oauth2_github_default_eligible' +` + +func (q *sqlQuerier) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error { + _, err := q.db.ExecContext(ctx, upsertOAuth2GithubDefaultEligible, eligible) + return err +} + const upsertOAuthSigningKey = `-- name: UpsertOAuthSigningKey :exec INSERT INTO site_configs (key, value) VALUES ('oauth_signing_key', $1) ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'oauth_signing_key' diff --git a/coderd/database/queries/siteconfig.sql b/coderd/database/queries/siteconfig.sql index e8d02372e5a4f..ab9fda7969cea 100644 --- a/coderd/database/queries/siteconfig.sql +++ b/coderd/database/queries/siteconfig.sql @@ -107,3 +107,27 @@ ON CONFLICT (key) DO UPDATE SET value = $2 WHERE site_configs.key = $1; DELETE FROM site_configs WHERE site_configs.key = $1; +-- name: GetOAuth2GithubDefaultEligible :one +SELECT + CASE + WHEN value = 'true' THEN TRUE + ELSE FALSE + END +FROM site_configs +WHERE key = 'oauth2_github_default_eligible'; + +-- name: UpsertOAuth2GithubDefaultEligible :exec +INSERT INTO site_configs (key, value) +VALUES ( + 'oauth2_github_default_eligible', + CASE + WHEN sqlc.arg(eligible)::bool THEN 'true' + ELSE 'false' + END +) +ON CONFLICT (key) DO UPDATE +SET value = CASE + WHEN sqlc.arg(eligible)::bool THEN 'true' + ELSE 'false' +END +WHERE site_configs.key = 'oauth2_github_default_eligible'; diff --git a/coderd/userauth.go b/coderd/userauth.go index 709d22389fba3..d8f52f79d2b60 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -765,6 +765,8 @@ type GithubOAuth2Config struct { AllowEveryone bool AllowOrganizations []string AllowTeams []GithubOAuth2Team + + DefaultProviderConfigured bool } func (c *GithubOAuth2Config) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { @@ -806,7 +808,10 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) { Password: codersdk.AuthMethod{ Enabled: !api.DeploymentValues.DisablePasswordAuth.Value(), }, - Github: codersdk.AuthMethod{Enabled: api.GithubOAuth2Config != nil}, + Github: codersdk.GithubAuthMethod{ + Enabled: api.GithubOAuth2Config != nil, + DefaultProviderConfigured: api.GithubOAuth2Config != nil && api.GithubOAuth2Config.DefaultProviderConfigured, + }, OIDC: codersdk.OIDCAuthMethod{ AuthMethod: codersdk.AuthMethod{Enabled: api.OIDCConfig != nil}, SignInText: signInText, diff --git a/codersdk/deployment.go b/codersdk/deployment.go index b15dc94274d84..428ebac4944f5 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -503,14 +503,15 @@ type OAuth2Config struct { } type OAuth2GithubConfig struct { - ClientID serpent.String `json:"client_id" typescript:",notnull"` - ClientSecret serpent.String `json:"client_secret" typescript:",notnull"` - DeviceFlow serpent.Bool `json:"device_flow" typescript:",notnull"` - AllowedOrgs serpent.StringArray `json:"allowed_orgs" typescript:",notnull"` - AllowedTeams serpent.StringArray `json:"allowed_teams" typescript:",notnull"` - AllowSignups serpent.Bool `json:"allow_signups" typescript:",notnull"` - AllowEveryone serpent.Bool `json:"allow_everyone" typescript:",notnull"` - EnterpriseBaseURL serpent.String `json:"enterprise_base_url" typescript:",notnull"` + ClientID serpent.String `json:"client_id" typescript:",notnull"` + ClientSecret serpent.String `json:"client_secret" typescript:",notnull"` + DeviceFlow serpent.Bool `json:"device_flow" typescript:",notnull"` + DefaultProviderEnable serpent.Bool `json:"default_provider_enable" typescript:",notnull"` + AllowedOrgs serpent.StringArray `json:"allowed_orgs" typescript:",notnull"` + AllowedTeams serpent.StringArray `json:"allowed_teams" typescript:",notnull"` + AllowSignups serpent.Bool `json:"allow_signups" typescript:",notnull"` + AllowEveryone serpent.Bool `json:"allow_everyone" typescript:",notnull"` + EnterpriseBaseURL serpent.String `json:"enterprise_base_url" typescript:",notnull"` } type OIDCConfig struct { @@ -1593,6 +1594,16 @@ func (c *DeploymentValues) Options() serpent.OptionSet { YAML: "deviceFlow", Default: "false", }, + { + Name: "OAuth2 GitHub Default Provider Enable", + Description: "Enable the default GitHub OAuth2 provider managed by Coder.", + Flag: "oauth2-github-default-provider-enable", + Env: "CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE", + Value: &c.OAuth2.Github.DefaultProviderEnable, + Group: &deploymentGroupOAuth2GitHub, + YAML: "defaultProviderEnable", + Default: "true", + }, { Name: "OAuth2 GitHub Allowed Orgs", Description: "Organizations the user must be a member of to Login with GitHub.", diff --git a/codersdk/users.go b/codersdk/users.go index 4dbdc0d4e4f91..7177a1bc3e76d 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -275,10 +275,10 @@ type OAuthConversionResponse struct { // AuthMethods contains authentication method information like whether they are enabled or not or custom text, etc. type AuthMethods struct { - TermsOfServiceURL string `json:"terms_of_service_url,omitempty"` - Password AuthMethod `json:"password"` - Github AuthMethod `json:"github"` - OIDC OIDCAuthMethod `json:"oidc"` + TermsOfServiceURL string `json:"terms_of_service_url,omitempty"` + Password AuthMethod `json:"password"` + Github GithubAuthMethod `json:"github"` + OIDC OIDCAuthMethod `json:"oidc"` } type AuthMethod struct { @@ -289,6 +289,11 @@ type UserLoginType struct { LoginType LoginType `json:"login_type"` } +type GithubAuthMethod struct { + Enabled bool `json:"enabled"` + DefaultProviderConfigured bool `json:"default_provider_configured"` +} + type OIDCAuthMethod struct { AuthMethod SignInText string `json:"signInText"` diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 7d85388e73e96..2b4a1e36c22cc 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -328,6 +328,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ ], "client_id": "string", "client_secret": "string", + "default_provider_enable": true, "device_flow": true, "enterprise_base_url": "string" } diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 753ee857c027c..99f94e53992e8 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -787,6 +787,7 @@ ```json { "github": { + "default_provider_configured": true, "enabled": true }, "oidc": { @@ -803,12 +804,12 @@ ### Properties -| Name | Type | Required | Restrictions | Description | -|------------------------|----------------------------------------------------|----------|--------------|-------------| -| `github` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | | -| `oidc` | [codersdk.OIDCAuthMethod](#codersdkoidcauthmethod) | false | | | -| `password` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | | -| `terms_of_service_url` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|------------------------|--------------------------------------------------------|----------|--------------|-------------| +| `github` | [codersdk.GithubAuthMethod](#codersdkgithubauthmethod) | false | | | +| `oidc` | [codersdk.OIDCAuthMethod](#codersdkoidcauthmethod) | false | | | +| `password` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | | +| `terms_of_service_url` | string | false | | | ## codersdk.AuthorizationCheck @@ -1977,6 +1978,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o ], "client_id": "string", "client_secret": "string", + "default_provider_enable": true, "device_flow": true, "enterprise_base_url": "string" } @@ -2449,6 +2451,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o ], "client_id": "string", "client_secret": "string", + "default_provider_enable": true, "device_flow": true, "enterprise_base_url": "string" } @@ -3101,6 +3104,22 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `updated_at` | string | false | | | | `user_id` | string | false | | | +## codersdk.GithubAuthMethod + +```json +{ + "default_provider_configured": true, + "enabled": true +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-------------------------------|---------|----------|--------------|-------------| +| `default_provider_configured` | boolean | false | | | +| `enabled` | boolean | false | | | + ## codersdk.Group ```json @@ -3807,6 +3826,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ], "client_id": "string", "client_secret": "string", + "default_provider_enable": true, "device_flow": true, "enterprise_base_url": "string" } @@ -3833,6 +3853,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ], "client_id": "string", "client_secret": "string", + "default_provider_enable": true, "device_flow": true, "enterprise_base_url": "string" } @@ -3840,16 +3861,17 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ### Properties -| Name | Type | Required | Restrictions | Description | -|-----------------------|-----------------|----------|--------------|-------------| -| `allow_everyone` | boolean | false | | | -| `allow_signups` | boolean | false | | | -| `allowed_orgs` | array of string | false | | | -| `allowed_teams` | array of string | false | | | -| `client_id` | string | false | | | -| `client_secret` | string | false | | | -| `device_flow` | boolean | false | | | -| `enterprise_base_url` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------------|-----------------|----------|--------------|-------------| +| `allow_everyone` | boolean | false | | | +| `allow_signups` | boolean | false | | | +| `allowed_orgs` | array of string | false | | | +| `allowed_teams` | array of string | false | | | +| `client_id` | string | false | | | +| `client_secret` | string | false | | | +| `default_provider_enable` | boolean | false | | | +| `device_flow` | boolean | false | | | +| `enterprise_base_url` | string | false | | | ## codersdk.OAuth2ProviderApp diff --git a/docs/reference/api/users.md b/docs/reference/api/users.md index 4055a4170baa5..df0a8ca094df2 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -159,6 +159,7 @@ curl -X GET http://coder-server:8080/api/v2/users/authmethods \ ```json { "github": { + "default_provider_configured": true, "enabled": true }, "oidc": { diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 62af563f17ad1..91d565952d943 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -373,6 +373,17 @@ Client secret for Login with GitHub. Enable device flow for Login with GitHub. +### --oauth2-github-default-provider-enable + +| | | +|-------------|-----------------------------------------------------------| +| Type | bool | +| Environment | $CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE | +| YAML | oauth2.github.defaultProviderEnable | +| Default | true | + +Enable the default GitHub OAuth2 provider managed by Coder. + ### --oauth2-github-allowed-orgs | | | diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index d0437fdff6ad3..f0b3e4b0aaac7 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -499,6 +499,9 @@ OAUTH2 / GITHUB OPTIONS: --oauth2-github-client-secret string, $CODER_OAUTH2_GITHUB_CLIENT_SECRET Client secret for Login with GitHub. + --oauth2-github-default-provider-enable bool, $CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE (default: true) + Enable the default GitHub OAuth2 provider managed by Coder. + --oauth2-github-device-flow bool, $CODER_OAUTH2_GITHUB_DEVICE_FLOW (default: false) Enable device flow for Login with GitHub. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index a00d3a20cf16f..fdda12254052c 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -200,7 +200,7 @@ export interface AuthMethod { export interface AuthMethods { readonly terms_of_service_url?: string; readonly password: AuthMethod; - readonly github: AuthMethod; + readonly github: GithubAuthMethod; readonly oidc: OIDCAuthMethod; } @@ -916,6 +916,12 @@ export interface GitSSHKey { readonly public_key: string; } +// From codersdk/users.go +export interface GithubAuthMethod { + readonly enabled: boolean; + readonly default_provider_configured: boolean; +} + // From codersdk/groups.go export interface Group { readonly id: string; @@ -1326,6 +1332,7 @@ export interface OAuth2GithubConfig { readonly client_id: string; readonly client_secret: string; readonly device_flow: boolean; + readonly default_provider_enable: boolean; readonly allowed_orgs: string; readonly allowed_teams: string; readonly allow_signups: boolean; diff --git a/site/src/pages/LoginPage/SignInForm.stories.tsx b/site/src/pages/LoginPage/SignInForm.stories.tsx index 8e02ccfb3cfdc..125e912e08e70 100644 --- a/site/src/pages/LoginPage/SignInForm.stories.tsx +++ b/site/src/pages/LoginPage/SignInForm.stories.tsx @@ -20,7 +20,7 @@ export const SigningIn: Story = { isSigningIn: true, authMethods: { password: { enabled: true }, - github: { enabled: true }, + github: { enabled: true, default_provider_configured: false }, oidc: { enabled: false, signInText: "", iconUrl: "" }, }, }, @@ -44,7 +44,7 @@ export const WithGithub: Story = { args: { authMethods: { password: { enabled: true }, - github: { enabled: true }, + github: { enabled: true, default_provider_configured: false }, oidc: { enabled: false, signInText: "", iconUrl: "" }, }, }, @@ -54,7 +54,7 @@ export const WithOIDC: Story = { args: { authMethods: { password: { enabled: true }, - github: { enabled: false }, + github: { enabled: false, default_provider_configured: false }, oidc: { enabled: true, signInText: "", iconUrl: "" }, }, }, @@ -64,7 +64,7 @@ export const WithOIDCWithoutPassword: Story = { args: { authMethods: { password: { enabled: false }, - github: { enabled: false }, + github: { enabled: false, default_provider_configured: false }, oidc: { enabled: true, signInText: "", iconUrl: "" }, }, }, @@ -74,7 +74,7 @@ export const WithoutAny: Story = { args: { authMethods: { password: { enabled: false }, - github: { enabled: false }, + github: { enabled: false, default_provider_configured: false }, oidc: { enabled: false, signInText: "", iconUrl: "" }, }, }, @@ -84,7 +84,7 @@ export const WithGithubAndOIDC: Story = { args: { authMethods: { password: { enabled: true }, - github: { enabled: true }, + github: { enabled: true, default_provider_configured: false }, oidc: { enabled: true, signInText: "", iconUrl: "" }, }, }, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 74d4de9121e2e..938537c08d70c 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1684,20 +1684,20 @@ export const MockUserAgent = { export const MockAuthMethodsPasswordOnly: TypesGen.AuthMethods = { password: { enabled: true }, - github: { enabled: false }, + github: { enabled: false, default_provider_configured: true }, oidc: { enabled: false, signInText: "", iconUrl: "" }, }; export const MockAuthMethodsPasswordTermsOfService: TypesGen.AuthMethods = { terms_of_service_url: "https://www.youtube.com/watch?v=C2f37Vb2NAE", password: { enabled: true }, - github: { enabled: false }, + github: { enabled: false, default_provider_configured: true }, oidc: { enabled: false, signInText: "", iconUrl: "" }, }; export const MockAuthMethodsExternal: TypesGen.AuthMethods = { password: { enabled: false }, - github: { enabled: true }, + github: { enabled: true, default_provider_configured: true }, oidc: { enabled: true, signInText: "Google", @@ -1707,7 +1707,7 @@ export const MockAuthMethodsExternal: TypesGen.AuthMethods = { export const MockAuthMethodsAll: TypesGen.AuthMethods = { password: { enabled: true }, - github: { enabled: true }, + github: { enabled: true, default_provider_configured: true }, oidc: { enabled: true, signInText: "Google",