diff --git a/controller/account.go b/controller/account.go index 13257c7..1da5bab 100644 --- a/controller/account.go +++ b/controller/account.go @@ -1,15 +1,11 @@ package controller import ( - "crypto/ed25519" - "encoding/hex" "errors" - "fmt" + "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" - "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/kataras/iris/v12" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" @@ -21,34 +17,6 @@ type AccountController struct { Ctx iris.Context } -type RegisterRequest struct { - Email string `json:"email"` - Password string `json:"password"` - Pubkey string `json:"pubkey"` -} - -func CheckPubkeyValidator(value interface{}) error { - p, _ := value.(string) - pubkeyBytes, err := hex.DecodeString(p) - if err != nil { - return err - } - - if len(pubkeyBytes) != ed25519.PublicKeySize { - return errors.New(fmt.Sprintf("pubkey must be %d bytes in hexadecimal format", ed25519.PublicKeySize)) - } - - return nil -} - -func (r RegisterRequest) Validate() error { - return validation.ValidateStruct(&r, - validation.Field(&r.Email, validation.Required, is.EmailFormat), - validation.Field(&r.Pubkey, validation.When(len(r.Password) == 0, validation.Required, validation.By(CheckPubkeyValidator))), - validation.Field(&r.Password, validation.When(len(r.Pubkey) == 0, validation.Required)), - ) -} - func hashPassword(password string) (string, error) { // Generate a new bcrypt hash from the provided password. hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) @@ -62,12 +30,13 @@ func hashPassword(password string) (string, error) { } func (a *AccountController) PostRegister() { - var r RegisterRequest - - if !tryParseRequest(r, a.Ctx) { + ri, success := tryParseRequest(request.RegisterRequest{}, a.Ctx) + if !success { return } + 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 diff --git a/controller/auth.go b/controller/auth.go index ac40b79..4a6debf 100644 --- a/controller/auth.go +++ b/controller/auth.go @@ -5,11 +5,11 @@ import ( "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" - validation "github.com/go-ozzo/ozzo-validation" - "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/joomcode/errorx" "github.com/kataras/iris/v12" "github.com/kataras/jwt" @@ -31,54 +31,6 @@ type AuthController struct { Ctx iris.Context } -type LoginRequest struct { - Email string `json:"email"` - Password string `json:"password"` -} - -func (r LoginRequest) Validate() error { - return validation.ValidateStruct(&r, - validation.Field(&r.Email, is.EmailFormat, validation.Required), - validation.Field(&r.Password, validation.Required), - ) -} - -type LoginResponse struct { - Token string `json:"token"` -} - -type LogoutRequest struct { - Token string `json:"token"` -} - -type ChallengeRequest struct { - Pubkey string `json:"pubkey"` -} - -func (r ChallengeRequest) Validate() error { - return validation.ValidateStruct(&r, - validation.Field(&r.Pubkey, validation.Required, validation.By(CheckPubkeyValidator)), - ) -} - -type ChallengeResponse struct { - Challenge string `json:"challenge"` -} - -type PubkeyLoginRequest struct { - Pubkey string `json:"pubkey"` - Challenge string `json:"challenge"` - Signature string `json:"signature"` -} - -func (r PubkeyLoginRequest) Validate() error { - return validation.ValidateStruct(&r, - validation.Field(&r.Pubkey, validation.Required, validation.By(CheckPubkeyValidator)), - validation.Field(&r.Challenge, validation.Required), - validation.Field(&r.Signature, validation.Required, validation.Length(128, 128)), - ) -} - // 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)) @@ -166,12 +118,13 @@ func generateAndSaveChallengeToken(accountID uint, maxAge time.Duration) (string // PostLogin handles the POST /api/auth/login request to authenticate a user and return a JWT token. func (a *AuthController) PostLogin() { - var r LoginRequest - - if !tryParseRequest(r, a.Ctx) { + ri, success := tryParseRequest(request.LoginRequest{}, a.Ctx) + if !success { return } + 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 { @@ -205,7 +158,7 @@ func (a *AuthController) PostLogin() { } // Return the JWT token to the client. - err = a.Ctx.JSON(&LoginResponse{Token: token}) + err = a.Ctx.JSON(&response.LoginResponse{Token: token}) if err != nil { logger.Get().Error("failed to generate response", zap.Error(err)) } @@ -213,12 +166,13 @@ func (a *AuthController) PostLogin() { // PostChallenge handles the POST /api/auth/pubkey/challenge request to generate a challenge for a user's public key. func (a *AuthController) PostPubkeyChallenge() { - var r ChallengeRequest - - if !tryParseRequest(r, a.Ctx) { + ri, success := tryParseRequest(request.PubkeyChallengeRequest{}, a.Ctx) + if !success { return } + r, _ := (ri).(*request.PubkeyChallengeRequest) + r.Pubkey = strings.ToLower(r.Pubkey) // Retrieve the account for the given email. @@ -236,20 +190,21 @@ func (a *AuthController) PostPubkeyChallenge() { } // Return the challenge to the client. - err = a.Ctx.JSON(&ChallengeResponse{Challenge: challenge}) + err = a.Ctx.JSON(&response.ChallengeResponse{Challenge: challenge}) if err != nil { - panic(fmt.Errorf("Error with challenge request: %s \n", err)) + 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. func (a *AuthController) PostPubkeyLogin() { - var r PubkeyLoginRequest - - if !tryParseRequest(r, a.Ctx) { + ri, success := tryParseRequest(request.PubkeyLoginRequest{}, a.Ctx) + if !success { return } + r, _ := ri.(*request.PubkeyLoginRequest) + r.Pubkey = strings.ToLower(r.Pubkey) r.Signature = strings.ToLower(r.Signature) @@ -318,7 +273,7 @@ func (a *AuthController) PostPubkeyLogin() { } // Return the JWT token to the client. - err = a.Ctx.JSON(&LoginResponse{Token: token}) + err = a.Ctx.JSON(&response.LoginResponse{Token: token}) if err != nil { logger.Get().Error("failed to create response", zap.Error(err)) } @@ -327,12 +282,13 @@ func (a *AuthController) PostPubkeyLogin() { // PostLogout handles the POST /api/auth/logout request to invalidate a JWT token. func (a *AuthController) PostLogout() { - var r LogoutRequest - - if !tryParseRequest(r, a.Ctx) { + ri, success := tryParseRequest(request.LogoutRequest{}, a.Ctx) + if !success { return } + r, _ := ri.(*request.LogoutRequest) + // Verify the provided token. claims, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Token), blocklist) if err != nil { diff --git a/controller/controller.go b/controller/controller.go new file mode 100644 index 0000000..03b3532 --- /dev/null +++ b/controller/controller.go @@ -0,0 +1,40 @@ +package controller + +import ( + "git.lumeweb.com/LumeWeb/portal/controller/validators" + "git.lumeweb.com/LumeWeb/portal/logger" + "github.com/kataras/iris/v12" + "go.uber.org/zap" +) + +func tryParseRequest(r interface{}, ctx iris.Context) (interface{}, bool) { + v, ok := r.(validators.Validatable) + + if !ok { + return r, true + } + + var d map[string]interface{} + + // Read the logout request from the client. + if err := ctx.ReadJSON(&d); err != nil { + logger.Get().Debug("failed to parse request", zap.Error(err)) + ctx.StopWithError(iris.StatusBadRequest, err) + return nil, false + } + + data, err := v.Import(d) + if err != nil { + logger.Get().Debug("failed to parse request", zap.Error(err)) + ctx.StopWithError(iris.StatusBadRequest, err) + return nil, false + } + + if err := data.Validate(); err != nil { + logger.Get().Debug("failed to parse request", zap.Error(err)) + ctx.StopWithError(iris.StatusBadRequest, err) + return nil, false + } + + return data, true +} diff --git a/controller/request/login.go b/controller/request/login.go new file mode 100644 index 0000000..33681bc --- /dev/null +++ b/controller/request/login.go @@ -0,0 +1,23 @@ +package request + +import ( + "git.lumeweb.com/LumeWeb/portal/controller/validators" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" +) + +type LoginRequest struct { + validatable validators.ValidatableImpl + Email string `json:"email"` + Password string `json:"password"` +} + +func (r LoginRequest) Validate() error { + return validation.ValidateStruct(&r, + validation.Field(&r.Email, is.EmailFormat, validation.Required), + validation.Field(&r.Password, validation.Required), + ) +} +func (r LoginRequest) Import(d map[string]interface{}) (validators.Validatable, error) { + return r.validatable.Import(d, r) +} diff --git a/controller/request/logout.go b/controller/request/logout.go new file mode 100644 index 0000000..e40dd73 --- /dev/null +++ b/controller/request/logout.go @@ -0,0 +1,19 @@ +package request + +import ( + "git.lumeweb.com/LumeWeb/portal/controller/validators" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type LogoutRequest struct { + validatable validators.ValidatableImpl + Token string `json:"token"` +} + +func (r LogoutRequest) Validate() error { + return validation.ValidateStruct(&r, validation.Field(&r.Token, validation.Required)) +} + +func (r LogoutRequest) Import(d map[string]interface{}) (validators.Validatable, error) { + return r.validatable.Import(d, r) +} diff --git a/controller/request/pubkey_challenge.go b/controller/request/pubkey_challenge.go new file mode 100644 index 0000000..714feec --- /dev/null +++ b/controller/request/pubkey_challenge.go @@ -0,0 +1,21 @@ +package request + +import ( + "git.lumeweb.com/LumeWeb/portal/controller/validators" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type PubkeyChallengeRequest struct { + validatable validators.ValidatableImpl + Pubkey string `json:"pubkey"` +} + +func (r PubkeyChallengeRequest) Validate() error { + return validation.ValidateStruct(&r, + validation.Field(&r.Pubkey, validation.Required, validation.By(validators.CheckPubkeyValidator)), + ) +} + +func (r PubkeyChallengeRequest) Import(d map[string]interface{}) (validators.Validatable, error) { + return r.validatable.Import(d, r) +} diff --git a/controller/request/pubkey_login.go b/controller/request/pubkey_login.go new file mode 100644 index 0000000..c18e536 --- /dev/null +++ b/controller/request/pubkey_login.go @@ -0,0 +1,25 @@ +package request + +import ( + "git.lumeweb.com/LumeWeb/portal/controller/validators" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type PubkeyLoginRequest struct { + validatable validators.ValidatableImpl + Pubkey string `json:"pubkey"` + Challenge string `json:"challenge"` + Signature string `json:"signature"` +} + +func (r PubkeyLoginRequest) Validate() error { + return validation.ValidateStruct(&r, + validation.Field(&r.Pubkey, validation.Required, validation.By(validators.CheckPubkeyValidator)), + validation.Field(&r.Challenge, validation.Required), + validation.Field(&r.Signature, validation.Required, validation.Length(128, 128)), + ) +} + +func (r PubkeyLoginRequest) Import(d map[string]interface{}) (validators.Validatable, error) { + return r.validatable.Import(d, r) +} diff --git a/controller/request/register.go b/controller/request/register.go new file mode 100644 index 0000000..9e3fe4f --- /dev/null +++ b/controller/request/register.go @@ -0,0 +1,21 @@ +package request + +import ( + "git.lumeweb.com/LumeWeb/portal/controller/validators" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" +) + +type RegisterRequest struct { + Email string `json:"email"` + Password string `json:"password"` + Pubkey string `json:"pubkey"` +} + +func (r RegisterRequest) Validate() error { + return validation.ValidateStruct(&r, + validation.Field(&r.Email, validation.Required, is.EmailFormat), + validation.Field(&r.Pubkey, validation.When(len(r.Password) == 0, validation.Required, validation.By(validators.CheckPubkeyValidator))), + validation.Field(&r.Password, validation.When(len(r.Pubkey) == 0, validation.Required)), + ) +} diff --git a/controller/response/challenge.go b/controller/response/challenge.go new file mode 100644 index 0000000..109f467 --- /dev/null +++ b/controller/response/challenge.go @@ -0,0 +1,5 @@ +package response + +type ChallengeResponse struct { + Challenge string `json:"challenge"` +} diff --git a/controller/response/login.go b/controller/response/login.go new file mode 100644 index 0000000..6a2afb2 --- /dev/null +++ b/controller/response/login.go @@ -0,0 +1,5 @@ +package response + +type LoginResponse struct { + Token string `json:"token"` +} diff --git a/controller/validators/validators.go b/controller/validators/validators.go new file mode 100644 index 0000000..5af093f --- /dev/null +++ b/controller/validators/validators.go @@ -0,0 +1,43 @@ +package validators + +import ( + "crypto/ed25519" + "encoding/hex" + "errors" + "fmt" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/imdario/mergo" + "reflect" +) + +func CheckPubkeyValidator(value interface{}) error { + p, _ := value.(string) + pubkeyBytes, err := hex.DecodeString(p) + if err != nil { + return err + } + + if len(pubkeyBytes) != ed25519.PublicKeySize { + return errors.New(fmt.Sprintf("pubkey must be %d bytes in hexadecimal format", ed25519.PublicKeySize)) + } + + return nil +} + +type Validatable interface { + validation.Validatable + Import(d map[string]interface{}) (Validatable, error) +} + +type ValidatableImpl struct { +} + +func (v ValidatableImpl) Import(d map[string]interface{}, destType Validatable) (Validatable, error) { + instance := reflect.New(reflect.TypeOf(destType)).Interface().(Validatable) + // Perform the import logic + if err := mergo.Map(instance, d, mergo.WithOverride); err != nil { + return nil, err + } + + return instance, nil +} diff --git a/go.mod b/go.mod index e3fc0f1..26214f9 100644 --- a/go.mod +++ b/go.mod @@ -55,6 +55,7 @@ require ( github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/iris-contrib/go.uuid v2.0.0+incompatible // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/jinzhu/inflection v1.0.0 // indirect diff --git a/go.sum b/go.sum index ab0d42c..4a600d1 100644 --- a/go.sum +++ b/go.sum @@ -810,6 +810,8 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/iris-contrib/go.uuid v2.0.0+incompatible h1:XZubAYg61/JwnJNbZilGjf3b3pB80+OQg2qf6c8BfWE= github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= diff --git a/main.go b/main.go index c7cdfa6..99d5edb 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,6 @@ import ( "git.lumeweb.com/LumeWeb/portal/logger" "git.lumeweb.com/LumeWeb/portal/service/files" "git.lumeweb.com/LumeWeb/portal/tus" - validation "github.com/go-ozzo/ozzo-validation" "github.com/iris-contrib/swagger" "github.com/iris-contrib/swagger/swaggerFiles" "github.com/kataras/iris/v12" @@ -49,8 +48,6 @@ func main() { // Create a new Iris app instance app := iris.New() - - app.Validator = ozzValidator{} // Enable Gzip compression for responses app.Use(iris.Compression) @@ -105,15 +102,3 @@ func main() { logger.Get().Error("Failed starting webserver proof", zap.Error(err)) } } - -type ozzValidator struct{} - -func (o ozzValidator) Struct(d interface{}) error { - v, ok := d.(validation.Validatable) - - if !ok { - return nil - } - - return v.Validate() -}