@@ -0,0 +1,146 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"vctp/internal/auth"
|
||||
"vctp/server/models"
|
||||
)
|
||||
|
||||
const (
|
||||
authLoginFailureMessage = "invalid username or password"
|
||||
authLoginRequestTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
type ldapAuthenticator interface {
|
||||
AuthenticateAndFetchGroups(ctx context.Context, username string, password string) (auth.LDAPIdentity, error)
|
||||
}
|
||||
|
||||
type jwtService interface {
|
||||
IssueToken(subject string, roles []string, groups []string) (string, auth.Claims, error)
|
||||
}
|
||||
|
||||
var newLDAPAuthenticator = func(cfg auth.LDAPConfig) (ldapAuthenticator, error) {
|
||||
return auth.NewLDAPAuthenticator(cfg)
|
||||
}
|
||||
|
||||
var newJWTService = func(cfg auth.JWTConfig) (jwtService, error) {
|
||||
return auth.NewJWTService(cfg)
|
||||
}
|
||||
|
||||
// AuthLogin authenticates a user against LDAP and returns a signed JWT.
|
||||
// @Summary Login
|
||||
// @Description Authenticates a username/password against LDAP and returns a signed access token.
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body models.AuthLoginRequest true "Login credentials"
|
||||
// @Success 200 {object} models.AuthLoginResponse "Login success"
|
||||
// @Failure 400 {object} models.ErrorResponse "Invalid request"
|
||||
// @Failure 401 {object} models.ErrorResponse "Invalid credentials"
|
||||
// @Failure 500 {object} models.ErrorResponse "Server error"
|
||||
// @Failure 503 {object} models.ErrorResponse "Authentication disabled"
|
||||
// @Router /api/auth/login [post]
|
||||
func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
if h == nil || h.Settings == nil || h.Settings.Values == nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "authentication is not configured")
|
||||
return
|
||||
}
|
||||
|
||||
cfg := h.Settings.Values.Settings
|
||||
if !cfg.AuthEnabled {
|
||||
writeJSONError(w, http.StatusServiceUnavailable, "authentication is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.AuthLoginRequest
|
||||
if err := decodeJSONBody(w, r, &req); err != nil {
|
||||
h.Logger.Error("unable to decode auth login request", "error", err)
|
||||
writeJSONError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
username := strings.TrimSpace(req.Username)
|
||||
password := req.Password
|
||||
if username == "" || strings.TrimSpace(password) == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "username and password are required")
|
||||
return
|
||||
}
|
||||
|
||||
ldapAuth, err := newLDAPAuthenticator(auth.LDAPConfig{
|
||||
BindAddress: cfg.LDAPBindAddress,
|
||||
BaseDN: cfg.LDAPBaseDN,
|
||||
TrustCertFile: cfg.LDAPTrustCertFile,
|
||||
DisableValidation: cfg.LDAPDisableValidation,
|
||||
Insecure: cfg.LDAPInsecure,
|
||||
DialTimeout: authLoginRequestTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
h.Logger.Error("failed to initialize ldap authenticator", "error", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, "authentication service unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := withRequestTimeout(r, authLoginRequestTimeout)
|
||||
defer cancel()
|
||||
identity, err := ldapAuth.AuthenticateAndFetchGroups(ctx, username, password)
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.ErrLDAPInvalidCredentials) {
|
||||
h.Logger.Warn("auth login rejected", "username", username, "reason", "invalid_credentials")
|
||||
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
||||
h.Logger.Warn("auth login ldap timeout", "username", username, "error", err)
|
||||
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
|
||||
return
|
||||
}
|
||||
h.Logger.Warn("auth login ldap failure", "username", username, "error", err)
|
||||
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
|
||||
return
|
||||
}
|
||||
|
||||
roles := auth.ResolveRoles(identity.Groups, cfg.AuthGroupRoleMappings)
|
||||
if !auth.HasAnyGroup(identity.Groups, cfg.LDAPGroups) || len(roles) == 0 {
|
||||
h.Logger.Warn("auth login rejected", "username", username, "reason", "group_or_role_denied")
|
||||
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
|
||||
return
|
||||
}
|
||||
|
||||
jwtSvc, err := newJWTService(auth.JWTConfig{
|
||||
SigningKeyBase64: cfg.AuthJWTSigningKey,
|
||||
Issuer: cfg.AuthJWTIssuer,
|
||||
Audience: cfg.AuthJWTAudience,
|
||||
TokenLifespan: time.Duration(cfg.AuthTokenLifespanMinutes) * time.Minute,
|
||||
ClockSkew: time.Duration(cfg.AuthClockSkewSeconds) * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
h.Logger.Error("failed to initialize jwt service", "error", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, "authentication service unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
subject := strings.TrimSpace(identity.Username)
|
||||
if subject == "" {
|
||||
subject = username
|
||||
}
|
||||
token, claims, err := jwtSvc.IssueToken(subject, roles, identity.Groups)
|
||||
if err != nil {
|
||||
h.Logger.Error("failed to issue auth token", "username", username, "error", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, "failed to issue access token")
|
||||
return
|
||||
}
|
||||
|
||||
h.Logger.Info("auth login successful", "username", subject, "roles", roles)
|
||||
writeJSON(w, http.StatusOK, models.AuthLoginResponse{
|
||||
AccessToken: token,
|
||||
ExpiresAt: claims.ExpiresAt,
|
||||
TokenType: "Bearer",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user