package coderd_test import ( "context" "net/http" "net/url" "testing" "github.com/google/go-github/v43/github" "github.com/stretchr/testify/require" "golang.org/x/oauth2" "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" ) type oauth2Config struct{} func (*oauth2Config) AuthCodeURL(state string, _ ...oauth2.AuthCodeOption) string { return "/?state=" + url.QueryEscape(state) } func (*oauth2Config) Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) { return &oauth2.Token{ AccessToken: "token", }, nil } func (*oauth2Config) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource { return nil } func TestUserAuthMethods(t *testing.T) { t.Parallel() t.Run("Password", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) methods, err := client.AuthMethods(context.Background()) require.NoError(t, err) require.True(t, methods.Password) require.False(t, methods.Github) }) t.Run("Github", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ GithubOAuth2Config: &coderd.GithubOAuth2Config{}, }) methods, err := client.AuthMethods(context.Background()) require.NoError(t, err) require.True(t, methods.Password) require.True(t, methods.Github) }) } func TestUserOAuth2Github(t *testing.T) { t.Parallel() t.Run("NotInAllowedOrganization", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ GithubOAuth2Config: &coderd.GithubOAuth2Config{ OAuth2Config: &oauth2Config{}, ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) { return []*github.Membership{{ Organization: &github.Organization{ Login: github.String("kyle"), }, }}, nil }, }, }) resp := oauth2Callback(t, client) require.Equal(t, http.StatusUnauthorized, resp.StatusCode) }) t.Run("UnverifiedEmail", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ GithubOAuth2Config: &coderd.GithubOAuth2Config{ OAuth2Config: &oauth2Config{}, AllowOrganizations: []string{"coder"}, ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) { return []*github.Membership{{ Organization: &github.Organization{ Login: github.String("coder"), }, }}, nil }, AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) { return &github.User{}, nil }, ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) { return []*github.UserEmail{{ Email: github.String("testuser@coder.com"), Verified: github.Bool(false), }}, nil }, }, }) _ = coderdtest.CreateFirstUser(t, client) resp := oauth2Callback(t, client) require.Equal(t, http.StatusForbidden, resp.StatusCode) }) t.Run("BlockSignups", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ GithubOAuth2Config: &coderd.GithubOAuth2Config{ OAuth2Config: &oauth2Config{}, AllowOrganizations: []string{"coder"}, ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) { return []*github.Membership{{ Organization: &github.Organization{ Login: github.String("coder"), }, }}, nil }, AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) { return &github.User{}, nil }, ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) { return []*github.UserEmail{}, nil }, }, }) resp := oauth2Callback(t, client) require.Equal(t, http.StatusForbidden, resp.StatusCode) }) t.Run("Signup", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ GithubOAuth2Config: &coderd.GithubOAuth2Config{ OAuth2Config: &oauth2Config{}, AllowOrganizations: []string{"coder"}, AllowSignups: true, ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) { return []*github.Membership{{ Organization: &github.Organization{ Login: github.String("coder"), }, }}, nil }, AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) { return &github.User{ Login: github.String("kyle"), }, nil }, ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) { return []*github.UserEmail{{ Email: github.String("kyle@coder.com"), Verified: github.Bool(true), Primary: github.Bool(true), }}, nil }, }, }) resp := oauth2Callback(t, client) require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) }) t.Run("Login", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ GithubOAuth2Config: &coderd.GithubOAuth2Config{ OAuth2Config: &oauth2Config{}, AllowOrganizations: []string{"coder"}, ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) { return []*github.Membership{{ Organization: &github.Organization{ Login: github.String("coder"), }, }}, nil }, AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) { return &github.User{}, nil }, ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) { return []*github.UserEmail{{ Email: github.String("testuser@coder.com"), Verified: github.Bool(true), }}, nil }, }, }) _ = coderdtest.CreateFirstUser(t, client) resp := oauth2Callback(t, client) require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) }) } func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response { client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } state := "somestate" oauthURL, err := client.URL.Parse("/api/v2/users/oauth2/github/callback?code=asd&state=" + state) require.NoError(t, err) req, err := http.NewRequest("GET", oauthURL.String(), nil) require.NoError(t, err) req.AddCookie(&http.Cookie{ Name: "oauth_state", Value: state, }) res, err := client.HTTPClient.Do(req) require.NoError(t, err) t.Cleanup(func() { _ = res.Body.Close() }) return res }