Support Github and Gitea OAuth providers

This commit is contained in:
Thomas Miceli
2023-04-17 14:25:39 +02:00
parent 47cbf5e7ef
commit a6c5696ceb
10 changed files with 641 additions and 20 deletions

View File

@ -1,8 +1,6 @@
package models
import (
"errors"
"github.com/mattn/go-sqlite3"
"gorm.io/gorm/clause"
)
@ -38,7 +36,7 @@ func setSetting(key string, value string) error {
func initAdminSettings(settings map[string]string) error {
for key, value := range settings {
if err := setSetting(key, value); err != nil {
if !isUniqueConstraintViolation(err) {
if !IsUniqueConstraintViolation(err) {
return err
}
}
@ -46,11 +44,3 @@ func initAdminSettings(settings map[string]string) error {
return nil
}
func isUniqueConstraintViolation(err error) bool {
var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
return true
}
return false
}

View File

@ -1,6 +1,8 @@
package models
import (
"errors"
"github.com/mattn/go-sqlite3"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
@ -32,3 +34,11 @@ func CountAll(table interface{}) (int64, error) {
err := db.Model(table).Count(&count).Error
return count, err
}
func IsUniqueConstraintViolation(err error) bool {
var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
return true
}
return false
}

View File

@ -1,6 +1,12 @@
package models
import "time"
import (
"crypto/sha256"
"encoding/base64"
"golang.org/x/crypto/ssh"
"gorm.io/gorm"
"time"
)
type SSHKey struct {
ID uint `gorm:"primaryKey"`
@ -13,6 +19,16 @@ type SSHKey struct {
User User `validate:"-" `
}
func (sshKey *SSHKey) BeforeCreate(tx *gorm.DB) error {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content))
if err != nil {
return err
}
sha := sha256.Sum256(pubKey.Marshal())
sshKey.SHA = base64.StdEncoding.EncodeToString(sha[:])
return nil
}
func GetSSHKeysByUserID(userId uint) ([]*SSHKey, error) {
var sshKeys []*SSHKey
err := db.

View File

@ -12,6 +12,8 @@ type User struct {
CreatedAt int64
Email string
MD5Hash string // for gravatar, if no Email is specified, the value is random
GithubID string
GiteaID string
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
SSHKeys []SSHKey `gorm:"foreignKey:UserID"`
@ -90,6 +92,19 @@ func GetUserBySSHKeyID(sshKeyId uint) (*User, error) {
return user, err
}
func GetUserByProvider(id string, provider string) (*User, error) {
user := new(User)
var err error
switch provider {
case "github":
err = db.Where("github_id = ?", id).First(&user).Error
case "gitea":
err = db.Where("gitea_id = ?", id).First(&user).Error
}
return user, err
}
func (user *User) Create() error {
return db.Create(&user).Error
}
@ -118,6 +133,17 @@ func (user *User) HasLiked(gist *Gist) (bool, error) {
return true, nil
}
func (user *User) DeleteProvider(provider string) error {
switch provider {
case "github":
return db.Model(&user).Update("github_id", nil).Error
case "gitea":
return db.Model(&user).Update("gitea_id", nil).Error
}
return nil
}
// -- DTO -- //
type UserDTO struct {

View File

@ -1,11 +1,18 @@
package web
import (
"context"
"crypto/md5"
"errors"
"fmt"
"github.com/labstack/echo/v4"
"github.com/markbates/goth/gothic"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"io"
"net/http"
"opengist/internal/models"
"strings"
)
func register(ctx echo.Context) error {
@ -106,6 +113,143 @@ func processLogin(ctx echo.Context) error {
return redirect(ctx, "/")
}
func oauthCallback(ctx echo.Context) error {
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
if err != nil {
return errorRes(400, "Cannot complete user auth", err)
}
currUser := getUserLogged(ctx)
if currUser != nil {
// if user is logged in, link account to user
switch user.Provider {
case "github":
currUser.GithubID = user.UserID
case "gitea":
currUser.GiteaID = user.UserID
}
if err = currUser.Update(); err != nil {
return errorRes(500, "Cannot update user "+strings.Title(user.Provider)+" id", err)
}
addFlash(ctx, "Account linked to "+strings.Title(user.Provider), "success")
return redirect(ctx, "/settings")
}
// if user is not in database, create it
userDB, err := models.GetUserByProvider(user.UserID, user.Provider)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return errorRes(500, "Cannot get user", err)
}
userDB = &models.User{
Username: user.NickName,
Email: user.Email,
MD5Hash: fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(user.Email))))),
}
switch user.Provider {
case "github":
userDB.GithubID = user.UserID
case "gitea":
userDB.GiteaID = user.UserID
}
if err = userDB.Create(); err != nil {
if models.IsUniqueConstraintViolation(err) {
addFlash(ctx, "Username "+user.NickName+" already exists in opengist", "error")
return redirect(ctx, "/login")
}
return errorRes(500, "Cannot create user", err)
}
var resp *http.Response
switch user.Provider {
case "github":
resp, err = http.Get("https://github.com/" + user.NickName + ".keys")
case "gitea":
resp, err = http.Get("https://gitea.com/" + user.NickName + ".keys")
}
if err == nil {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
addFlash(ctx, "Could not get user keys", "error")
log.Error().Err(err).Msg("Could not get user keys")
}
keys := strings.Split(string(body), "\n")
if len(keys[len(keys)-1]) == 0 {
keys = keys[:len(keys)-1]
}
for _, key := range keys {
sshKey := models.SSHKey{
Title: "Added from " + user.Provider,
Content: key,
User: *userDB,
}
if err = sshKey.Create(); err != nil {
addFlash(ctx, "Could not create ssh key", "error")
log.Error().Err(err).Msg("Could not create ssh key")
}
}
}
}
sess := getSession(ctx)
sess.Values["user"] = userDB.ID
saveSession(sess, ctx)
deleteCsrfCookie(ctx)
return redirect(ctx, "/")
}
func oauth(ctx echo.Context) error {
provider := ctx.Param("provider")
currUser := getUserLogged(ctx)
if currUser != nil {
isDelete := false
var err error
switch provider {
case "github":
if currUser.GithubID != "" {
isDelete = true
err = currUser.DeleteProvider(provider)
}
case "gitea":
if currUser.GiteaID != "" {
isDelete = true
err = currUser.DeleteProvider(provider)
}
}
if err != nil {
return errorRes(500, "Cannot unlink account from "+strings.Title(provider), err)
}
if isDelete {
addFlash(ctx, "Account unlinked from "+strings.Title(provider), "success")
return redirect(ctx, "/settings")
}
}
ctxValue := context.WithValue(ctx.Request().Context(), "provider", provider)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
if provider != "github" && provider != "gitea" {
return errorRes(400, "Unsupported provider", nil)
}
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
return nil
}
func logout(ctx echo.Context) error {
deleteSession(ctx)
deleteCsrfCookie(ctx)

View File

@ -2,8 +2,6 @@ package web
import (
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"fmt"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/ssh"
@ -76,15 +74,12 @@ func sshKeysProcess(ctx echo.Context) error {
key.UserID = user.ID
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
if err != nil {
addFlash(ctx, "Invalid SSH key", "error")
return redirect(ctx, "/settings")
}
sha := sha256.Sum256(pubKey.Marshal())
key.SHA = base64.StdEncoding.EncodeToString(sha[:])
if err := key.Create(); err != nil {
return errorRes(500, "Cannot add SSH key", err)
}

View File

@ -8,6 +8,10 @@ import (
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/gitea"
"github.com/markbates/goth/providers/github"
"github.com/rs/zerolog/log"
"html/template"
"io"
@ -96,6 +100,17 @@ func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Con
func Start() {
store = sessions.NewCookieStore([]byte("opengist"))
gothic.Store = store
goth.UseProviders(
github.New("d92c7e165383b2804407", "ffc450216b9776a752cdb0e533f953f65ce632a3", "http://localhost:6157/oauth/github/callback"),
gitea.NewCustomisedURL(
"efdd0fed-6972-42ce-8f9f-e65b9fd0ca09",
"gto_dwilh6ia4nic4f4dt5owv4h7rvuss5ajw2ctqqa44xcpwevyg6wq",
"http://localhost:6157/oauth/gitea/callback",
"http://localhost:3000/login/oauth/authorize",
"http://localhost:3000/login/oauth/access_token",
"http://localhost:3000/api/v1/user"),
)
assetsFS := echo.MustSubFS(EmbedFS, "public/assets")
e := echo.New()
@ -166,6 +181,8 @@ func Start() {
g1.GET("/login", login)
g1.POST("/login", processLogin)
g1.GET("/logout", logout)
g1.GET("/oauth/:provider", oauth)
g1.GET("/oauth/:provider/callback", oauthCallback)
g1.GET("/settings", userSettings, logged)
g1.POST("/settings/email", emailProcess, logged)