diff --git a/server/plugin/plg_authenticate_local/auth.go b/server/plugin/plg_authenticate_local/auth.go new file mode 100644 index 00000000..f4cca734 --- /dev/null +++ b/server/plugin/plg_authenticate_local/auth.go @@ -0,0 +1,269 @@ +package plg_authenticate_local + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "image/png" + "net/http" + "text/template" + + . "github.com/mickael-kerjean/filestash/server/common" + + "github.com/pquerna/otp/totp" + "golang.org/x/crypto/bcrypt" +) + +type SimpleAuth struct{} + +func (this SimpleAuth) Setup() Form { + nUsers := 0 + aUsers := 0 + if users, err := getUsers(); err == nil { + nUsers = len(users) + for i := range users { + if users[i].Disabled == false { + aUsers += 1 + } + } + } + + return Form{ + Elmnts: []FormElement{ + { + Name: "banner", + Type: "hidden", + Description: fmt.Sprintf(`Manage your team members and their account permissions by visiting [/admin/simple-user-management](/admin/simple-user-management). +
+STATS:
+┌─────────────┐   ┌──────────────┐
+│ TOTAL USERS │   │ ACTIVE USERS │
+|    %.4d     │   |     %.4d     │
+└─────────────┘   └──────────────┘
+
+MANAGEMENT GUI: /admin/simple-user-management
+EMAIL SERVER  : %t
+
`, nUsers, aUsers, isEmailSetup()), + }, + { + Name: "type", + Type: "hidden", + Value: "local", + }, + { + Name: "mfa", + Type: "select", + Default: "", + Opts: []string{"", "TOTP"}, + }, + { + Name: "notification_subject", + Type: "text", + }, + { + Name: "notification_body", + Type: "long_text", + Placeholder: `Hello, + +Your account to Filestash was created by an administrator. You can access +it via http://demo.filestash.app. + +Your password is: {{ .password }} +The roles assigned to you: {{ .role }} + +Cheers!`, + }, + { + Name: "db", + Type: "hidden", + }, + }, + } +} + +func (this SimpleAuth) EntryPoint(idpParams map[string]string, req *http.Request, res http.ResponseWriter) error { + getFlash := func() string { + c, err := req.Cookie("flash") + if err != nil { + return "" + } + http.SetCookie(res, &http.Cookie{ + Name: "flash", + MaxAge: -1, + Path: "/", + }) + return fmt.Sprintf(`

%s

`, c.Value) + } + res.Header().Set("Content-Type", "text/html; charset=utf-8") + res.WriteHeader(http.StatusOK) + if c, err := req.Cookie("mfa"); err == nil && c.Value != "" { + user := withMFA(User{}, c.Value) + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: Config.Get("general.name").String(), + AccountName: user.Email, + }) + if err != nil { + return err + } + var buf bytes.Buffer + img, err := key.Image(200, 200) + if err != nil { + return err + } + if err = png.Encode(&buf, img); err != nil { + return err + } + template.Must(template.New("app").Parse(Page(` +
+ {{ if eq .User.MFA "" }} + +
+ + +
+ {{ end }} + + + + `+getFlash()+` + +
+ `))).Execute(res, struct { + User User + Session string + MFASecret string + QRCode string + }{ + User: user, + Session: c.Value, + MFASecret: key.Secret(), + QRCode: base64.StdEncoding.EncodeToString(buf.Bytes()), + }) + return nil + } + res.Write([]byte(Page(` +
+ + + + ` + getFlash() + ` + +
`))) + return nil +} + +func (this SimpleAuth) Callback(formData map[string]string, idpParams map[string]string, res http.ResponseWriter) (map[string]string, error) { + users, err := getUsers() + if err != nil { + return nil, err + } + requestedUser := withMFA(User{ + Email: formData["email"], + Password: formData["password"], + }, formData["session"]) + requestedUser.Code = formData["code"] + for i := range users { + if users[i].Email != requestedUser.Email { + continue + } + if err = bcrypt.CompareHashAndPassword([]byte(users[i].Password), []byte(requestedUser.Password)); err != nil { + break + } + if users[i].Disabled == true { + http.SetCookie(res, &http.Cookie{ + Name: "flash", + Value: "Account is disabled", + MaxAge: 1, + Path: "/", + }) + Log.Warning("plg_authentication_simple::auth action=authenticate email=%s err=disabled", users[i].Email) + return nil, ErrAuthenticationFailed + } + if idpParams["mfa"] == "TOTP" { + shouldSaveMFAKey := false + if users[i].MFA == "" { + users[i].MFA = formData["mfa"] + shouldSaveMFAKey = true + } + if totp.Validate(requestedUser.Code, users[i].MFA) == false { + requestedUser.MFA = users[i].MFA + http.SetCookie(res, &http.Cookie{ + Name: "mfa", + Value: requestedUser.EncryptedString(), + MaxAge: 1, + }) + return nil, ErrAuthenticationFailed + } + if shouldSaveMFAKey { + saveUsers(users) + } + } + session := map[string]string{ + "user": requestedUser.Email, + "password": requestedUser.Password, + "bcrypt": users[i].Password, + "role": users[i].Role, + } + s := "" + for k, v := range session { + if k == "password" || k == "bcrypt" { + v = "*****" + } + s += fmt.Sprintf("%s[%s] ", k, v) + } + Log.Debug("IDP Attributes => %s", s) + return session, nil + } + + http.SetCookie(res, &http.Cookie{ + Name: "flash", + Value: "Invalid username or password", + MaxAge: 1, + Path: "/", + }) + return nil, ErrAuthenticationFailed +} + +func withMFA(user User, session string) User { + if session == "" { + return user + } + data, err := DecryptString(SECRET_KEY_DERIVATE_FOR_USER, session) + if err != nil { + return User{} + } + var u User + if err = json.Unmarshal([]byte(data), &u); err != nil { + return User{} + } + user.Email = u.Email + user.Password = u.Password + return u +} + +func (user User) EncryptedString() string { + b, err := json.Marshal(user) + if err != nil { + return "" + } + d, err := EncryptString(SECRET_KEY_DERIVATE_FOR_USER, string(b)) + if err != nil { + return "" + } + return d +} diff --git a/server/plugin/plg_authenticate_simple/data.go b/server/plugin/plg_authenticate_local/data.go similarity index 95% rename from server/plugin/plg_authenticate_simple/data.go rename to server/plugin/plg_authenticate_local/data.go index 90ef29f1..b0d16776 100644 --- a/server/plugin/plg_authenticate_simple/data.go +++ b/server/plugin/plg_authenticate_local/data.go @@ -1,4 +1,4 @@ -package plg_authenticate_simple +package plg_authenticate_local import ( "encoding/json" @@ -8,7 +8,7 @@ import ( func getPluginData() (pluginConfig, error) { var cfg pluginConfig - if Config.Get("middleware.identity_provider.type").String() != "simple" { + if Config.Get("middleware.identity_provider.type").String() != "local" { Log.Warning("plg_authenticate_simple::disable msg=middleware_is_not_enabled") return cfg, ErrMissingDependency } @@ -30,7 +30,7 @@ func getPluginData() (pluginConfig, error) { } func savePluginData(cfg pluginConfig) error { - if Config.Get("middleware.identity_provider.type").String() != "simple" { + if Config.Get("middleware.identity_provider.type").String() != "local" { Log.Warning("plg_authenticate_simple::disable msg=middleware_is_not_enabled") return ErrMissingDependency } diff --git a/server/plugin/plg_authenticate_simple/handler.go b/server/plugin/plg_authenticate_local/handler.go similarity index 98% rename from server/plugin/plg_authenticate_simple/handler.go rename to server/plugin/plg_authenticate_local/handler.go index be9d1430..a0399dda 100644 --- a/server/plugin/plg_authenticate_simple/handler.go +++ b/server/plugin/plg_authenticate_local/handler.go @@ -1,4 +1,4 @@ -package plg_authenticate_simple +package plg_authenticate_local import ( _ "embed" diff --git a/server/plugin/plg_authenticate_simple/handler.html b/server/plugin/plg_authenticate_local/handler.html similarity index 100% rename from server/plugin/plg_authenticate_simple/handler.html rename to server/plugin/plg_authenticate_local/handler.html diff --git a/server/plugin/plg_authenticate_simple/index.go b/server/plugin/plg_authenticate_local/index.go similarity index 84% rename from server/plugin/plg_authenticate_simple/index.go rename to server/plugin/plg_authenticate_local/index.go index ab868728..8d7704f7 100644 --- a/server/plugin/plg_authenticate_simple/index.go +++ b/server/plugin/plg_authenticate_local/index.go @@ -1,4 +1,4 @@ -package plg_authenticate_simple +package plg_authenticate_local import ( "net/http" @@ -9,22 +9,8 @@ import ( "github.com/gorilla/mux" ) -type User struct { - Email string `json:"email"` - Password string `json:"password"` - Role string `json:"role"` - Disabled bool `json:"disabled"` -} - -type pluginConfig struct { - DB string `json:"db"` - Users []User `json:"-"` - Subject string `json:"notification_subject"` - Body string `json:"notification_body"` -} - func init() { - Hooks.Register.AuthenticationMiddleware("simple", SimpleAuth{}) + Hooks.Register.AuthenticationMiddleware("local", SimpleAuth{}) Hooks.Register.HttpEndpoint(func(r *mux.Router, app *App) error { r.Handle("/admin/simple-user-management", http.RedirectHandler("/admin/api/simple-user-management", http.StatusSeeOther)).Methods("GET") r.HandleFunc("/admin/api/simple-user-management", middleware.NewMiddlewareChain( @@ -35,3 +21,21 @@ func init() { return nil }) } + +type User struct { + Email string `json:"email"` + Password string `json:"password"` + Role string `json:"role"` + Disabled bool `json:"disabled"` + + Code string `json:"-"` + MFA string `json:"mfa"` +} + +type pluginConfig struct { + DB string `json:"db"` + MFA string `json:"mfa"` + Users []User `json:"-"` + Subject string `json:"notification_subject"` + Body string `json:"notification_body"` +} diff --git a/server/plugin/plg_authenticate_simple/notify.go b/server/plugin/plg_authenticate_local/notify.go similarity index 98% rename from server/plugin/plg_authenticate_simple/notify.go rename to server/plugin/plg_authenticate_local/notify.go index 6f6723ee..f098bb95 100644 --- a/server/plugin/plg_authenticate_simple/notify.go +++ b/server/plugin/plg_authenticate_local/notify.go @@ -1,4 +1,4 @@ -package plg_authenticate_simple +package plg_authenticate_local import ( "bytes" diff --git a/server/plugin/plg_authenticate_simple/service.go b/server/plugin/plg_authenticate_local/service.go similarity index 94% rename from server/plugin/plg_authenticate_simple/service.go rename to server/plugin/plg_authenticate_local/service.go index a40d231a..f26a7d38 100644 --- a/server/plugin/plg_authenticate_simple/service.go +++ b/server/plugin/plg_authenticate_local/service.go @@ -1,4 +1,4 @@ -package plg_authenticate_simple +package plg_authenticate_local import ( "sort" @@ -53,7 +53,8 @@ func updateUser(user User) error { } user.Password = string(p) } - users[i] = user + users[i].Disabled = user.Disabled + users[i].Role = user.Role return saveUsers(users) } } diff --git a/server/plugin/plg_authenticate_simple/utils.go b/server/plugin/plg_authenticate_local/utils.go similarity index 92% rename from server/plugin/plg_authenticate_simple/utils.go rename to server/plugin/plg_authenticate_local/utils.go index e0d1cbf3..b8c902df 100644 --- a/server/plugin/plg_authenticate_simple/utils.go +++ b/server/plugin/plg_authenticate_local/utils.go @@ -1,4 +1,4 @@ -package plg_authenticate_simple +package plg_authenticate_local import ( "strings" diff --git a/server/plugin/plg_authenticate_simple/auth.go b/server/plugin/plg_authenticate_simple/auth.go deleted file mode 100644 index e26f6f81..00000000 --- a/server/plugin/plg_authenticate_simple/auth.go +++ /dev/null @@ -1,152 +0,0 @@ -package plg_authenticate_simple - -import ( - "fmt" - "net/http" - - . "github.com/mickael-kerjean/filestash/server/common" - - "golang.org/x/crypto/bcrypt" -) - -type SimpleAuth struct{} - -func (this SimpleAuth) Setup() Form { - nUsers := 0 - aUsers := 0 - if users, err := getUsers(); err == nil { - nUsers = len(users) - for i := range users { - if users[i].Disabled == false { - aUsers += 1 - } - } - } - - return Form{ - Elmnts: []FormElement{ - { - Name: "banner", - Type: "hidden", - Description: fmt.Sprintf(`Manage your team members and their account permissions by visiting [/admin/simple-user-management](/admin/simple-user-management). -
-STATS:
-┌─────────────┐   ┌──────────────┐
-│ TOTAL USERS │   │ ACTIVE USERS │
-|    %.4d     │   |     %.4d     │
-└─────────────┘   └──────────────┘
-
-MANAGEMENT GUI: /admin/simple-user-management
-EMAIL SERVER  : %t
-
`, nUsers, aUsers, isEmailSetup()), - }, - { - Name: "type", - Type: "hidden", - Value: "simple", - }, - { - Name: "notification_subject", - Type: "text", - }, - { - Name: "notification_body", - Type: "long_text", - Placeholder: `Hello, - -Your account to Filestash was created by an administrator. You can access -it via http://demo.filestash.app. - -Your password is: {{ .password }} -The roles assigned to you: {{ .role }} - -Cheers!`, - }, - { - Name: "db", - Type: "hidden", - }, - }, - } -} - -func (this SimpleAuth) EntryPoint(idpParams map[string]string, req *http.Request, res http.ResponseWriter) error { - getFlash := func() string { - c, err := req.Cookie("flash") - if err != nil { - return "" - } - http.SetCookie(res, &http.Cookie{ - Name: "flash", - MaxAge: -1, - Path: "/", - }) - return fmt.Sprintf(`

%s

`, c.Value) - } - res.Header().Set("Content-Type", "text/html; charset=utf-8") - res.WriteHeader(http.StatusOK) - res.Write([]byte(Page(` -
- - - - ` + getFlash() + ` - -
`))) - return nil -} - -func (this SimpleAuth) Callback(formData map[string]string, idpParams map[string]string, res http.ResponseWriter) (map[string]string, error) { - users, err := getUsers() - if err != nil { - return nil, err - } - for i := range users { - if users[i].Email != formData["email"] { - continue - } - if err = bcrypt.CompareHashAndPassword([]byte(users[i].Password), []byte(formData["password"])); err != nil { - break - } - if users[i].Disabled == true { - http.SetCookie(res, &http.Cookie{ - Name: "flash", - Value: "Account is disabled", - MaxAge: 1, - Path: "/", - }) - Log.Warning("plg_authentication_simple::auth action=authenticate email=%s err=disabled", users[i].Email) - return nil, ErrAuthenticationFailed - } - session := map[string]string{ - "user": formData["email"], - "password": formData["password"], - "bcrypt": users[i].Password, - "role": users[i].Role, - } - s := "" - for k, v := range session { - if k == "password" || k == "bcrypt" { - v = "*****" - } - s += fmt.Sprintf("%s[%s] ", k, v) - } - Log.Debug("IDP Attributes => %s", s) - return session, nil - } - - http.SetCookie(res, &http.Cookie{ - Name: "flash", - Value: "Inalid username or password", - MaxAge: 1, - Path: "/", - }) - return nil, ErrAuthenticationFailed -}