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
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user