Files
tyto/backend/internal/auth/middleware.go
vikingowl 50c5811e22 feat: add authentication system with local and LDAP support
Auth Package (internal/auth/):
- Service: main auth orchestrator with multi-provider support
- LocalProvider: username/password auth with bcrypt hashing
- LDAPProvider: LDAP/Active Directory authentication with:
  - Service account bind for user search
  - User bind for password verification
  - Automatic user provisioning on first login
  - Group membership to role synchronization
- SessionManager: token-based session lifecycle
- Middleware: Gin middleware for route protection
- API: REST endpoints for login/logout/register

Security Features:
- bcrypt with cost factor 12 for password hashing
- Secure random 32-byte session tokens
- HTTP-only session cookies with SameSite=Lax
- Bearer token support for API clients
- Session expiration and cleanup
- Account disable with session invalidation

API Endpoints:
- POST /auth/login - Authenticate and get session
- POST /auth/logout - Invalidate current session
- POST /auth/logout/all - Invalidate all user sessions
- POST /auth/register - Create account (if enabled)
- GET /auth/me - Get current user info
- PUT /auth/me - Update profile
- PUT /auth/me/password - Change password

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 08:24:39 +01:00

146 lines
3.6 KiB
Go

package auth
import (
"net/http"
"strings"
"tyto/internal/database"
"github.com/gin-gonic/gin"
)
const (
// ContextKeyUser is the key for the authenticated user in gin.Context.
ContextKeyUser = "auth_user"
// ContextKeySession is the key for the session in gin.Context.
ContextKeySession = "auth_session"
)
// Middleware provides authentication middleware for Gin.
type Middleware struct {
auth *Service
}
// NewMiddleware creates a new authentication middleware.
func NewMiddleware(auth *Service) *Middleware {
return &Middleware{auth: auth}
}
// Required returns middleware that requires authentication.
// Requests without valid authentication are rejected with 401.
func (m *Middleware) Required() gin.HandlerFunc {
return func(c *gin.Context) {
session, user, err := m.authenticate(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "authentication required",
})
return
}
c.Set(ContextKeyUser, user)
c.Set(ContextKeySession, session)
c.Next()
}
}
// Optional returns middleware that attempts authentication but doesn't require it.
// Unauthenticated requests are allowed to proceed.
func (m *Middleware) Optional() gin.HandlerFunc {
return func(c *gin.Context) {
session, user, err := m.authenticate(c)
if err == nil {
c.Set(ContextKeyUser, user)
c.Set(ContextKeySession, session)
}
c.Next()
}
}
// authenticate extracts and validates the session token.
func (m *Middleware) authenticate(c *gin.Context) (*database.Session, *database.User, error) {
token := m.extractToken(c)
if token == "" {
return nil, nil, ErrInvalidToken
}
return m.auth.ValidateSession(c.Request.Context(), token)
}
// extractToken gets the session token from the request.
// Checks Authorization header first, then cookies.
func (m *Middleware) extractToken(c *gin.Context) string {
// Check Authorization header: "Bearer <token>"
auth := c.GetHeader("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
return strings.TrimPrefix(auth, "Bearer ")
}
// Check cookie
if cookie, err := c.Cookie(m.auth.config.SessionCookieName); err == nil {
return cookie
}
return ""
}
// GetUser returns the authenticated user from the context.
// Returns nil if not authenticated.
func GetUser(c *gin.Context) *database.User {
user, exists := c.Get(ContextKeyUser)
if !exists {
return nil
}
return user.(*database.User)
}
// GetSession returns the session from the context.
// Returns nil if not authenticated.
func GetSession(c *gin.Context) *database.Session {
session, exists := c.Get(ContextKeySession)
if !exists {
return nil
}
return session.(*database.Session)
}
// MustGetUser returns the authenticated user from the context.
// Panics if not authenticated (use after Required middleware).
func MustGetUser(c *gin.Context) *database.User {
user := GetUser(c)
if user == nil {
panic("auth: MustGetUser called without authentication")
}
return user
}
// SetSessionCookie sets the session cookie on the response.
func (m *Middleware) SetSessionCookie(c *gin.Context, session *database.Session) {
maxAge := int(m.auth.config.SessionDuration.Seconds())
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie(
m.auth.config.SessionCookieName,
session.Token,
maxAge,
m.auth.config.SessionCookiePath,
"", // domain (empty = current domain)
m.auth.config.SessionCookieHTTPS,
true, // httpOnly
)
}
// ClearSessionCookie removes the session cookie.
func (m *Middleware) ClearSessionCookie(c *gin.Context) {
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie(
m.auth.config.SessionCookieName,
"",
-1, // MaxAge -1 = delete
m.auth.config.SessionCookiePath,
"",
m.auth.config.SessionCookieHTTPS,
true,
)
}