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", }) }