Files
Auto-ssl/backend/auth/auth.go
T
cnbugs 2d70a15307 feat: add login authentication and certificate expiry notifications
- JWT-based login with ADMIN_USER/ADMIN_PASSWORD env config
- Login page with auth guard for Vue frontend
- Feishu bot webhook notification for expiring certificates
- SMTP email notification for expiring certificates
- Daily 8:00 AM cron for expiry checks
- Startup health check notification
2026-07-23 16:25:02 +08:00

123 lines
3.0 KiB
Go

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
}