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",
AllowHeaders: "Origin, Content-Type, Accept, Authorization, X-Telegram-Bot-Api-Secret-Token",
AllowMethods: "GET, POST, PUT, DELETE, OPTIONS",
AllowCredentials: true,
}))
loginLimiter := limiter.New(limiter.Config{

View File

@ -11,15 +11,27 @@ const UserIDKey = "user_id"
func Middleware(svc *Service) fiber.Handler {
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")
if h == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "missing Authorization header"})
}
if h != "" {
parts := strings.SplitN(h, " ", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid Authorization header"})
if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
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 {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid token"})
}

View File

@ -16,6 +16,21 @@ import (
"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 {
cfg *config.Config
users *users.Repo
@ -145,6 +160,8 @@ func (h *AuthHandler) Register(c *fiber.Ctx) error {
if err != nil {
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{
"user": fiber.Map{
@ -207,12 +224,8 @@ func (h *AuthHandler) Login(c *fiber.Ctx) error {
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token failed"})
}
uid := u.ID
_ = h.audit.Log(c.UserContext(), &audit.Event{
UserID: &uid, Action: "user.login",
IP: c.IP(), UserAgent: c.Get("User-Agent"),
})
// ставим access-token в HttpOnly cookie — браузер шлёт автоматически, без CORS preflight
setAuthCookie(c, tokens.Access, h.cfg.JwTAccessTTL())
return c.JSON(fiber.Map{
"user": u,