Auth: HttpOnly cookie buhapp_at (без CORS preflight)

- Login/Register ставят cookie (HttpOnly, Secure, SameSite=Lax)
- Middleware читает cookie первым
- CORS: AllowCredentials=true
- Для прямых API-вызовов остался Authorization Bearer
- Legacy ?_t= query fallback
This commit is contained in:
ga 2026-08-20 21:50:28 +00:00
parent 249c3d2a8f
commit 5b70bc3137
3 changed files with 42 additions and 16 deletions

View File

@ -120,6 +120,7 @@ func main() {
AllowOrigins: "https://buhapp.mygoodservice.ru,https://t.me,https://web.telegram.org", AllowOrigins: "https://buhapp.mygoodservice.ru,https://t.me,https://web.telegram.org",
AllowHeaders: "Origin, Content-Type, Accept, Authorization, X-Telegram-Bot-Api-Secret-Token", AllowHeaders: "Origin, Content-Type, Accept, Authorization, X-Telegram-Bot-Api-Secret-Token",
AllowMethods: "GET, POST, PUT, DELETE, OPTIONS", AllowMethods: "GET, POST, PUT, DELETE, OPTIONS",
AllowCredentials: true,
})) }))
loginLimiter := limiter.New(limiter.Config{ loginLimiter := limiter.New(limiter.Config{

View File

@ -11,15 +11,27 @@ const UserIDKey = "user_id"
func Middleware(svc *Service) fiber.Handler { func Middleware(svc *Service) fiber.Handler {
return func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error {
var token string
// 1. HttpOnly cookie (основной способ — без CORS preflight)
token = c.Cookies("buhapp_at")
// 2. Authorization: Bearer <token> (для прямых API-вызовов)
if token == "" {
h := c.Get("Authorization") h := c.Get("Authorization")
if h == "" { if h != "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "missing Authorization header"})
}
parts := strings.SplitN(h, " ", 2) parts := strings.SplitN(h, " ", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid Authorization header"}) token = parts[1]
} }
claims, err := svc.Parse(c.UserContext(), parts[1]) }
}
// 3. Fallback — query param ?_t=<token> (legacy)
if token == "" {
token = c.Query("_t")
}
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "missing token"})
}
claims, err := svc.Parse(c.UserContext(), token)
if err != nil { if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid token"}) return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid token"})
} }

View File

@ -16,6 +16,21 @@ import (
"github.com/buhapp/backend/internal/users" "github.com/buhapp/backend/internal/users"
) )
const cookieName = "buhapp_at"
// setAuthCookie — кладём access-token в HttpOnly cookie (CORS-friendly, без preflight)
func setAuthCookie(c *fiber.Ctx, token string, ttl time.Duration) {
c.Cookie(&fiber.Cookie{
Name: cookieName,
Value: token,
Path: "/",
MaxAge: int(ttl.Seconds()),
HTTPOnly: true,
Secure: true,
SameSite: "Lax",
})
}
type AuthHandler struct { type AuthHandler struct {
cfg *config.Config cfg *config.Config
users *users.Repo users *users.Repo
@ -145,6 +160,8 @@ func (h *AuthHandler) Register(c *fiber.Ctx) error {
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token failed"}) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token failed"})
} }
// ставим access-token в HttpOnly cookie
setAuthCookie(c, tokens.Access, h.cfg.JwTAccessTTL())
return c.Status(fiber.StatusCreated).JSON(fiber.Map{ return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"user": fiber.Map{ "user": fiber.Map{
@ -207,12 +224,8 @@ func (h *AuthHandler) Login(c *fiber.Ctx) error {
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token failed"}) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token failed"})
} }
// ставим access-token в HttpOnly cookie — браузер шлёт автоматически, без CORS preflight
uid := u.ID setAuthCookie(c, tokens.Access, h.cfg.JwTAccessTTL())
_ = h.audit.Log(c.UserContext(), &audit.Event{
UserID: &uid, Action: "user.login",
IP: c.IP(), UserAgent: c.Get("User-Agent"),
})
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"user": u, "user": u,