feat: enable GitHub OAuth2 login by default on new deployments (#16662)

Third and final PR to address
https://github.com/coder/coder/issues/16230.

This PR enables GitHub OAuth2 login by default on new deployments.
Combined with https://github.com/coder/coder/pull/16629, this will allow
the first admin user to sign up with GitHub rather than email and
password.

We take care not to enable the default on deployments that would upgrade
to a Coder version with this change.

To disable the default provider an admin can set the
`CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER` env variable to false.
This commit is contained in:
Hugo Dutka
2025-02-25 16:31:33 +01:00
committed by GitHub
parent 67d89bb102
commit d3a56ae3ef
25 changed files with 544 additions and 83 deletions
+114 -41
View File
@@ -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 &params, 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 &params, 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 <organization>/<team>", 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
}
+141
View File
@@ -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) {
+3
View File
@@ -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.
+3
View File
@@ -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: <unset>, type: string-array)
allowedOrgs: []
+15 -1
View File
@@ -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"
},
+15 -1
View File
@@ -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"
},
+14
View File
@@ -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
+6
View File
@@ -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() {
+19
View File
@@ -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()
+14
View File
@@ -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)
+29
View File
@@ -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()
+2
View File
@@ -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
+39
View File
@@ -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'
+24
View File
@@ -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';
+6 -1
View File
@@ -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,
+19 -8
View File
@@ -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.",
+9 -4
View File
@@ -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"`
+1
View File
@@ -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"
}
+38 -16
View File
@@ -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
+1
View File
@@ -159,6 +159,7 @@ curl -X GET http://coder-server:8080/api/v2/users/authmethods \
```json
{
"github": {
"default_provider_configured": true,
"enabled": true
},
"oidc": {
+11
View File
@@ -373,6 +373,17 @@ Client secret for Login with GitHub.
Enable device flow for Login with GitHub.
### --oauth2-github-default-provider-enable
| | |
|-------------|-----------------------------------------------------------|
| Type | <code>bool</code> |
| Environment | <code>$CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE</code> |
| YAML | <code>oauth2.github.defaultProviderEnable</code> |
| Default | <code>true</code> |
Enable the default GitHub OAuth2 provider managed by Coder.
### --oauth2-github-allowed-orgs
| | |
+3
View File
@@ -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.
+8 -1
View File
@@ -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;
@@ -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: "" },
},
},
+4 -4
View File
@@ -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",