package auth import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "errors" "os" "time" "github.com/golang-jwt/jwt/v5" ) var ( ErrInvalidCredentials = errors.New("invalid username or password") ErrInvalidToken = errors.New("invalid or expired token") ) type User struct { Username string `json:"username"` Password string `json:"password"` // plaintext only used at login } // defaultAdmin returns the admin user from env vars func defaultAdmin() User { username := os.Getenv("ADMIN_USER") if username == "" { username = "admin" } password := os.Getenv("ADMIN_PASSWORD") if password == "" { password = "admin123" } return User{Username: username, Password: password} } // LoginRequest is the JSON body for login type LoginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } // LoginResponse is the JSON response for login type LoginResponse struct { Token string `json:"token"` Username string `json:"username"` ExpiresAt int64 `json:"expires_at"` } // Claims represents JWT claims type Claims struct { Username string `json:"username"` jwt.RegisteredClaims } // secretKey returns the JWT signing key (env or default) func secretKey() []byte { key := os.Getenv("JWT_SECRET") if key == "" { key = "autossl-default-secret-change-me" } return []byte(key) } // TokenExpiry returns token lifetime func TokenExpiry() time.Duration { return 24 * time.Hour } // Authenticate checks credentials against environment config func Authenticate(username, password string) bool { admin := defaultAdmin() // Constant-time comparison to prevent timing attacks mac1 := hmac.New(sha256.New, []byte(admin.Username)) mac1.Write([]byte(admin.Password)) expected := hex.EncodeToString(mac1.Sum(nil)) mac2 := hmac.New(sha256.New, []byte(username)) mac2.Write([]byte(password)) actual := hex.EncodeToString(mac2.Sum(nil)) return hmac.Equal([]byte(expected), []byte(actual)) } // GenerateToken creates a JWT token for the given username func GenerateToken(username string) (string, int64, error) { expiresAt := time.Now().Add(TokenExpiry()) claims := Claims{ Username: username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: "autossl", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(secretKey()) if err != nil { return "", 0, err } return tokenString, expiresAt.Unix(), nil } // ValidateToken parses and validates a JWT token, returning claims func ValidateToken(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, ErrInvalidToken } return secretKey(), nil }) if err != nil { return nil, ErrInvalidToken } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, ErrInvalidToken } return claims, nil }