Skip to content
Prev Previous commit
Next Next commit
Add AuthMethods endpoint
  • Loading branch information
kylecarbs committed Apr 17, 2022
commit 53c2bf444a28366e77b081bba3a2835c5fc8db5f
9 changes: 6 additions & 3 deletions cli/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string) (*
RedirectURL: redirectURL.String(),
Scopes: []string{
"read:user",
"read:org",
"user:email",
},
},
Expand All @@ -570,9 +571,11 @@ func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string) (*
emails, _, err := github.NewClient(client).Users.ListEmails(ctx, &github.ListOptions{})
return emails, err
},
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
orgs, _, err := github.NewClient(client).Organizations.List(ctx, "", &github.ListOptions{})
return orgs, err
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
memberships, _, err := github.NewClient(client).Organizations.ListOrgMemberships(ctx, &github.ListOrgMembershipsOptions{
State: "active",
})
return memberships, err
},
}, nil
}
1 change: 1 addition & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ func New(options *Options) (http.Handler, func()) {
r.Post("/first", api.postFirstUser)
r.Post("/login", api.postLogin)
r.Post("/logout", api.postLogout)
r.Get("/authmethods", api.userAuthMethods)
r.Route("/oauth2", func(r chi.Router) {
r.Route("/github", func(r chi.Router) {
r.Use(httpmw.ExtractOAuth2(options.GithubOAuth2Config))
Expand Down
36 changes: 23 additions & 13 deletions coderd/useroauth2.go → coderd/userauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,39 +20,43 @@ import (
// GithubOAuth2Provider exposes required functions for the Github authentication flow.
type GithubOAuth2Config struct {
httpmw.OAuth2Config
AuthenticatedUser func(ctx context.Context, client *http.Client) (*github.User, error)
ListEmails func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error)
ListOrganizations func(ctx context.Context, client *http.Client) ([]*github.Organization, error)
AuthenticatedUser func(ctx context.Context, client *http.Client) (*github.User, error)
ListEmails func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error)
ListOrganizationMemberships func(ctx context.Context, client *http.Client) ([]*github.Membership, error)

AllowSignups bool
AllowOrganizations []string
}

func (api *api) userAuthMethods(rw http.ResponseWriter, _ *http.Request) {
httpapi.Write(rw, http.StatusOK, codersdk.AuthMethods{
Password: true,
Github: api.GithubOAuth2Config != nil,
})
}

func (api *api) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
state := httpmw.OAuth2(r)

oauthClient := oauth2.NewClient(r.Context(), oauth2.StaticTokenSource(state.Token))
organizations, err := api.GithubOAuth2Config.ListOrganizations(r.Context(), oauthClient)
memberships, err := api.GithubOAuth2Config.ListOrganizationMemberships(r.Context(), oauthClient)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get authenticated github user organizations: %s", err),
})
return
}
var selectedOrganization *github.Organization
for _, organization := range organizations {
if organization.Login == nil {
continue
}
var selectedMembership *github.Membership
for _, membership := range memberships {
for _, allowed := range api.GithubOAuth2Config.AllowOrganizations {
if *organization.Login != allowed {
if *membership.Organization.Login != allowed {
continue
}
selectedOrganization = organization
selectedMembership = membership
break
}
}
if selectedOrganization == nil {
if selectedMembership == nil {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: fmt.Sprintf("You aren't a member of the authorized Github organizations!"),
})
Expand Down Expand Up @@ -132,7 +136,13 @@ func (api *api) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
}
}

_, created := api.createAPIKey(rw, r, user.ID)
_, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypeGithub,
OAuthAccessToken: state.Token.AccessToken,
OAuthRefreshToken: state.Token.RefreshToken,
OAuthExpiry: state.Token.Expiry,
})
if !created {
return
}
Expand Down
62 changes: 47 additions & 15 deletions coderd/useroauth2_test.go → coderd/userauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,40 @@ func (*oauth2Config) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSou
return nil
}

func TestUserAuthMethods(t *testing.T) {
t.Parallel()
t.Run("Basic", 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{},
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
return []*github.Organization{{
Login: github.String("kyle"),
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
Organization: &github.Organization{
Login: github.String("kyle"),
},
}}, nil
},
},
Expand All @@ -55,9 +79,11 @@ func TestUserOAuth2Github(t *testing.T) {
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &oauth2Config{},
AllowOrganizations: []string{"coder"},
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
return []*github.Organization{{
Login: github.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) {
Expand All @@ -81,9 +107,11 @@ func TestUserOAuth2Github(t *testing.T) {
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &oauth2Config{},
AllowOrganizations: []string{"coder"},
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
return []*github.Organization{{
Login: github.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) {
Expand All @@ -104,9 +132,11 @@ func TestUserOAuth2Github(t *testing.T) {
OAuth2Config: &oauth2Config{},
AllowOrganizations: []string{"coder"},
AllowSignups: true,
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
return []*github.Organization{{
Login: github.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) {
Expand All @@ -129,9 +159,11 @@ func TestUserOAuth2Github(t *testing.T) {
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &oauth2Config{},
AllowOrganizations: []string{"coder"},
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
return []*github.Organization{{
Login: github.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) {
Expand Down
29 changes: 20 additions & 9 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,10 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
return
}

sessionToken, created := api.createAPIKey(rw, r, user.ID)
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypeBasic,
})
if !created {
return
}
Expand All @@ -397,7 +400,10 @@ func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) {
return
}

sessionToken, created := api.createAPIKey(rw, r, user.ID)
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypeBasic,
})
if !created {
return
}
Expand Down Expand Up @@ -773,7 +779,7 @@ func convertUser(user database.User) codersdk.User {
}
}

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

_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: keyID,
UserID: userID,
ExpiresAt: database.Now().Add(24 * time.Hour),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
ID: keyID,
UserID: params.UserID,
ExpiresAt: database.Now().Add(24 * time.Hour),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
LoginType: params.LoginType,
OAuthAccessToken: params.OAuthAccessToken,
OAuthRefreshToken: params.OAuthRefreshToken,
OAuthIDToken: params.OAuthIDToken,
OAuthExpiry: params.OAuthExpiry,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Expand Down
22 changes: 22 additions & 0 deletions codersdk/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ type CreateWorkspaceRequest struct {
ParameterValues []CreateParameterRequest `json:"parameter_values"`
}

// AuthMethods contains whether authentication types are enabled or not.
type AuthMethods struct {
Password bool `json:"password"`
Github bool `json:"github"`
}

// HasFirstUser returns whether the first user has been created.
func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
res, err := c.request(ctx, http.MethodGet, "/api/v2/users/first", nil)
Expand Down Expand Up @@ -287,6 +293,22 @@ func (c *Client) WorkspaceByName(ctx context.Context, userID uuid.UUID, name str
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
}

// AuthMethods returns types of authentication available to the user.
func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) {
res, err := c.request(ctx, http.MethodGet, "/api/v2/users/authmethods", nil)
if err != nil {
return AuthMethods{}, err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return AuthMethods{}, readBodyAsError(res)
}

var userAuth AuthMethods
return userAuth, json.NewDecoder(res.Body).Decode(&userAuth)
}

// uuidOrMe returns the provided uuid as a string if it's valid, ortherwise
// `me`.
func uuidOrMe(id uuid.UUID) string {
Expand Down