feat: Add GitHub OAuth (#1050)

* Initial oauth

* Add Github authentication

* Add AuthMethods endpoint

* Add frontend

* Rename basic authentication to password

* Add flags for configuring GitHub auth

* Remove name from API keys

* Fix authmethods in test

* Add stories and display auth methods error
This commit is contained in:
Kyle Carberry
2022-04-23 22:58:57 +00:00
committed by GitHub
parent 3976994781
commit 7496c3da81
41 changed files with 1251 additions and 422 deletions
+128 -169
View File
@@ -1,6 +1,7 @@
package coderd
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/json"
@@ -71,66 +72,10 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {
return
}
hashedPassword, err := userpassword.Hash(createUser.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("hash password: %s", err.Error()),
})
return
}
// Create the user, organization, and membership to the user.
var user database.User
var organization database.Organization
err = api.Database.InTx(func(db database.Store) error {
user, err = api.Database.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.New(),
Email: createUser.Email,
HashedPassword: []byte(hashedPassword),
Username: createUser.Username,
LoginType: database.LoginTypeBuiltIn,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create user: %w", err)
}
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
if err != nil {
return xerrors.Errorf("generate user gitsshkey: %w", err)
}
_, err = db.InsertGitSSHKey(r.Context(), database.InsertGitSSHKeyParams{
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
PrivateKey: privateKey,
PublicKey: publicKey,
})
if err != nil {
return xerrors.Errorf("insert user gitsshkey: %w", err)
}
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
ID: uuid.New(),
Name: createUser.OrganizationName,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create organization: %w", err)
}
_, err = api.Database.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
OrganizationID: organization.ID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{"organization-admin"},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
user, organizationID, err := api.createUser(r.Context(), codersdk.CreateUserRequest{
Email: createUser.Email,
Username: createUser.Username,
Password: createUser.Password,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -141,7 +86,7 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(rw, http.StatusCreated, codersdk.CreateFirstUserResponse{
UserID: user.ID,
OrganizationID: organization.ID,
OrganizationID: organizationID,
})
}
@@ -262,56 +207,7 @@ func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) {
return
}
hashedPassword, err := userpassword.Hash(createUser.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("hash password: %s", err.Error()),
})
return
}
var user database.User
err = api.Database.InTx(func(db database.Store) error {
user, err = db.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.New(),
Email: createUser.Email,
HashedPassword: []byte(hashedPassword),
Username: createUser.Username,
LoginType: database.LoginTypeBuiltIn,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create user: %w", err)
}
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
if err != nil {
return xerrors.Errorf("generate user gitsshkey: %w", err)
}
_, err = db.InsertGitSSHKey(r.Context(), database.InsertGitSSHKeyParams{
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
PrivateKey: privateKey,
PublicKey: publicKey,
})
if err != nil {
return xerrors.Errorf("insert user gitsshkey: %w", err)
}
_, err = db.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
OrganizationID: organization.ID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
})
user, _, err := api.createUser(r.Context(), createUser)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
@@ -542,41 +438,13 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
return
}
keyID, keySecret, err := generateAPIKeyIDSecret()
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("generate api key parts: %s", err.Error()),
})
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypePassword,
})
if !created {
return
}
hashed := sha256.Sum256([]byte(keySecret))
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: keyID,
UserID: user.ID,
ExpiresAt: database.Now().Add(24 * time.Hour),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
LoginType: database.LoginTypeBuiltIn,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert api key: %s", err.Error()),
})
return
}
// This format is consumed by the APIKey middleware.
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
http.SetCookie(rw, &http.Cookie{
Name: httpmw.AuthCookie,
Value: sessionToken,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: api.SecureAuthCookie,
})
httpapi.Write(rw, http.StatusCreated, codersdk.LoginWithPasswordResponse{
SessionToken: sessionToken,
@@ -595,35 +463,15 @@ func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) {
return
}
keyID, keySecret, err := generateAPIKeyIDSecret()
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("generate api key parts: %s", err.Error()),
})
return
}
hashed := sha256.Sum256([]byte(keySecret))
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: keyID,
UserID: apiKey.UserID,
ExpiresAt: database.Now().AddDate(1, 0, 0), // Expire after 1 year (same as v1)
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
LoginType: database.LoginTypeBuiltIn,
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypePassword,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert api key: %s", err.Error()),
})
if !created {
return
}
// This format is consumed by the APIKey middleware.
generatedAPIKey := fmt.Sprintf("%s-%s", keyID, keySecret)
httpapi.Write(rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: generatedAPIKey})
httpapi.Write(rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: sessionToken})
}
// Clear the user's session cookie
@@ -984,6 +832,117 @@ func generateAPIKeyIDSecret() (id string, secret string, err error) {
return id, secret, nil
}
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{
Message: fmt.Sprintf("generate api key parts: %s", err.Error()),
})
return "", false
}
hashed := sha256.Sum256([]byte(keySecret))
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
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{
Message: fmt.Sprintf("insert api key: %s", err.Error()),
})
return "", false
}
// This format is consumed by the APIKey middleware.
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
http.SetCookie(rw, &http.Cookie{
Name: httpmw.AuthCookie,
Value: sessionToken,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: api.SecureAuthCookie,
})
return sessionToken, true
}
func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest) (database.User, uuid.UUID, error) {
var user database.User
return user, req.OrganizationID, api.Database.InTx(func(db database.Store) error {
// If no organization is provided, create a new one for the user.
if req.OrganizationID == uuid.Nil {
organization, err := db.InsertOrganization(ctx, database.InsertOrganizationParams{
ID: uuid.New(),
Name: req.Username,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create organization: %w", err)
}
req.OrganizationID = organization.ID
}
params := database.InsertUserParams{
ID: uuid.New(),
Email: req.Email,
Username: req.Username,
LoginType: database.LoginTypePassword,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
}
// If a user signs up with OAuth, they can have no password!
if req.Password != "" {
hashedPassword, err := userpassword.Hash(req.Password)
if err != nil {
return xerrors.Errorf("hash password: %w", err)
}
params.HashedPassword = []byte(hashedPassword)
}
var err error
user, err = db.InsertUser(ctx, params)
if err != nil {
return xerrors.Errorf("create user: %w", err)
}
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
if err != nil {
return xerrors.Errorf("generate user gitsshkey: %w", err)
}
_, err = db.InsertGitSSHKey(ctx, database.InsertGitSSHKeyParams{
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
PrivateKey: privateKey,
PublicKey: publicKey,
})
if err != nil {
return xerrors.Errorf("insert user gitsshkey: %w", err)
}
_, err = db.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
OrganizationID: req.OrganizationID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
})
}
func convertUser(user database.User) codersdk.User {
return codersdk.User{
ID: user.ID,