Files

174 lines
5.7 KiB
Go

package plg_authenticate_htpasswd
import (
"crypto/sha1"
"crypto/subtle"
"encoding/base64"
"fmt"
"html"
"net/http"
"strings"
. "github.com/mickael-kerjean/filestash/server/common"
"github.com/mickael-kerjean/filestash/server/plugin/plg_authenticate_htpasswd/deps/crypt"
"github.com/mickael-kerjean/filestash/server/plugin/plg_authenticate_htpasswd/deps/crypt/apr1_crypt"
"github.com/mickael-kerjean/filestash/server/plugin/plg_authenticate_htpasswd/deps/crypt/md5_crypt"
"github.com/mickael-kerjean/filestash/server/plugin/plg_authenticate_htpasswd/deps/crypt/sha256_crypt"
"github.com/mickael-kerjean/filestash/server/plugin/plg_authenticate_htpasswd/deps/crypt/sha512_crypt"
"golang.org/x/crypto/bcrypt"
)
func init() {
Hooks.Register.AuthenticationMiddleware("htpasswd", Htpasswd{})
}
type Htpasswd struct{}
func (this Htpasswd) Setup() Form {
return Form{
Elmnts: []FormElement{
{
Name: "banner",
Type: "hidden",
Description: `Inspired by Apache, the htpasswd plugin uses the content of a .htpasswd file to authenticate users. It displays a username and password login page, verifying credentials against the provided htpasswd data.
The plugin exposes 2 variables: {{ .user }} and {{ .password }} which can be used in the attribute mapping section to create rules tailored to your specific use case. Examples of this can be found [in the documentation](https://www.filestash.app/docs/install-and-upgrade/#advanced-authentication---facade-pattern)`,
},
{
Name: "type",
Type: "hidden",
Value: "htpasswd",
},
{
Name: "users",
Type: "long_text",
Placeholder: `test1:$apr1$ZiAIyyhS$ovyMo9eJRgDF/luvmAigP0
test2:{SHA}EJ9LPFDXsN9ynSmbxvjp75Bmlx8=
test3:$6$ME6DxvSEUjW4Kx/j$vQ5Yh1utmNEr4EZnWH0ZQa6hrG5yu2siybFW10aAax4u611W9awI5V90YWqGs4NjTSHkCrhpdbJoNErW9/Pbh1:19306:0:99999:7:::
test4:$6$wTy86P73X/DsCiQy$El3JVUjepBUO.e.1OTuDt4yL9w2CnzY4jHaIbg1P7p508n8vjzCC8ZNsWa1IlbhciBM8.0LqqXWi3OuhGfPmP.
test5:$5$RkdUxGLHGhmrO0yj$K6bCqmB.OPR7KM4i5eiAG.mxFyhElLNdthSL.dreqN5
test6:$1$vuUKD.37$R6eCPFBa6lKIVfnkABveB1`,
Default: "",
Description: `The list of users who are granted access using either or both the htpasswd file format or the /etc/shadow file format. To generate a password:
'openssl passwd -6' or 'mkpasswd -m SHA-512' or the htpasswd cli tool.`,
},
},
}
}
func (this Htpasswd) 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>`, html.EscapeString(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="user" value="" placeholder="User" autocorrect="off" autocapitalize="off" />
</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 Htpasswd) Callback(formData map[string]string, idpParams map[string]string, res http.ResponseWriter) (map[string]string, error) {
if idpParams["users"] == "" {
Log.Error("plg_authenticate_htpasswd::callback there is no user configured")
return nil, NewError("You haven't configured any users", 500)
}
lines := strings.Split(idpParams["users"], "\n")
for n, line := range lines {
pair := strings.SplitN(line, ":", 2)
if len(pair) != 2 {
continue
} else if formData["user"] != pair[0] {
continue
} else if verifyPassword(
formData["password"],
strings.SplitN(pair[1], ":", 2)[0], // filter out unwanted fields from hash
formData["user"],
) == false {
continue
}
return map[string]string{
"user": formData["user"],
"password": formData["password"],
"n": fmt.Sprintf("%d", n),
}, nil
}
http.SetCookie(res, &http.Cookie{
Name: "flash",
Value: "Invalid username or password",
MaxAge: 1,
Path: "/",
})
return nil, ErrAuthenticationFailed
}
func verifyPassword(password string, hash string, _user string) bool {
if strings.HasPrefix(hash, "{SHA}") {
d := sha1.New()
d.Write([]byte(password))
return subtle.ConstantTimeCompare(
[]byte(strings.TrimPrefix(hash, "{SHA}")),
[]byte(base64.StdEncoding.EncodeToString(d.Sum(nil))),
) == 1
}
var c crypt.Crypter
parts := strings.SplitN(hash, "$", 4)
if len(parts) != 4 {
if password == hash {
Log.Warning("plg_authenticate_htpasswd password for user '%s' isn't stored in a secure way, you should hash your password using something like 'openssl passwd -6'", _user)
return true
} else {
return false
}
}
if strings.HasPrefix(hash, "$apr1$") {
c = apr1_crypt.New()
parts[2] = "$apr1$" + parts[2]
} else if strings.HasPrefix(hash, "$6$") {
c = sha512_crypt.New()
parts[2] = "$6$" + parts[2]
} else if strings.HasPrefix(hash, "$5$") {
c = sha256_crypt.New()
parts[2] = "$5$" + parts[2]
} else if strings.HasPrefix(hash, "$1$") {
c = md5_crypt.New()
parts[2] = "$1$" + parts[2]
} else if strings.HasPrefix(hash, "$2a$") {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
} else {
return false
}
shadow, err := c.Generate(
[]byte(password),
[]byte(parts[2]),
)
if err != nil {
return false
} else if shadow != hash {
return false
}
return true
}