2d70a15307
- 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
123 lines
3.0 KiB
Go
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
|
|
}
|