2022-04-23 22:58:57 +00:00
|
|
|
package coderd
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"database/sql"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
|
|
|
|
"github.com/google/go-github/v43/github"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"golang.org/x/oauth2"
|
|
|
|
|
|
|
|
"github.com/coder/coder/coderd/database"
|
|
|
|
"github.com/coder/coder/coderd/httpapi"
|
|
|
|
"github.com/coder/coder/coderd/httpmw"
|
|
|
|
"github.com/coder/coder/codersdk"
|
|
|
|
)
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
ListOrganizationMemberships func(ctx context.Context, client *http.Client) ([]*github.Membership, error)
|
|
|
|
|
|
|
|
AllowSignups bool
|
|
|
|
AllowOrganizations []string
|
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) userAuthMethods(rw http.ResponseWriter, _ *http.Request) {
|
2022-04-23 22:58:57 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, codersdk.AuthMethods{
|
|
|
|
Password: true,
|
|
|
|
Github: api.GithubOAuth2Config != nil,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
|
2022-04-23 22:58:57 +00:00
|
|
|
state := httpmw.OAuth2(r)
|
|
|
|
|
|
|
|
oauthClient := oauth2.NewClient(r.Context(), oauth2.StaticTokenSource(state.Token))
|
|
|
|
memberships, err := api.GithubOAuth2Config.ListOrganizationMemberships(r.Context(), oauthClient)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-06-03 21:48:09 +00:00
|
|
|
Message: "Internal error fetching authenticated Github user organizations",
|
|
|
|
Detail: err.Error(),
|
2022-04-23 22:58:57 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var selectedMembership *github.Membership
|
|
|
|
for _, membership := range memberships {
|
|
|
|
for _, allowed := range api.GithubOAuth2Config.AllowOrganizations {
|
|
|
|
if *membership.Organization.Login != allowed {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
selectedMembership = membership
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if selectedMembership == nil {
|
|
|
|
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
2022-05-10 21:04:23 +00:00
|
|
|
Message: "You aren't a member of the authorized Github organizations!",
|
2022-04-23 22:58:57 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
emails, err := api.GithubOAuth2Config.ListEmails(r.Context(), oauthClient)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-06-03 21:48:09 +00:00
|
|
|
Message: "Internal error fetching personal Github user",
|
|
|
|
Detail: err.Error(),
|
2022-04-23 22:58:57 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var user database.User
|
|
|
|
// Search for existing users with matching and verified emails.
|
|
|
|
// If a verified GitHub email matches a Coder user, we will return.
|
|
|
|
for _, email := range emails {
|
2022-04-29 20:13:35 +00:00
|
|
|
if !email.GetVerified() {
|
2022-04-23 22:58:57 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
user, err = api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
|
|
|
Email: *email.Email,
|
|
|
|
})
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-06-03 21:48:09 +00:00
|
|
|
Message: fmt.Sprintf("Internal error fetching user by email %q", *email.Email),
|
|
|
|
Detail: err.Error(),
|
2022-04-23 22:58:57 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !*email.Verified {
|
|
|
|
httpapi.Write(rw, http.StatusForbidden, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("Verify the %q email address on Github to authenticate!", *email.Email),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the user doesn't exist, create a new one!
|
|
|
|
if user.ID == uuid.Nil {
|
|
|
|
if !api.GithubOAuth2Config.AllowSignups {
|
|
|
|
httpapi.Write(rw, http.StatusForbidden, httpapi.Response{
|
|
|
|
Message: "Signups are disabled for Github authentication!",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var organizationID uuid.UUID
|
|
|
|
organizations, _ := api.Database.GetOrganizations(r.Context())
|
|
|
|
if len(organizations) > 0 {
|
|
|
|
// Add the user to the first organization. Once multi-organization
|
|
|
|
// support is added, we should enable a configuration map of user
|
|
|
|
// email to organization.
|
|
|
|
organizationID = organizations[0].ID
|
|
|
|
}
|
|
|
|
ghUser, err := api.GithubOAuth2Config.AuthenticatedUser(r.Context(), oauthClient)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-06-03 21:48:09 +00:00
|
|
|
Message: "Internal error fetching authenticated Github user",
|
|
|
|
Detail: err.Error(),
|
2022-04-23 22:58:57 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-29 20:13:35 +00:00
|
|
|
var verifiedEmail *github.UserEmail
|
|
|
|
for _, email := range emails {
|
|
|
|
if !email.GetPrimary() || !email.GetVerified() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
verifiedEmail = email
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if verifiedEmail == nil {
|
|
|
|
httpapi.Write(rw, http.StatusPreconditionRequired, httpapi.Response{
|
|
|
|
Message: "Your primary email must be verified on GitHub!",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-23 22:58:57 +00:00
|
|
|
user, _, err = api.createUser(r.Context(), codersdk.CreateUserRequest{
|
2022-04-29 20:13:35 +00:00
|
|
|
Email: *verifiedEmail.Email,
|
2022-04-23 22:58:57 +00:00
|
|
|
Username: *ghUser.Login,
|
|
|
|
OrganizationID: organizationID,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-06-03 21:48:09 +00:00
|
|
|
Message: "Internal error creating user",
|
|
|
|
Detail: err.Error(),
|
2022-04-23 22:58:57 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_, 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
|
|
|
|
}
|
|
|
|
|
|
|
|
redirect := state.Redirect
|
|
|
|
if redirect == "" {
|
|
|
|
redirect = "/"
|
|
|
|
}
|
|
|
|
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
|
|
|
|
}
|