refactor: move all primary logic to service packages and standardize error objects
This commit is contained in:
parent
d18be0acc8
commit
73e1c5a363
|
@ -1,32 +1,13 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/request"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/account"
|
||||
"github.com/kataras/iris/v12"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AccountController struct {
|
||||
Ctx iris.Context
|
||||
}
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
// Generate a new bcrypt hash from the provided password.
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to hash password", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Convert the hashed password to a string and return it.
|
||||
return string(hashedPassword), nil
|
||||
Controller
|
||||
}
|
||||
|
||||
func (a *AccountController) PostRegister() {
|
||||
|
@ -37,75 +18,15 @@ func (a *AccountController) PostRegister() {
|
|||
|
||||
r, _ := ri.(*request.RegisterRequest)
|
||||
|
||||
// Check if an account with the same email address already exists.
|
||||
existingAccount := model.Account{}
|
||||
err := db.Get().Where("email = ?", r.Email).First(&existingAccount).Error
|
||||
if err == nil {
|
||||
logger.Get().Debug("account with email already exists", zap.Error(err), zap.String("email", r.Email))
|
||||
// An account with the same email address already exists.
|
||||
// Return an error response to the client.
|
||||
a.Ctx.StopWithError(iris.StatusConflict, errors.New("an account with this email address already exists"))
|
||||
return
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Get().Error("error querying accounts", zap.Error(err), zap.String("email", r.Email))
|
||||
// An unexpected error occurred while querying the database.
|
||||
// Return an error response to the client.
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
err := account.Register(r.Email, r.Password, r.Pubkey)
|
||||
|
||||
if len(r.Pubkey) > 0 {
|
||||
r.Pubkey = strings.ToLower(r.Pubkey)
|
||||
var count int64
|
||||
err := db.Get().Model(&model.Key{}).Where("pubkey = ?", r.Pubkey).Count(&count).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Get().Error("error querying accounts", zap.Error(err), zap.String("pubkey", r.Pubkey))
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
}
|
||||
if count > 0 {
|
||||
logger.Get().Debug("account with pubkey already exists", zap.Error(err), zap.String("pubkey", r.Pubkey))
|
||||
// An account with the same pubkey already exists.
|
||||
// Return an error response to the client.
|
||||
a.Ctx.StopWithError(iris.StatusConflict, errors.New("an account with this pubkey already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create a new Account model with the provided email and hashed password.
|
||||
account := model.Account{
|
||||
Email: r.Email,
|
||||
}
|
||||
|
||||
// Hash the password before saving it to the database.
|
||||
if len(r.Password) > 0 {
|
||||
hashedPassword, err := hashPassword(r.Password)
|
||||
if err != nil {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
account.Password = &hashedPassword
|
||||
}
|
||||
|
||||
err = db.Get().Transaction(func(tx *gorm.DB) error {
|
||||
// do some database operations in the transaction (use 'tx' from this point, not 'db')
|
||||
if err := tx.Create(&account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(r.Pubkey) > 0 {
|
||||
if err := tx.Create(&model.Key{Account: account, Pubkey: strings.ToLower(r.Pubkey)}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// return nil will commit the whole transaction
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to create account", zap.Error(err))
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
if err == account.ErrQueryingAcct || err == account.ErrFailedCreateAccount {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,119 +1,14 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/request"
|
||||
"git.lumeweb.com/LumeWeb/portal/controller/response"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"github.com/joomcode/errorx"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/auth"
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/jwt"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var sharedKey = []byte("sercrethatmaycontainch@r$32chars")
|
||||
|
||||
var blocklist *jwt.Blocklist
|
||||
|
||||
func init() {
|
||||
blocklist = jwt.NewBlocklist(1 * time.Hour)
|
||||
}
|
||||
|
||||
type AuthController struct {
|
||||
Ctx iris.Context
|
||||
}
|
||||
|
||||
// verifyPassword compares the provided plaintext password with a hashed password and returns an error if they don't match.
|
||||
func verifyPassword(hashedPassword, password string) error {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
if err != nil {
|
||||
return errors.New("invalid email or password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateToken generates a JWT token for the given account ID.
|
||||
func generateToken(maxAge time.Duration) (string, error) {
|
||||
// Define the JWT claims.
|
||||
claim := jwt.Claims{
|
||||
Expiry: time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours.
|
||||
IssuedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
token, err := jwt.Sign(jwt.HS256, sharedKey, claim, jwt.MaxAge(maxAge))
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to sign jwt", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
func generateAndSaveLoginToken(accountID uint, maxAge time.Duration) (string, error) {
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateToken(maxAge)
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to generate token", zap.Error(err))
|
||||
return "", fmt.Errorf("failed to generate token: %s", err)
|
||||
}
|
||||
|
||||
verifiedToken, _ := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
||||
var claim *jwt.Claims
|
||||
|
||||
_ = verifiedToken.Claims(&claim)
|
||||
|
||||
// Save the token to the database.
|
||||
session := model.LoginSession{
|
||||
Account: model.Account{ID: accountID},
|
||||
Token: token,
|
||||
Expiration: claim.ExpiresAt(),
|
||||
}
|
||||
|
||||
if err := db.Get().Create(&session).Error; err != nil {
|
||||
msg := "failed to save token"
|
||||
logger.Get().Error(msg, zap.Error(err))
|
||||
return "", errorx.Decorate(err, msg)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func generateAndSaveChallengeToken(accountID uint, maxAge time.Duration) (string, error) {
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateToken(maxAge)
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to generate token", zap.Error(err))
|
||||
return "", fmt.Errorf("failed to generate token: %s", err)
|
||||
}
|
||||
|
||||
verifiedToken, _ := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
||||
var claim *jwt.Claims
|
||||
|
||||
_ = verifiedToken.Claims(&claim)
|
||||
|
||||
// Save the token to the database.
|
||||
keyChallenge := model.KeyChallenge{
|
||||
AccountID: accountID,
|
||||
Challenge: token,
|
||||
Expiration: claim.ExpiresAt(),
|
||||
}
|
||||
|
||||
if err := db.Get().Create(&keyChallenge).Error; err != nil {
|
||||
msg := "failed to save token"
|
||||
logger.Get().Error(msg, zap.Error(err))
|
||||
return "", errorx.Decorate(err, msg)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
Controller
|
||||
}
|
||||
|
||||
// PostLogin handles the POST /api/auth/login request to authenticate a user and return a JWT token.
|
||||
|
@ -125,43 +20,18 @@ func (a *AuthController) PostLogin() {
|
|||
|
||||
r, _ := ri.(*request.LoginRequest)
|
||||
|
||||
// Retrieve the account for the given email.
|
||||
account := model.Account{}
|
||||
if err := db.Get().Where("email = ?", r.Email).First(&account).Error; err != nil {
|
||||
msg := "invalid email or password"
|
||||
logger.Get().Debug(msg, zap.Error(err))
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New(msg))
|
||||
return
|
||||
}
|
||||
token, err := auth.LoginWithPassword(r.Email, r.Password)
|
||||
|
||||
if account.Password == nil || len(*account.Password) == 0 {
|
||||
msg := "only pubkey login is supported"
|
||||
logger.Get().Debug(msg)
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New(msg))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the provided password against the hashed password stored in the database.
|
||||
if err := verifyPassword(*account.Password, r.Password); err != nil {
|
||||
msg := "invalid email or password"
|
||||
logger.Get().Debug(msg, zap.Error(err))
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New(msg))
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateAndSaveLoginToken(account.ID, 24*time.Hour)
|
||||
if err != nil {
|
||||
logger.Get().Debug("failed to generate token", zap.Error(err))
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, fmt.Errorf("failed to generate token: %s", err))
|
||||
if err == auth.ErrFailedGenerateToken {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Return the JWT token to the client.
|
||||
err = a.Ctx.JSON(&response.LoginResponse{Token: token})
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to generate response", zap.Error(err))
|
||||
}
|
||||
a.respondJSON(&response.LoginResponse{Token: token})
|
||||
}
|
||||
|
||||
// PostChallenge handles the POST /api/auth/pubkey/challenge request to generate a challenge for a user's public key.
|
||||
|
@ -173,27 +43,17 @@ func (a *AuthController) PostPubkeyChallenge() {
|
|||
|
||||
r, _ := (ri).(*request.PubkeyChallengeRequest)
|
||||
|
||||
r.Pubkey = strings.ToLower(r.Pubkey)
|
||||
|
||||
// Retrieve the account for the given email.
|
||||
account := model.Key{}
|
||||
if err := db.Get().Where("pubkey = ?", r.Pubkey).First(&account).Error; err != nil {
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid pubkey"))
|
||||
challenge, err := auth.GeneratePubkeyChallenge(r.Pubkey)
|
||||
if err != nil {
|
||||
if err == auth.ErrFailedGenerateKeyChallenge {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a random challenge string.
|
||||
challenge, err := generateAndSaveChallengeToken(account.AccountID, time.Minute)
|
||||
if err != nil {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, errors.New("failed to generate challenge"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return the challenge to the client.
|
||||
err = a.Ctx.JSON(&response.ChallengeResponse{Challenge: challenge})
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to create response", zap.Error(err))
|
||||
}
|
||||
a.respondJSON(&response.ChallengeResponse{Challenge: challenge})
|
||||
}
|
||||
|
||||
// PostKeyLogin handles the POST /api/auth/pubkey/login request to authenticate a user using a public key challenge and return a JWT token.
|
||||
|
@ -205,78 +65,18 @@ func (a *AuthController) PostPubkeyLogin() {
|
|||
|
||||
r, _ := ri.(*request.PubkeyLoginRequest)
|
||||
|
||||
r.Pubkey = strings.ToLower(r.Pubkey)
|
||||
r.Signature = strings.ToLower(r.Signature)
|
||||
token, err := auth.LoginWithPubkey(r.Pubkey, r.Challenge, r.Signature)
|
||||
|
||||
// Retrieve the key challenge for the given challenge.
|
||||
challenge := model.KeyChallenge{}
|
||||
if err := db.Get().Where("challenge = ?", r.Challenge).First(&challenge).Error; err != nil {
|
||||
msg := "invalid key challenge"
|
||||
logger.Get().Debug(msg, zap.Error(err), zap.String("challenge", r.Challenge))
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
||||
return
|
||||
}
|
||||
|
||||
verifiedToken, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Challenge), blocklist)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("invalid key challenge: %s", err.Error())
|
||||
logger.Get().Debug(msg, zap.Error(err), zap.String("challenge", r.Challenge))
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
||||
if err == auth.ErrFailedGenerateKeyChallenge || err == auth.ErrFailedGenerateToken || err == auth.ErrFailedSaveToken {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
} else {
|
||||
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
rawPubKey, err := hex.DecodeString(r.Pubkey)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("invalid pubkey: %s", err.Error())
|
||||
logger.Get().Debug(msg, zap.Error(err), zap.String("pubkey", r.Pubkey))
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
||||
return
|
||||
}
|
||||
|
||||
rawSignature, err := hex.DecodeString(r.Signature)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("invalid signature: %s", err.Error())
|
||||
logger.Get().Debug(msg, zap.Error(err), zap.String("signature", r.Signature))
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
||||
return
|
||||
}
|
||||
|
||||
publicKeyDecoded := ed25519.PublicKey(rawPubKey)
|
||||
|
||||
// Verify the challenge signature.
|
||||
if !ed25519.Verify(publicKeyDecoded, []byte(r.Challenge), rawSignature) {
|
||||
msg := "invalid challenge"
|
||||
logger.Get().Debug(msg, zap.Error(err), zap.String("challenge", r.Challenge))
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
||||
}
|
||||
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateAndSaveLoginToken(challenge.AccountID, 24*time.Hour)
|
||||
if err != nil {
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, errorx.RejectedOperation.Wrap(err, "failed to generate token"))
|
||||
return
|
||||
}
|
||||
|
||||
err = blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims)
|
||||
if err != nil {
|
||||
msg := "failed to invalidate token"
|
||||
logger.Get().Error(msg, zap.Error(err), zap.String("token", hex.EncodeToString(verifiedToken.Token)))
|
||||
a.Ctx.StopWithError(iris.StatusInternalServerError, errorx.RejectedOperation.Wrap(err, msg))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Get().Delete(&challenge).Error; err != nil {
|
||||
msg := "failed to delete key challenge"
|
||||
logger.Get().Error(msg, zap.Error(err), zap.Any("key_challenge", challenge))
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
||||
return
|
||||
}
|
||||
|
||||
// Return the JWT token to the client.
|
||||
err = a.Ctx.JSON(&response.LoginResponse{Token: token})
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to create response", zap.Error(err))
|
||||
}
|
||||
a.respondJSON(&response.LoginResponse{Token: token})
|
||||
|
||||
}
|
||||
|
||||
|
@ -289,20 +89,10 @@ func (a *AuthController) PostLogout() {
|
|||
|
||||
r, _ := ri.(*request.LogoutRequest)
|
||||
|
||||
// Verify the provided token.
|
||||
claims, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Token), blocklist)
|
||||
if err != nil {
|
||||
msg := "invalid token"
|
||||
logger.Get().Debug(msg, zap.Error(err))
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New(msg))
|
||||
return
|
||||
}
|
||||
err := auth.Logout(r.Token)
|
||||
|
||||
err = blocklist.InvalidateToken(claims.Token, claims.StandardClaims)
|
||||
if err != nil {
|
||||
msg := "failed to invalidate token"
|
||||
logger.Get().Error(msg, zap.Error(err), zap.String("token", hex.EncodeToString(claims.Token)))
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New(msg))
|
||||
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -59,3 +59,14 @@ func internalErrorCustom(ctx iris.Context, err error, customError error) bool {
|
|||
func sendError(ctx iris.Context, err error, irisError int) bool {
|
||||
return sendErrorCustom(ctx, err, nil, irisError)
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
Ctx iris.Context
|
||||
}
|
||||
|
||||
func (c Controller) respondJSON(data interface{}) {
|
||||
err := c.Ctx.JSON(data)
|
||||
if err != nil {
|
||||
logger.Get().Error("failed to generate response", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
|
20
jwt/jwt.go
20
jwt/jwt.go
|
@ -1,20 +0,0 @@
|
|||
package jwt
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12/middleware/jwt"
|
||||
_ "github.com/kataras/iris/v12/middleware/jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
Secret = []byte("signature_hmac_secret_shared_key")
|
||||
v *jwt.Verifier
|
||||
)
|
||||
|
||||
func init() {
|
||||
v = jwt.NewVerifier(jwt.HS256, Secret)
|
||||
v.WithDefaultBlocklist()
|
||||
}
|
||||
|
||||
func Get() *jwt.Verifier {
|
||||
return v
|
||||
}
|
4
main.go
4
main.go
|
@ -7,6 +7,7 @@ import (
|
|||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
_ "git.lumeweb.com/LumeWeb/portal/docs"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/auth"
|
||||
"git.lumeweb.com/LumeWeb/portal/service/files"
|
||||
"git.lumeweb.com/LumeWeb/portal/tus"
|
||||
"github.com/iris-contrib/swagger"
|
||||
|
@ -42,9 +43,9 @@ func main() {
|
|||
|
||||
// Initialize the database connection
|
||||
db.Init()
|
||||
|
||||
logger.Init()
|
||||
files.Init()
|
||||
auth.Init()
|
||||
|
||||
// Create a new Iris app instance
|
||||
app := iris.New()
|
||||
|
@ -68,6 +69,7 @@ func main() {
|
|||
|
||||
mvc.Configure(v1.Party("/files"), func(app *mvc.Application) {
|
||||
app.Handle(new(controller.FilesController))
|
||||
app.Router.Use()
|
||||
})
|
||||
|
||||
tusHandler := tus.Init()
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmailExists = errors.New("Account with email already exists")
|
||||
ErrPubkeyExists = errors.New("Account with pubkey already exists")
|
||||
ErrQueryingAcct = errors.New("Error querying accounts")
|
||||
ErrFailedHashPassword = errors.New("Failed to hash password")
|
||||
ErrFailedCreateAccount = errors.New("Failed to create account")
|
||||
)
|
||||
|
||||
func Register(email string, password string, pubkey string) error {
|
||||
// Check if an account with the same email address already exists.
|
||||
existingAccount := model.Account{}
|
||||
err := db.Get().Where("email = ?", email).First(&existingAccount).Error
|
||||
if err == nil {
|
||||
logger.Get().Debug(ErrEmailExists.Error(), zap.Error(err), zap.String("email", email))
|
||||
// An account with the same email address already exists.
|
||||
// Return an error response to the client.
|
||||
return ErrEmailExists
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Get().Error(ErrQueryingAcct.Error(), zap.Error(err))
|
||||
return ErrQueryingAcct
|
||||
}
|
||||
|
||||
if len(pubkey) > 0 {
|
||||
pubkey = strings.ToLower(pubkey)
|
||||
var count int64
|
||||
err := db.Get().Model(&model.Key{}).Where("pubkey = ?", pubkey).Count(&count).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Get().Error(ErrQueryingAcct.Error(), zap.Error(err), zap.String("pubkey", pubkey))
|
||||
return ErrQueryingAcct
|
||||
}
|
||||
if count > 0 {
|
||||
logger.Get().Debug(ErrPubkeyExists.Error(), zap.Error(err), zap.String("pubkey", pubkey))
|
||||
// An account with the same pubkey already exists.
|
||||
// Return an error response to the client.
|
||||
return ErrPubkeyExists
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create a new Account model with the provided email and hashed password.
|
||||
account := model.Account{
|
||||
Email: email,
|
||||
}
|
||||
|
||||
// Hash the password before saving it to the database.
|
||||
if len(password) > 0 {
|
||||
hashedPassword, err := hashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account.Password = &hashedPassword
|
||||
}
|
||||
|
||||
err = db.Get().Transaction(func(tx *gorm.DB) error {
|
||||
// do some database operations in the transaction (use 'tx' from this point, not 'db')
|
||||
if err := tx.Create(&account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pubkey) > 0 {
|
||||
if err := tx.Create(&model.Key{Account: account, Pubkey: pubkey}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// return nil will commit the whole transaction
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedCreateAccount.Error(), zap.Error(err))
|
||||
return ErrFailedCreateAccount
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
// Generate a new bcrypt hash from the provided password.
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedHashPassword.Error(), zap.Error(err))
|
||||
return "", ErrFailedHashPassword
|
||||
}
|
||||
|
||||
// Convert the hashed password to a string and return it.
|
||||
return string(hashedPassword), nil
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"github.com/kataras/jwt"
|
||||
"go.uber.org/zap"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var sharedKey = []byte("sercrethatmaycontainch@r$32chars")
|
||||
|
||||
var blocklist *jwt.Blocklist
|
||||
|
||||
var (
|
||||
ErrInvalidEmailPassword = errors.New("Invalid email or password")
|
||||
ErrPubkeyOnly = errors.New("Only pubkey login is supported")
|
||||
ErrFailedGenerateToken = errors.New("Failed to generate token")
|
||||
ErrFailedGenerateKeyChallenge = errors.New("Failed to generate key challenge")
|
||||
ErrFailedSignJwt = errors.New("Failed to sign jwt")
|
||||
ErrFailedSaveToken = errors.New("Failed to sign token")
|
||||
ErrFailedDeleteKeyChallenge = errors.New("Failed to delete key challenge")
|
||||
ErrFailedInvalidateToken = errors.New("Failed to invalidate token")
|
||||
ErrInvalidKeyChallenge = errors.New("Invalid key challenge")
|
||||
ErrInvalidPubkey = errors.New("Invalid pubkey")
|
||||
ErrInvalidSignature = errors.New("Invalid signature")
|
||||
ErrInvalidToken = errors.New("Invalid token")
|
||||
)
|
||||
|
||||
func Init() {
|
||||
blocklist = jwt.NewBlocklist(0)
|
||||
}
|
||||
|
||||
func LoginWithPassword(email string, password string) (string, error) {
|
||||
// Retrieve the account for the given email.
|
||||
account := model.Account{}
|
||||
if err := db.Get().Model(&account).Where("email = ?", email).First(&account).Error; err != nil {
|
||||
logger.Get().Debug(ErrInvalidEmailPassword.Error(), zap.String("email", email))
|
||||
return "", ErrInvalidEmailPassword
|
||||
}
|
||||
|
||||
if account.Password == nil || len(*account.Password) == 0 {
|
||||
logger.Get().Debug(ErrPubkeyOnly.Error(), zap.String("email", email))
|
||||
return "", ErrPubkeyOnly
|
||||
}
|
||||
|
||||
// Verify the provided password against the hashed password stored in the database.
|
||||
if err := verifyPassword(*account.Password, password); err != nil {
|
||||
logger.Get().Debug(ErrPubkeyOnly.Error(), zap.String("email", email))
|
||||
return "", ErrInvalidEmailPassword
|
||||
}
|
||||
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateAndSaveLoginToken(account.ID, 24*time.Hour)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func LoginWithPubkey(pubkey string, challenge string, signature string) (string, error) {
|
||||
pubkey = strings.ToLower(pubkey)
|
||||
signature = strings.ToLower(signature)
|
||||
|
||||
// Retrieve the key challenge for the given challenge.
|
||||
challengeObj := model.KeyChallenge{}
|
||||
if err := db.Get().Model(challengeObj).Where("challenge = ?", challenge).First(&challengeObj).Error; err != nil {
|
||||
logger.Get().Debug(ErrInvalidKeyChallenge.Error(), zap.Error(err), zap.String("challenge", challenge))
|
||||
return "", ErrInvalidKeyChallenge
|
||||
}
|
||||
|
||||
verifiedToken, err := jwt.Verify(jwt.HS256, sharedKey, []byte(challenge), blocklist)
|
||||
if err != nil {
|
||||
logger.Get().Debug(ErrInvalidKeyChallenge.Error(), zap.Error(err), zap.String("challenge", challenge))
|
||||
return "", ErrInvalidKeyChallenge
|
||||
}
|
||||
|
||||
rawPubKey, err := hex.DecodeString(pubkey)
|
||||
if err != nil {
|
||||
logger.Get().Debug(ErrInvalidPubkey.Error(), zap.Error(err), zap.String("pubkey", pubkey))
|
||||
return "", ErrInvalidPubkey
|
||||
}
|
||||
|
||||
rawSignature, err := hex.DecodeString(signature)
|
||||
if err != nil {
|
||||
logger.Get().Debug(ErrInvalidPubkey.Error(), zap.Error(err), zap.String("signature", pubkey))
|
||||
return "", ErrInvalidSignature
|
||||
}
|
||||
|
||||
publicKeyDecoded := ed25519.PublicKey(rawPubKey)
|
||||
|
||||
// Verify the challenge signature.
|
||||
if !ed25519.Verify(publicKeyDecoded, []byte(challenge), rawSignature) {
|
||||
logger.Get().Debug(ErrInvalidKeyChallenge.Error(), zap.Error(err), zap.String("challenge", challenge))
|
||||
return "", ErrInvalidKeyChallenge
|
||||
}
|
||||
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateAndSaveLoginToken(challengeObj.AccountID, 24*time.Hour)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedInvalidateToken.Error(), zap.Error(err), zap.String("pubkey", pubkey), zap.ByteString("token", verifiedToken.Token), zap.String("challenge", challenge))
|
||||
return "", ErrFailedInvalidateToken
|
||||
}
|
||||
|
||||
if err := db.Get().Delete(&challenge).Error; err != nil {
|
||||
logger.Get().Debug(ErrFailedDeleteKeyChallenge.Error(), zap.Error(err))
|
||||
return "", ErrFailedDeleteKeyChallenge
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func GeneratePubkeyChallenge(pubkey string) (string, error) {
|
||||
pubkey = strings.ToLower(pubkey)
|
||||
|
||||
// Retrieve the account for the given email.
|
||||
account := model.Key{}
|
||||
if err := db.Get().Where("pubkey = ?", pubkey).First(&account).Error; err != nil {
|
||||
logger.Get().Debug("failed to query pubkey", zap.Error(err))
|
||||
return "", errors.New("invalid pubkey")
|
||||
}
|
||||
|
||||
// Generate a random challenge string.
|
||||
challenge, err := generateAndSaveChallengeToken(account.AccountID, time.Minute)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedGenerateKeyChallenge.Error())
|
||||
return "", ErrFailedGenerateKeyChallenge
|
||||
}
|
||||
|
||||
return challenge, nil
|
||||
}
|
||||
|
||||
func Logout(token string) error {
|
||||
// Verify the provided token.
|
||||
claims, err := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
||||
if err != nil {
|
||||
logger.Get().Debug(ErrInvalidToken.Error(), zap.Error(err))
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
err = blocklist.InvalidateToken(claims.Token, claims.StandardClaims)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedInvalidateToken.Error(), zap.Error(err), zap.String("token", token))
|
||||
return ErrFailedInvalidateToken
|
||||
}
|
||||
|
||||
// Retrieve the key challenge for the given challenge.
|
||||
session := model.LoginSession{}
|
||||
if err := db.Get().Model(session).Where("token = ?", token).First(&session).Error; err != nil {
|
||||
logger.Get().Debug(ErrFailedInvalidateToken.Error(), zap.Error(err), zap.String("token", token))
|
||||
return ErrFailedInvalidateToken
|
||||
}
|
||||
|
||||
db.Get().Delete(&session)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func VerifyLoginToken(token string) error {
|
||||
_, err := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session := model.LoginSession{}
|
||||
if err := db.Get().Model(session).Where("token = ?", token).First(&session).Error; err != nil {
|
||||
logger.Get().Debug(ErrInvalidToken.Error(), zap.Error(err), zap.String("token", token))
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/portal/db"
|
||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||
"git.lumeweb.com/LumeWeb/portal/model"
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/jwt"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// verifyPassword compares the provided plaintext password with a hashed password and returns an error if they don't match.
|
||||
func verifyPassword(hashedPassword, password string) error {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
if err != nil {
|
||||
return errors.New("invalid email or password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateToken generates a JWT token for the given account ID.
|
||||
func generateToken(maxAge time.Duration) (string, error) {
|
||||
// Define the JWT claims.
|
||||
claim := jwt.Claims{
|
||||
Expiry: time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours.
|
||||
IssuedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
token, err := jwt.Sign(jwt.HS256, sharedKey, claim, jwt.MaxAge(maxAge))
|
||||
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedSignJwt.Error(), zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
func generateAndSaveLoginToken(accountID uint, maxAge time.Duration) (string, error) {
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateToken(maxAge)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedGenerateToken.Error())
|
||||
return "", ErrFailedGenerateToken
|
||||
}
|
||||
|
||||
verifiedToken, _ := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
||||
var claim *jwt.Claims
|
||||
|
||||
_ = verifiedToken.Claims(&claim)
|
||||
|
||||
// Save the token to the database.
|
||||
session := model.LoginSession{
|
||||
Account: model.Account{ID: accountID},
|
||||
Token: token,
|
||||
Expiration: claim.ExpiresAt(),
|
||||
}
|
||||
|
||||
if err := db.Get().Create(&session).Error; err != nil {
|
||||
logger.Get().Error(ErrFailedSaveToken.Error(), zap.Uint("account_id", accountID), zap.Duration("max_age", maxAge))
|
||||
return "", ErrFailedSaveToken
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func generateAndSaveChallengeToken(accountID uint, maxAge time.Duration) (string, error) {
|
||||
// Generate a JWT token for the authenticated user.
|
||||
token, err := generateToken(maxAge)
|
||||
if err != nil {
|
||||
logger.Get().Error(ErrFailedGenerateToken.Error(), zap.Error(err))
|
||||
return "", ErrFailedGenerateToken
|
||||
}
|
||||
|
||||
verifiedToken, _ := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
||||
var claim *jwt.Claims
|
||||
|
||||
_ = verifiedToken.Claims(&claim)
|
||||
|
||||
// Save the token to the database.
|
||||
keyChallenge := model.KeyChallenge{
|
||||
AccountID: accountID,
|
||||
Challenge: token,
|
||||
Expiration: claim.ExpiresAt(),
|
||||
}
|
||||
|
||||
if err := db.Get().Create(&keyChallenge).Error; err != nil {
|
||||
logger.Get().Error(ErrFailedSaveToken.Error(), zap.Error(err))
|
||||
return "", ErrFailedSaveToken
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func GetRequestAuthCode(ctx iris.Context) string {
|
||||
authHeader := ctx.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// pure check: authorization header format must be Bearer {token}
|
||||
authHeaderParts := strings.Split(authHeader, " ")
|
||||
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return authHeaderParts[1]
|
||||
}
|
Loading…
Reference in New Issue