diff --git a/coderd/userauth.go b/coderd/userauth.go index b41f496814306..6079772945027 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -644,7 +644,15 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { if user.ID == uuid.Nil { aReq.Action = database.AuditActionRegister } - + // See: https://github.com/coder/coder/discussions/13340 + // In GitHub Enterprise, admins are permitted to have `_` + // in their usernames. This is janky, but much better + // than changing the username format globally. + username := ghUser.GetLogin() + if strings.Contains(username, "_") { + api.Logger.Warn(ctx, "login associates a github username that contains underscores. underscores are not permitted in usernames, replacing with `-`", slog.F("username", username)) + username = strings.ReplaceAll(username, "_", "-") + } params := (&oauthLoginParams{ User: user, Link: link, @@ -653,7 +661,7 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { LoginType: database.LoginTypeGithub, AllowSignups: api.GithubOAuth2Config.AllowSignups, Email: verifiedEmail.GetEmail(), - Username: ghUser.GetLogin(), + Username: username, AvatarURL: ghUser.GetAvatarURL(), Name: normName, DebugContext: OauthDebugContext{}, diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 1c647f3cca281..ef62005b9e1f4 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -665,6 +665,50 @@ func TestUserOAuth2Github(t *testing.T) { require.Len(t, auditor.AuditLogs(), numLogs) require.Equal(t, database.AuditActionRegister, auditor.AuditLogs()[numLogs-1].Action) }) + t.Run("SignupReplaceUnderscores", func(t *testing.T) { + t.Parallel() + auditor := audit.NewMock() + client := coderdtest.New(t, &coderdtest.Options{ + Auditor: auditor, + GithubOAuth2Config: &coderd.GithubOAuth2Config{ + AllowSignups: true, + AllowEveryone: true, + OAuth2Config: &testutil.OAuth2Config{}, + ListOrganizationMemberships: func(_ context.Context, _ *http.Client) ([]*github.Membership, error) { + return []*github.Membership{}, nil + }, + TeamMembership: func(_ context.Context, _ *http.Client, _, _, _ string) (*github.Membership, error) { + return nil, xerrors.New("no teams") + }, + AuthenticatedUser: func(_ context.Context, _ *http.Client) (*github.User, error) { + return &github.User{ + ID: github.Int64(100), + Login: github.String("mathias_coder"), + }, nil + }, + ListEmails: func(_ context.Context, _ *http.Client) ([]*github.UserEmail, error) { + return []*github.UserEmail{{ + Email: github.String("mathias@coder.com"), + Verified: github.Bool(true), + Primary: github.Bool(true), + }}, nil + }, + }, + }) + numLogs := len(auditor.AuditLogs()) + + resp := oauth2Callback(t, client) + numLogs++ // add an audit log for login + + require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + require.Len(t, auditor.AuditLogs(), numLogs) + require.Equal(t, database.AuditActionRegister, auditor.AuditLogs()[numLogs-1].Action) + + client.SetSessionToken(authCookieValue(resp.Cookies())) + user, err := client.User(context.Background(), "me") + require.NoError(t, err) + require.Equal(t, "mathias-coder", user.Username) + }) t.Run("SignupFailedInactiveInOrg", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{