Skip to content

Commit 53c2bf4

Browse files
committed
Add AuthMethods endpoint
1 parent 05b6a37 commit 53c2bf4

File tree

6 files changed

+119
-40
lines changed

6 files changed

+119
-40
lines changed

cli/start.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,7 @@ func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string) (*
557557
RedirectURL: redirectURL.String(),
558558
Scopes: []string{
559559
"read:user",
560+
"read:org",
560561
"user:email",
561562
},
562563
},
@@ -570,9 +571,11 @@ func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string) (*
570571
emails, _, err := github.NewClient(client).Users.ListEmails(ctx, &github.ListOptions{})
571572
return emails, err
572573
},
573-
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
574-
orgs, _, err := github.NewClient(client).Organizations.List(ctx, "", &github.ListOptions{})
575-
return orgs, err
574+
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
575+
memberships, _, err := github.NewClient(client).Organizations.ListOrgMemberships(ctx, &github.ListOrgMembershipsOptions{
576+
State: "active",
577+
})
578+
return memberships, err
576579
},
577580
}, nil
578581
}

coderd/coderd.go

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ func New(options *Options) (http.Handler, func()) {
146146
r.Post("/first", api.postFirstUser)
147147
r.Post("/login", api.postLogin)
148148
r.Post("/logout", api.postLogout)
149+
r.Get("/authmethods", api.userAuthMethods)
149150
r.Route("/oauth2", func(r chi.Router) {
150151
r.Route("/github", func(r chi.Router) {
151152
r.Use(httpmw.ExtractOAuth2(options.GithubOAuth2Config))

coderd/useroauth2.go renamed to coderd/userauth.go

+23-13
Original file line numberDiff line numberDiff line change
@@ -20,39 +20,43 @@ import (
2020
// GithubOAuth2Provider exposes required functions for the Github authentication flow.
2121
type GithubOAuth2Config struct {
2222
httpmw.OAuth2Config
23-
AuthenticatedUser func(ctx context.Context, client *http.Client) (*github.User, error)
24-
ListEmails func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error)
25-
ListOrganizations func(ctx context.Context, client *http.Client) ([]*github.Organization, error)
23+
AuthenticatedUser func(ctx context.Context, client *http.Client) (*github.User, error)
24+
ListEmails func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error)
25+
ListOrganizationMemberships func(ctx context.Context, client *http.Client) ([]*github.Membership, error)
2626

2727
AllowSignups bool
2828
AllowOrganizations []string
2929
}
3030

31+
func (api *api) userAuthMethods(rw http.ResponseWriter, _ *http.Request) {
32+
httpapi.Write(rw, http.StatusOK, codersdk.AuthMethods{
33+
Password: true,
34+
Github: api.GithubOAuth2Config != nil,
35+
})
36+
}
37+
3138
func (api *api) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
3239
state := httpmw.OAuth2(r)
3340

3441
oauthClient := oauth2.NewClient(r.Context(), oauth2.StaticTokenSource(state.Token))
35-
organizations, err := api.GithubOAuth2Config.ListOrganizations(r.Context(), oauthClient)
42+
memberships, err := api.GithubOAuth2Config.ListOrganizationMemberships(r.Context(), oauthClient)
3643
if err != nil {
3744
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
3845
Message: fmt.Sprintf("get authenticated github user organizations: %s", err),
3946
})
4047
return
4148
}
42-
var selectedOrganization *github.Organization
43-
for _, organization := range organizations {
44-
if organization.Login == nil {
45-
continue
46-
}
49+
var selectedMembership *github.Membership
50+
for _, membership := range memberships {
4751
for _, allowed := range api.GithubOAuth2Config.AllowOrganizations {
48-
if *organization.Login != allowed {
52+
if *membership.Organization.Login != allowed {
4953
continue
5054
}
51-
selectedOrganization = organization
55+
selectedMembership = membership
5256
break
5357
}
5458
}
55-
if selectedOrganization == nil {
59+
if selectedMembership == nil {
5660
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
5761
Message: fmt.Sprintf("You aren't a member of the authorized Github organizations!"),
5862
})
@@ -132,7 +136,13 @@ func (api *api) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
132136
}
133137
}
134138

135-
_, created := api.createAPIKey(rw, r, user.ID)
139+
_, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
140+
UserID: user.ID,
141+
LoginType: database.LoginTypeGithub,
142+
OAuthAccessToken: state.Token.AccessToken,
143+
OAuthRefreshToken: state.Token.RefreshToken,
144+
OAuthExpiry: state.Token.Expiry,
145+
})
136146
if !created {
137147
return
138148
}

coderd/useroauth2_test.go renamed to coderd/userauth_test.go

+47-15
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,40 @@ func (*oauth2Config) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSou
3131
return nil
3232
}
3333

34+
func TestUserAuthMethods(t *testing.T) {
35+
t.Parallel()
36+
t.Run("Basic", func(t *testing.T) {
37+
t.Parallel()
38+
client := coderdtest.New(t, nil)
39+
methods, err := client.AuthMethods(context.Background())
40+
require.NoError(t, err)
41+
require.True(t, methods.Password)
42+
require.False(t, methods.Github)
43+
})
44+
t.Run("Github", func(t *testing.T) {
45+
t.Parallel()
46+
client := coderdtest.New(t, &coderdtest.Options{
47+
GithubOAuth2Config: &coderd.GithubOAuth2Config{},
48+
})
49+
methods, err := client.AuthMethods(context.Background())
50+
require.NoError(t, err)
51+
require.True(t, methods.Password)
52+
require.True(t, methods.Github)
53+
})
54+
}
55+
3456
func TestUserOAuth2Github(t *testing.T) {
3557
t.Parallel()
3658
t.Run("NotInAllowedOrganization", func(t *testing.T) {
3759
t.Parallel()
3860
client := coderdtest.New(t, &coderdtest.Options{
3961
GithubOAuth2Config: &coderd.GithubOAuth2Config{
4062
OAuth2Config: &oauth2Config{},
41-
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
42-
return []*github.Organization{{
43-
Login: github.String("kyle"),
63+
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
64+
return []*github.Membership{{
65+
Organization: &github.Organization{
66+
Login: github.String("kyle"),
67+
},
4468
}}, nil
4569
},
4670
},
@@ -55,9 +79,11 @@ func TestUserOAuth2Github(t *testing.T) {
5579
GithubOAuth2Config: &coderd.GithubOAuth2Config{
5680
OAuth2Config: &oauth2Config{},
5781
AllowOrganizations: []string{"coder"},
58-
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
59-
return []*github.Organization{{
60-
Login: github.String("coder"),
82+
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
83+
return []*github.Membership{{
84+
Organization: &github.Organization{
85+
Login: github.String("coder"),
86+
},
6187
}}, nil
6288
},
6389
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
@@ -81,9 +107,11 @@ func TestUserOAuth2Github(t *testing.T) {
81107
GithubOAuth2Config: &coderd.GithubOAuth2Config{
82108
OAuth2Config: &oauth2Config{},
83109
AllowOrganizations: []string{"coder"},
84-
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
85-
return []*github.Organization{{
86-
Login: github.String("coder"),
110+
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
111+
return []*github.Membership{{
112+
Organization: &github.Organization{
113+
Login: github.String("coder"),
114+
},
87115
}}, nil
88116
},
89117
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
@@ -104,9 +132,11 @@ func TestUserOAuth2Github(t *testing.T) {
104132
OAuth2Config: &oauth2Config{},
105133
AllowOrganizations: []string{"coder"},
106134
AllowSignups: true,
107-
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
108-
return []*github.Organization{{
109-
Login: github.String("coder"),
135+
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
136+
return []*github.Membership{{
137+
Organization: &github.Organization{
138+
Login: github.String("coder"),
139+
},
110140
}}, nil
111141
},
112142
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
@@ -129,9 +159,11 @@ func TestUserOAuth2Github(t *testing.T) {
129159
GithubOAuth2Config: &coderd.GithubOAuth2Config{
130160
OAuth2Config: &oauth2Config{},
131161
AllowOrganizations: []string{"coder"},
132-
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
133-
return []*github.Organization{{
134-
Login: github.String("coder"),
162+
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
163+
return []*github.Membership{{
164+
Organization: &github.Organization{
165+
Login: github.String("coder"),
166+
},
135167
}}, nil
136168
},
137169
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {

coderd/users.go

+20-9
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,10 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
375375
return
376376
}
377377

378-
sessionToken, created := api.createAPIKey(rw, r, user.ID)
378+
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
379+
UserID: user.ID,
380+
LoginType: database.LoginTypeBasic,
381+
})
379382
if !created {
380383
return
381384
}
@@ -397,7 +400,10 @@ func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) {
397400
return
398401
}
399402

400-
sessionToken, created := api.createAPIKey(rw, r, user.ID)
403+
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
404+
UserID: user.ID,
405+
LoginType: database.LoginTypeBasic,
406+
})
401407
if !created {
402408
return
403409
}
@@ -773,7 +779,7 @@ func convertUser(user database.User) codersdk.User {
773779
}
774780
}
775781

776-
func (api *api) createAPIKey(rw http.ResponseWriter, r *http.Request, userID uuid.UUID) (string, bool) {
782+
func (api *api) createAPIKey(rw http.ResponseWriter, r *http.Request, params database.InsertAPIKeyParams) (string, bool) {
777783
keyID, keySecret, err := generateAPIKeyIDSecret()
778784
if err != nil {
779785
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -784,12 +790,17 @@ func (api *api) createAPIKey(rw http.ResponseWriter, r *http.Request, userID uui
784790
hashed := sha256.Sum256([]byte(keySecret))
785791

786792
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
787-
ID: keyID,
788-
UserID: userID,
789-
ExpiresAt: database.Now().Add(24 * time.Hour),
790-
CreatedAt: database.Now(),
791-
UpdatedAt: database.Now(),
792-
HashedSecret: hashed[:],
793+
ID: keyID,
794+
UserID: params.UserID,
795+
ExpiresAt: database.Now().Add(24 * time.Hour),
796+
CreatedAt: database.Now(),
797+
UpdatedAt: database.Now(),
798+
HashedSecret: hashed[:],
799+
LoginType: params.LoginType,
800+
OAuthAccessToken: params.OAuthAccessToken,
801+
OAuthRefreshToken: params.OAuthRefreshToken,
802+
OAuthIDToken: params.OAuthIDToken,
803+
OAuthExpiry: params.OAuthExpiry,
793804
})
794805
if err != nil {
795806
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{

codersdk/users.go

+22
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ type CreateWorkspaceRequest struct {
7777
ParameterValues []CreateParameterRequest `json:"parameter_values"`
7878
}
7979

80+
// AuthMethods contains whether authentication types are enabled or not.
81+
type AuthMethods struct {
82+
Password bool `json:"password"`
83+
Github bool `json:"github"`
84+
}
85+
8086
// HasFirstUser returns whether the first user has been created.
8187
func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
8288
res, err := c.request(ctx, http.MethodGet, "/api/v2/users/first", nil)
@@ -287,6 +293,22 @@ func (c *Client) WorkspaceByName(ctx context.Context, userID uuid.UUID, name str
287293
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
288294
}
289295

296+
// AuthMethods returns types of authentication available to the user.
297+
func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) {
298+
res, err := c.request(ctx, http.MethodGet, "/api/v2/users/authmethods", nil)
299+
if err != nil {
300+
return AuthMethods{}, err
301+
}
302+
defer res.Body.Close()
303+
304+
if res.StatusCode != http.StatusOK {
305+
return AuthMethods{}, readBodyAsError(res)
306+
}
307+
308+
var userAuth AuthMethods
309+
return userAuth, json.NewDecoder(res.Body).Decode(&userAuth)
310+
}
311+
290312
// uuidOrMe returns the provided uuid as a string if it's valid, ortherwise
291313
// `me`.
292314
func uuidOrMe(id uuid.UUID) string {

0 commit comments

Comments
 (0)