feature (plg_authenticate_local): local source of user

This commit is contained in:
MickaelK 2024-12-06 18:28:33 +11:00
parent bc68bf50fe
commit 799ba868c3
9 changed files with 298 additions and 176 deletions

View file

@ -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).
<pre>
STATS:
TOTAL USERS ACTIVE USERS
| %.4d | %.4d
MANAGEMENT GUI: <a href="/admin/simple-user-management">/admin/simple-user-management</a>
EMAIL SERVER : %t
</pre>`, 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(`<p class="flash">%s</p>`, 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(`
<form action="`+WithBase("/api/session/auth/")+`" method="post" class="component_middleware">
{{ if eq .User.MFA "" }}
<style>
#init { padding: 20px 20px 10px 20px; text-align: center; background: rgba(0,0,0,0.1); border-radius: 10px; margin-top: -10vh; margin-bottom: 20px; }
#init input { background: transparent; margin-bottom: 0; text-align: center; }
</style>
<div id="init">
<img src="data:image/png;base64,{{ .QRCode }}" />
<input type="text" name="mfa" value="{{ .MFASecret }}" readonly />
</div>
{{ end }}
<label>
<input type="text" name="code" placeholder="code" />
</label>
<input type="hidden" name="session" value="{{ .Session }}" />
<button>SUBMIT</button>
`+getFlash()+`
<style>
form { padding-top: 10vh; }
</style>
</form>
`))).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(`
<form action="` + WithBase("/api/session/auth/") + `" method="post" class="component_middleware">
<label>
<input type="text" name="email" value="" placeholder="Email" />
</label>
<label>
<input type="password" name="password" value="" placeholder="Password" />
</label>
<button>CONNECT</button>
` + getFlash() + `
<style>
.flash{ color: #f26d6d; font-weight: bold; }
form { padding-top: 10vh; }
</style>
</form>`)))
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
}

View file

@ -1,4 +1,4 @@
package plg_authenticate_simple package plg_authenticate_local
import ( import (
"encoding/json" "encoding/json"
@ -8,7 +8,7 @@ import (
func getPluginData() (pluginConfig, error) { func getPluginData() (pluginConfig, error) {
var cfg pluginConfig 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") Log.Warning("plg_authenticate_simple::disable msg=middleware_is_not_enabled")
return cfg, ErrMissingDependency return cfg, ErrMissingDependency
} }
@ -30,7 +30,7 @@ func getPluginData() (pluginConfig, error) {
} }
func savePluginData(cfg 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") Log.Warning("plg_authenticate_simple::disable msg=middleware_is_not_enabled")
return ErrMissingDependency return ErrMissingDependency
} }

View file

@ -1,4 +1,4 @@
package plg_authenticate_simple package plg_authenticate_local
import ( import (
_ "embed" _ "embed"

View file

@ -1,4 +1,4 @@
package plg_authenticate_simple package plg_authenticate_local
import ( import (
"net/http" "net/http"
@ -9,22 +9,8 @@ import (
"github.com/gorilla/mux" "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() { func init() {
Hooks.Register.AuthenticationMiddleware("simple", SimpleAuth{}) Hooks.Register.AuthenticationMiddleware("local", SimpleAuth{})
Hooks.Register.HttpEndpoint(func(r *mux.Router, app *App) error { 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.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( r.HandleFunc("/admin/api/simple-user-management", middleware.NewMiddlewareChain(
@ -35,3 +21,21 @@ func init() {
return nil 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"`
}

View file

@ -1,4 +1,4 @@
package plg_authenticate_simple package plg_authenticate_local
import ( import (
"bytes" "bytes"

View file

@ -1,4 +1,4 @@
package plg_authenticate_simple package plg_authenticate_local
import ( import (
"sort" "sort"
@ -53,7 +53,8 @@ func updateUser(user User) error {
} }
user.Password = string(p) user.Password = string(p)
} }
users[i] = user users[i].Disabled = user.Disabled
users[i].Role = user.Role
return saveUsers(users) return saveUsers(users)
} }
} }

View file

@ -1,4 +1,4 @@
package plg_authenticate_simple package plg_authenticate_local
import ( import (
"strings" "strings"

View file

@ -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).
<pre>
STATS:
TOTAL USERS ACTIVE USERS
| %.4d | %.4d
MANAGEMENT GUI: <a href="/admin/simple-user-management">/admin/simple-user-management</a>
EMAIL SERVER : %t
</pre>`, 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(`<p class="flash">%s</p>`, c.Value)
}
res.Header().Set("Content-Type", "text/html; charset=utf-8")
res.WriteHeader(http.StatusOK)
res.Write([]byte(Page(`
<form action="` + WithBase("/api/session/auth/") + `" method="post" class="component_middleware">
<label>
<input type="text" name="email" value="" placeholder="Email" />
</label>
<label>
<input type="password" name="password" value="" placeholder="Password" />
</label>
<button>CONNECT</button>
` + getFlash() + `
<style>
.flash{ color: #f26d6d; font-weight: bold; }
form { padding-top: 10vh; }
</style>
</form>`)))
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
}