filebrowser/backend/http/auth.go

248 lines
6.8 KiB
Go

package http
import (
"encoding/json"
libError "errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v4/request"
"golang.org/x/crypto/bcrypt"
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/share"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/utils"
)
var (
revokedApiKeyList map[string]bool
revokeMu sync.Mutex
)
// first checks for cookie
// then checks for header Authorization as Bearer token
// then checks for query parameter
func extractToken(r *http.Request) (string, error) {
hasToken := false
tokenObj, err := r.Cookie("auth")
if err == nil {
hasToken = true
token := tokenObj.Value
// 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
}
}
auth := r.URL.Query().Get("auth")
if auth != "" {
hasToken = true
if strings.Count(auth, ".") == 2 {
return auth, nil
}
}
// Check for Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader != "" {
hasToken = true
// Split the header to get "Bearer {token}"
parts := strings.Split(authHeader, " ")
if len(parts) == 2 && parts[0] == "Bearer" {
token := parts[1]
return token, nil
}
}
if hasToken {
return "", fmt.Errorf("invalid token provided")
}
return "", request.ErrNoTokenInRequest
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
if !config.Auth.Methods.PasswordAuth.Enabled {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
// currently only supports user/pass
// Get the authentication method from the settings
auther, err := store.Auth.Get("password")
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// Authenticate the user based on the request
user, err := auther.Auth(r, store.Users)
if err == os.ErrPermission {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
} else if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
status, err := printToken(w, r, user) // Pass the data object
if err != nil {
http.Error(w, http.StatusText(status), status)
}
}
type signupBody struct {
Username string `json:"username"`
Password string `json:"password"`
}
func signupHandler(w http.ResponseWriter, r *http.Request) {
if !settings.Config.Auth.Signup {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
if r.Body == nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
info := &signupBody{}
err := json.NewDecoder(r.Body).Decode(info)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
if info.Password == "" || info.Username == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
user := settings.ApplyUserDefaults(users.User{})
user.Username = info.Username
user.Password = info.Password
userHome, err := config.MakeUserDir(user.Username, user.Scope, files.RootPaths["default"])
if err != nil {
logger.Error(fmt.Sprintf("create user: failed to mkdir user home dir: [%s]", userHome))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
user.Scope = userHome
logger.Debug(fmt.Sprintf("new user: %s, home dir: [%s].", user.Username, userHome))
err = store.Users.Save(&user)
if err == errors.ErrExist {
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
return
} else if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
func renewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// check if x-auth header is present and token is
return printToken(w, r, d.user)
}
func printToken(w http.ResponseWriter, _ *http.Request, user *users.User) (int, error) {
signed, err := makeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), time.Hour*time.Duration(config.Auth.TokenExpirationHours), user.Perm)
if err != nil {
if strings.Contains(err.Error(), "key already exists with same name") {
return http.StatusConflict, err
}
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "text/plain")
if _, err := w.Write([]byte(signed.Key)); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
func isRevokedApiKey(key string) bool {
_, exists := revokedApiKeyList[key]
return exists
}
func revokeAPIKey(key string) {
revokeMu.Lock()
delete(revokedApiKeyList, key)
revokeMu.Unlock()
}
func makeSignedTokenAPI(user *users.User, name string, duration time.Duration, perms users.Permissions) (users.AuthToken, error) {
_, ok := user.ApiKeys[name]
if ok {
return users.AuthToken{}, fmt.Errorf("key already exists with same name %v ", name)
}
now := time.Now()
expires := now.Add(duration)
claim := users.AuthToken{
Permissions: perms,
Created: now.Unix(),
Expires: expires.Unix(),
Name: name,
BelongsTo: user.ID,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(expires),
Issuer: "FileBrowser Quantum",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
tokenString, err := token.SignedString(config.Auth.Key)
if err != nil {
return claim, err
}
claim.Key = tokenString
if strings.HasPrefix(name, "WEB_TOKEN") {
// don't add to api tokens, its a short lived web token
return claim, err
}
// Perform the user update
err = store.Users.AddApiKey(user.ID, name, claim)
if err != nil {
return claim, err
}
return claim, err
}
func authenticateShareRequest(r *http.Request, l *share.Link) (int, error) {
if l.PasswordHash == "" {
return 200, nil
}
if r.URL.Query().Get("token") == l.Token {
return 200, nil
}
password := r.Header.Get("X-SHARE-PASSWORD")
password, err := url.QueryUnescape(password)
if err != nil {
return http.StatusUnauthorized, err
}
if password == "" {
return http.StatusUnauthorized, nil
}
if err := bcrypt.CompareHashAndPassword([]byte(l.PasswordHash), []byte(password)); err != nil {
if libError.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return http.StatusUnauthorized, nil
}
return 401, err
}
return 200, nil
}