filebrowser/http/totp.go

110 lines
2.8 KiB
Go

package http
import (
"net/http"
"strings"
"time"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v4/request"
)
type totpUserInfo struct {
ID uint `json:"id"`
}
type totpAuthToken struct {
User totpUserInfo `json:"user"`
jwt.RegisteredClaims
}
type totpExtractor []string
func (e totpExtractor) ExtractToken(r *http.Request) (string, error) {
token, _ := request.HeaderExtractor{"X-TOTP-Auth"}.ExtractToken(r)
// Checks if the token isn't empty and if it contains two dots.
// The former prevents incompatibility with URLs that previously
// used basic auth.
if token != "" && strings.Count(token, ".") == 2 {
return token, nil
}
return "", request.ErrNoTokenInRequest
}
func verifyTOTPHandler(tokenExpireTime time.Duration) handleFunc {
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
code := r.Header.Get("X-TOTP-CODE")
if code == "" {
return http.StatusUnauthorized, nil
}
keyFunc := func(_ *jwt.Token) (interface{}, error) {
return d.settings.Key, nil
}
var tk totpAuthToken
token, err := request.ParseFromRequest(r, &totpExtractor{}, keyFunc, request.WithClaims(&tk))
if err != nil || !token.Valid {
return http.StatusUnauthorized, nil
}
d.user, err = d.store.Users.Get(d.server.Root, tk.User.ID)
if err != nil {
return http.StatusInternalServerError, err
}
if ok, err := users.CheckTOTP(d.server.TOTPEncryptionKey, d.user.TOTPSecret, d.user.TOTPNonce, code); err != nil {
return http.StatusInternalServerError, err
} else if !ok {
return http.StatusUnauthorized, nil
}
return printToken(w, r, d, d.user, tokenExpireTime)
}
}
func withTOTP(fn handleFunc) handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if d.user.TOTPSecret == "" {
return fn(w, r, d)
}
if code := r.Header.Get("X-TOTP-CODE"); code == "" {
return http.StatusForbidden, nil
} else {
if ok, err := users.CheckTOTP(d.server.TOTPEncryptionKey, d.user.TOTPSecret, d.user.TOTPNonce, code); err != nil {
return http.StatusInternalServerError, err
} else if !ok {
return http.StatusForbidden, nil
}
return fn(w, r, d)
}
})
}
func printTOTPToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User, tokenExpirationTime time.Duration) (int, error) {
claims := &totpAuthToken{
User: totpUserInfo{
ID: user.ID,
},
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(tokenExpirationTime)),
Issuer: "File Browser TOTP",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(d.settings.Key)
if err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, nil, loginResponse{Token: signed, OTP: true})
}