110 lines
2.8 KiB
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})
|
|
}
|