Files
drive/apps/backend/internal/auth/middleware.go
Kenneth f4620dff3a fix(backend): optional auth for share routes
Add auth.NewOptionalAuthMiddleware to run auth only when credentials are
present (Authorization header or auth cookies). Use it on share
consumption routes so public shares remain accessible unauthenticated,
while authenticated callers can resolve account-scoped shares. This
prevents a panic in share middleware when accountId was provided but the
request wasn’t authenticated (nil reqctx.AuthenticatedUser type
assertion).
2025-12-29 00:07:44 +00:00

127 lines
3.8 KiB
Go

package auth
import (
"errors"
"log/slog"
"strings"
"time"
"github.com/get-drexa/drexa/internal/httperr"
"github.com/get-drexa/drexa/internal/reqctx"
"github.com/get-drexa/drexa/internal/user"
"github.com/gofiber/fiber/v2"
"github.com/uptrace/bun"
)
// NewAuthMiddleware creates a middleware that authenticates requests via Bearer token or cookies.
// To obtain the authenticated user in subsequent handlers, see reqctx.AuthenticatedUser.
func NewAuthMiddleware(s *Service, db *bun.DB, cookieConfig CookieConfig) fiber.Handler {
return func(c *fiber.Ctx) error {
var at string
var rt string
var setCookies bool
authHeader := c.Get("Authorization")
if authHeader != "" {
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
slog.Info("invalid auth header")
return c.SendStatus(fiber.StatusUnauthorized)
}
at = parts[1]
setCookies = false
} else {
cookies := authCookies(c)
at = cookies[cookieKeyAccessToken]
rt = cookies[cookieKeyRefreshToken]
setCookies = true
}
if at == "" && rt == "" {
slog.Info("no access token or refresh token")
return c.SendStatus(fiber.StatusUnauthorized)
}
if at == "" {
// if there is no access token, attempt to get new access token using the refresh token.
tx, err := db.BeginTx(c.Context(), nil)
if err != nil {
return c.SendStatus(fiber.StatusUnauthorized)
}
defer tx.Rollback()
newTokens, err := s.RefreshAccessToken(c.Context(), tx, rt)
if err != nil {
return c.SendStatus(fiber.StatusUnauthorized)
}
if err := tx.Commit(); err != nil {
return c.SendStatus(fiber.StatusUnauthorized)
}
SetAuthCookies(c, newTokens.AccessToken, newTokens.RefreshToken, cookieConfig)
at = newTokens.AccessToken
rt = newTokens.RefreshToken
}
authResult, err := s.AuthenticateWithAccessToken(c.Context(), db, at)
if err != nil {
var e *InvalidAccessTokenError
if errors.As(err, &e) {
slog.Info("invalid access token")
return c.SendStatus(fiber.StatusUnauthorized)
}
var nf *user.NotFoundError
if errors.As(err, &nf) {
slog.Info("user not found")
return c.SendStatus(fiber.StatusUnauthorized)
}
return httperr.Internal(err)
}
reqctx.SetAuthenticatedUser(c, authResult.User)
// if cookie based auth and access token is about to expire (within 5 minutes),
// attempt to refresh the access token. if there is any error, ignore it and let the request continue.
if setCookies && time.Until(authResult.Claims.ExpiresAt.Time) < 5*time.Minute && rt != "" {
tx, txErr := db.BeginTx(c.Context(), nil)
if txErr == nil {
newTokens, err := s.RefreshAccessToken(c.Context(), tx, rt)
if err == nil {
if commitErr := tx.Commit(); commitErr == nil {
SetAuthCookies(c, newTokens.AccessToken, newTokens.RefreshToken, cookieConfig)
}
} else {
_ = tx.Rollback()
slog.Debug("auto-refresh failed", "error", err)
}
}
}
return c.Next()
}
}
// NewOptionalAuthMiddleware conditionally runs the given auth middleware only when
// the request contains auth credentials (Authorization header or auth cookies).
//
// This is useful for endpoints that are publicly accessible but can also behave
// differently for authenticated callers (e.g. share consumption routes that may
// resolve additional permissions for a specific account when credentials are
// present).
//
// Usage:
// authMiddleware := auth.NewAuthMiddleware(...)
// router.Use(auth.NewOptionalAuthMiddleware(authMiddleware))
func NewOptionalAuthMiddleware(authMiddleware fiber.Handler) fiber.Handler {
return func(c *fiber.Ctx) error {
hasAuthHeader := c.Get("Authorization") != ""
hasAuthCookies := c.Cookies(cookieKeyAccessToken) != "" || c.Cookies(cookieKeyRefreshToken) != ""
if !hasAuthHeader && !hasAuthCookies {
return c.Next()
}
return authMiddleware(c)
}
}