jwt in headers+localstorage instead of httpOnly cookie (#117)

This commit is contained in:
Marco
2023-12-27 14:32:08 +01:00
committed by GitHub
parent f7ba203ed0
commit c5535fad71
14 changed files with 125 additions and 155 deletions

View File

@@ -30,7 +30,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
)
if username != req.Username || password != req.Password {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, "invalid username or password", http.StatusBadRequest)
return
}
@@ -47,18 +47,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
return
}
cookie := &http.Cookie{
Name: utils.TOKEN_COOKIE_NAME,
HttpOnly: true,
Secure: false,
Expires: expiresAt, // 30 days
Value: tokenString,
Path: "/",
}
http.SetCookie(w, cookie)
if err := json.NewEncoder(w).Encode("ok"); err != nil {
if err := json.NewEncoder(w).Encode(tokenString); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View File

@@ -1,56 +1,63 @@
package middlewares
import (
"errors"
"fmt"
"net/http"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/utils"
)
func validateToken(tokenValue string) error {
if tokenValue == "" {
return errors.New("invalid token")
}
token, _ := jwt.Parse(tokenValue, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
expiresAt, err := time.Parse(time.RFC3339, claims["expiresAt"].(string))
if err != nil {
return err
}
if time.Now().After(expiresAt) {
return errors.New("token expired")
}
} else {
return errors.New("invalid token")
}
return nil
}
func Authenticated(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !config.Instance().RequireAuth {
next.ServeHTTP(w, r)
return
}
token := r.Header.Get("X-Authentication")
cookie, err := r.Cookie(utils.TOKEN_COOKIE_NAME)
if err != nil {
http.Error(w, "invalid token", http.StatusBadRequest)
return
}
if cookie == nil {
http.Error(w, "invalid token", http.StatusBadRequest)
return
}
token, _ := jwt.Parse(cookie.Value, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
expiresAt, err := time.Parse(time.RFC3339, claims["expiresAt"].(string))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if time.Now().After(expiresAt) {
http.Error(w, "token expired", http.StatusBadRequest)
return
}
} else {
http.Error(w, "invalid token", http.StatusBadRequest)
if err := validateToken(token); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
next.ServeHTTP(w, r)
})
}
func WebSocketAuthentication(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if err := validateToken(token); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

View File

@@ -4,6 +4,7 @@ import (
"database/sql"
"github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
)
@@ -20,7 +21,9 @@ func ApplyRouter(db *sql.DB, mdb *internal.MemoryDB, mq *internal.MessageQueue)
h := Container(db, mdb, mq)
return func(r chi.Router) {
r.Use(middlewares.Authenticated)
if config.Instance().RequireAuth {
r.Use(middlewares.Authenticated)
}
r.Post("/exec", h.Exec())
r.Get("/running", h.Running())
r.Post("/cookies", h.SetCookies())

View File

@@ -2,6 +2,7 @@ package rpc
import (
"github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
)
@@ -17,8 +18,18 @@ func Container(db *internal.MemoryDB, mq *internal.MessageQueue) *Service {
// RPC service must be registered before applying this router!
func ApplyRouter() func(chi.Router) {
return func(r chi.Router) {
r.Use(middlewares.Authenticated)
r.Get("/ws", WebSocket)
r.Post("/http", Post)
r.Route("/ws", func(r chi.Router) {
if config.Instance().RequireAuth {
r.Use(middlewares.WebSocketAuthentication)
}
r.Get("/", WebSocket)
})
r.Route("/http", func(r chi.Router) {
if config.Instance().RequireAuth {
r.Use(middlewares.Authenticated)
}
r.Post("/", Post)
})
}
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/dbutils"
"github.com/marcopeocchi/yt-dlp-web-ui/server/handlers"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
@@ -96,7 +97,9 @@ func newServer(c serverConfig) *http.Server {
// Archive routes
r.Route("/archive", func(r chi.Router) {
r.Use(middlewares.Authenticated)
if config.Instance().RequireAuth {
r.Use(middlewares.Authenticated)
}
r.Post("/downloaded", handlers.ListDownloaded)
r.Post("/delete", handlers.DeleteFile)
r.Get("/d/{id}", handlers.SendFile)