Compare commits

..

6 Commits

13 changed files with 511 additions and 379 deletions

View File

@ -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 {
if err == account.ErrQueryingAcct || err == account.ErrFailedCreateAccount {
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
}

View File

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

View File

@ -38,3 +38,35 @@ func tryParseRequest(r interface{}, ctx iris.Context) (interface{}, bool) {
return data, true
}
func sendErrorCustom(ctx iris.Context, err error, customError error, irisError int) bool {
if err != nil {
if customError != nil {
err = customError
}
ctx.StopWithError(irisError, err)
return true
}
return false
}
func internalError(ctx iris.Context, err error) bool {
return sendErrorCustom(ctx, err, nil, iris.StatusInternalServerError)
}
func internalErrorCustom(ctx iris.Context, err error, customError error) bool {
return sendErrorCustom(ctx, err, customError, iris.StatusInternalServerError)
}
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))
}
}

View File

@ -3,7 +3,9 @@ package controller
import (
"errors"
"git.lumeweb.com/LumeWeb/portal/cid"
"git.lumeweb.com/LumeWeb/portal/controller/response"
"git.lumeweb.com/LumeWeb/portal/logger"
"git.lumeweb.com/LumeWeb/portal/middleware"
"git.lumeweb.com/LumeWeb/portal/service/files"
"github.com/kataras/iris/v12"
"go.uber.org/zap"
@ -11,14 +13,13 @@ import (
)
type FilesController struct {
Ctx iris.Context
}
type UploadResponse struct {
Cid string `json:"cid"`
Controller
}
type StatusResponse struct {
Status string `json:"status"`
func (f *FilesController) BeginRequest(ctx iris.Context) {
middleware.VerifyJwt(ctx)
}
func (f *FilesController) EndRequest(ctx iris.Context) {
}
func (f *FilesController) PostUpload() {
@ -44,7 +45,7 @@ func (f *FilesController) PostUpload() {
return
}
err = ctx.JSON(&UploadResponse{Cid: cidString})
err = ctx.JSON(&response.UploadResponse{Cid: cidString})
if err != nil {
logger.Get().Error("failed to create response", zap.Error(err))
@ -101,35 +102,9 @@ func (f *FilesController) GetStatusBy(cidString string) {
break
}
err := ctx.JSON(&StatusResponse{Status: statusCode})
if err != nil {
logger.Get().Error("failed to create response", zap.Error(err))
}
f.respondJSON(&response.StatusResponse{Status: statusCode})
}
func sendErrorCustom(ctx iris.Context, err error, customError error, irisError int) bool {
if err != nil {
if customError != nil {
err = customError
}
ctx.StopWithError(irisError, err)
return true
}
return false
}
func internalError(ctx iris.Context, err error) bool {
return sendErrorCustom(ctx, err, nil, iris.StatusInternalServerError)
}
func internalErrorCustom(ctx iris.Context, err error, customError error) bool {
return sendErrorCustom(ctx, err, customError, iris.StatusInternalServerError)
}
func sendError(ctx iris.Context, err error, irisError int) bool {
return sendErrorCustom(ctx, err, nil, irisError)
}
func validateCid(cidString string, validateStatus bool, ctx iris.Context) (string, bool) {
_, err := cid.Valid(cidString)
if sendError(ctx, err, iris.StatusBadRequest) {

View File

@ -0,0 +1,5 @@
package response
type StatusResponse struct {
Status string `json:"status"`
}

View File

@ -0,0 +1,5 @@
package response
type UploadResponse struct {
Cid string `json:"cid"`
}

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/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()

20
middleware/jwt.go Normal file
View File

@ -0,0 +1,20 @@
package middleware
import (
"git.lumeweb.com/LumeWeb/portal/service/auth"
"github.com/kataras/iris/v12"
)
func VerifyJwt(ctx iris.Context) {
token := auth.GetRequestAuthCode(ctx)
if len(token) == 0 {
ctx.StopWithError(iris.StatusUnauthorized, auth.ErrInvalidToken)
return
}
if err := auth.VerifyLoginToken(token); err != nil {
ctx.StopWithError(iris.StatusUnauthorized, auth.ErrInvalidToken)
return
}
}

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