refactor: move all primary logic to service packages and standardize error objects

This commit is contained in:
Derrick Hammer 2023-06-09 04:03:29 -04:00
parent d18be0acc8
commit 73e1c5a363
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
9 changed files with 451 additions and 345 deletions

View File

@ -1,32 +1,13 @@
package controller package controller
import ( import (
"errors"
"git.lumeweb.com/LumeWeb/portal/controller/request" "git.lumeweb.com/LumeWeb/portal/controller/request"
"git.lumeweb.com/LumeWeb/portal/db" "git.lumeweb.com/LumeWeb/portal/service/account"
"git.lumeweb.com/LumeWeb/portal/logger"
"git.lumeweb.com/LumeWeb/portal/model"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"strings"
) )
type AccountController struct { type AccountController struct {
Ctx iris.Context Controller
}
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
} }
func (a *AccountController) PostRegister() { func (a *AccountController) PostRegister() {
@ -37,75 +18,15 @@ func (a *AccountController) PostRegister() {
r, _ := ri.(*request.RegisterRequest) r, _ := ri.(*request.RegisterRequest)
// Check if an account with the same email address already exists. err := account.Register(r.Email, r.Password, r.Pubkey)
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
}
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 { if err != nil {
if err == account.ErrQueryingAcct || err == account.ErrFailedCreateAccount {
a.Ctx.StopWithError(iris.StatusInternalServerError, err) a.Ctx.StopWithError(iris.StatusInternalServerError, err)
return } else {
a.Ctx.StopWithError(iris.StatusBadRequest, 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(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)
return return
} }

View File

@ -1,119 +1,14 @@
package controller package controller
import ( import (
"crypto/ed25519"
"encoding/hex"
"errors"
"fmt"
"git.lumeweb.com/LumeWeb/portal/controller/request" "git.lumeweb.com/LumeWeb/portal/controller/request"
"git.lumeweb.com/LumeWeb/portal/controller/response" "git.lumeweb.com/LumeWeb/portal/controller/response"
"git.lumeweb.com/LumeWeb/portal/db" "git.lumeweb.com/LumeWeb/portal/service/auth"
"git.lumeweb.com/LumeWeb/portal/logger"
"git.lumeweb.com/LumeWeb/portal/model"
"github.com/joomcode/errorx"
"github.com/kataras/iris/v12" "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 { type AuthController struct {
Ctx iris.Context Controller
}
// 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
} }
// PostLogin handles the POST /api/auth/login request to authenticate a user and return a JWT token. // 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) r, _ := ri.(*request.LoginRequest)
// Retrieve the account for the given email. token, err := auth.LoginWithPassword(r.Email, r.Password)
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
}
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 { if err != nil {
logger.Get().Debug("failed to generate token", zap.Error(err)) if err == auth.ErrFailedGenerateToken {
a.Ctx.StopWithError(iris.StatusInternalServerError, fmt.Errorf("failed to generate token: %s", err)) a.Ctx.StopWithError(iris.StatusInternalServerError, err)
} else {
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
}
return return
} }
// Return the JWT token to the client. a.respondJSON(&response.LoginResponse{Token: token})
err = a.Ctx.JSON(&response.LoginResponse{Token: token})
if err != nil {
logger.Get().Error("failed to generate response", zap.Error(err))
}
} }
// PostChallenge handles the POST /api/auth/pubkey/challenge request to generate a challenge for a user's public key. // 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, _ := (ri).(*request.PubkeyChallengeRequest)
r.Pubkey = strings.ToLower(r.Pubkey) challenge, err := auth.GeneratePubkeyChallenge(r.Pubkey)
if err != nil {
// Retrieve the account for the given email. if err == auth.ErrFailedGenerateKeyChallenge {
account := model.Key{} a.Ctx.StopWithError(iris.StatusInternalServerError, err)
if err := db.Get().Where("pubkey = ?", r.Pubkey).First(&account).Error; err != nil { } else {
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid pubkey")) a.Ctx.StopWithError(iris.StatusUnauthorized, err)
}
return return
} }
// Generate a random challenge string. a.respondJSON(&response.ChallengeResponse{Challenge: challenge})
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))
}
} }
// PostKeyLogin handles the POST /api/auth/pubkey/login request to authenticate a user using a public key challenge and return a JWT token. // 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, _ := ri.(*request.PubkeyLoginRequest)
r.Pubkey = strings.ToLower(r.Pubkey) token, err := auth.LoginWithPubkey(r.Pubkey, r.Challenge, r.Signature)
r.Signature = strings.ToLower(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 { if err != nil {
msg := fmt.Sprintf("invalid key challenge: %s", err.Error()) if err == auth.ErrFailedGenerateKeyChallenge || err == auth.ErrFailedGenerateToken || err == auth.ErrFailedSaveToken {
logger.Get().Debug(msg, zap.Error(err), zap.String("challenge", r.Challenge)) a.Ctx.StopWithError(iris.StatusInternalServerError, err)
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg)) } else {
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
}
return return
} }
rawPubKey, err := hex.DecodeString(r.Pubkey) a.respondJSON(&response.LoginResponse{Token: token})
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))
}
} }
@ -289,20 +89,10 @@ func (a *AuthController) PostLogout() {
r, _ := ri.(*request.LogoutRequest) r, _ := ri.(*request.LogoutRequest)
// Verify the provided token. err := auth.Logout(r.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 = blocklist.InvalidateToken(claims.Token, claims.StandardClaims)
if err != nil { if err != nil {
msg := "failed to invalidate token" a.Ctx.StopWithError(iris.StatusBadRequest, err)
logger.Get().Error(msg, zap.Error(err), zap.String("token", hex.EncodeToString(claims.Token)))
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New(msg))
return return
} }

View File

@ -59,3 +59,14 @@ func internalErrorCustom(ctx iris.Context, err error, customError error) bool {
func sendError(ctx iris.Context, err error, irisError int) bool { func sendError(ctx iris.Context, err error, irisError int) bool {
return sendErrorCustom(ctx, err, nil, irisError) 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))
}
}

View File

@ -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
}

View File

@ -7,6 +7,7 @@ import (
"git.lumeweb.com/LumeWeb/portal/db" "git.lumeweb.com/LumeWeb/portal/db"
_ "git.lumeweb.com/LumeWeb/portal/docs" _ "git.lumeweb.com/LumeWeb/portal/docs"
"git.lumeweb.com/LumeWeb/portal/logger" "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/service/files"
"git.lumeweb.com/LumeWeb/portal/tus" "git.lumeweb.com/LumeWeb/portal/tus"
"github.com/iris-contrib/swagger" "github.com/iris-contrib/swagger"
@ -42,9 +43,9 @@ func main() {
// Initialize the database connection // Initialize the database connection
db.Init() db.Init()
logger.Init() logger.Init()
files.Init() files.Init()
auth.Init()
// Create a new Iris app instance // Create a new Iris app instance
app := iris.New() app := iris.New()
@ -68,6 +69,7 @@ func main() {
mvc.Configure(v1.Party("/files"), func(app *mvc.Application) { mvc.Configure(v1.Party("/files"), func(app *mvc.Application) {
app.Handle(new(controller.FilesController)) app.Handle(new(controller.FilesController))
app.Router.Use()
}) })
tusHandler := tus.Init() tusHandler := tus.Init()

View File

@ -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
}

19
service/account/util.go Normal file
View File

@ -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
}

183
service/auth/auth.go Normal file
View File

@ -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
}

112
service/auth/util.go Normal file
View File

@ -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]
}