cache_name = 'auth'; $this->cache_ttl = WEEK_IN_SECONDS; $this->emailManager = new EmailManager(); if (Features::forSite()->has('magicLink')) { $this->magic_link = new MagicLinkManager(); } parent::__construct(); } public function registerRoutes(): void { // Login endpoint register_rest_route($this->namespace, '/auth/login', [ 'methods' => 'POST', 'callback' => [$this, 'handleLogin'], 'permission_callback' => [$this, 'checkRateLimit'], 'args' => [ 'user_email' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_email' ], 'user_password' => [ 'required' => true, 'type' => 'string' ], 'remember_me' => [ 'required' => false, 'type' => 'boolean', 'default' => false ] ] ]); // Logout endpoint register_rest_route($this->namespace, '/auth/logout', [ 'methods' => 'POST', 'callback' => [$this, 'handleLogout'], 'permission_callback' => 'is_user_logged_in' ]); // Check auth status register_rest_route($this->namespace, '/auth/status', [ 'methods' => 'GET', 'callback' => [$this, 'getAuthStatus'], 'permission_callback' => '__return_true' ]); // Request password reset register_rest_route($this->namespace, '/auth/lostpassword', [ 'methods' => 'POST', 'callback' => [$this, 'requestPasswordReset'], 'permission_callback' => [$this, 'checkRateLimit'], 'args' => [ 'user_email' => [ 'required' => true, 'type' => 'string', 'format' => 'email', 'sanitize_callback' => 'sanitize_email' ] ] ]); // Complete password reset (with token) register_rest_route($this->namespace, '/auth/reset-password', [ 'methods' => 'POST', 'callback' => [$this, 'resetPassword'], 'permission_callback' => [$this, 'checkRateLimit'], 'args' => [ 'token' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ], 'user_email' => [ 'required' => true, 'type' => 'string', 'format' => 'email', 'sanitize_callback' => 'sanitize_email' ], 'password' => [ 'required' => true, 'type' => 'string' ] ] ]); register_rest_route($this->namespace, '/auth/register', [ 'method' => 'POST', 'callback' => [$this, 'handleRegister'], 'permission_callback' => [$this, 'checkRateLimit'], ]); // Refresh session register_rest_route($this->namespace, '/auth/refresh', [ 'methods' => 'POST', 'callback' => [$this, 'refreshSession'], 'permission_callback' => 'is_user_logged_in' ]); register_rest_route($this->namespace, '/auth/email', [ 'methods' => 'POST', 'callback' => [$this, 'checkEmailExists'], 'permission_callback' => [$this, 'checkRateLimit'], ]); } public function handleLogin(WP_REST_Request $request): WP_REST_Response { // Verify Turnstile if (!$this->verifyTurnstile($request->get_param('cf-turnstile-response') ?? '')) { return $this->error('Security verification failed', 'turnstile_failed', 403); } $username = $request->get_param('user_email'); $password = $request->get_param('user_password'); $remember = (bool)$request->get_param('remember_me'); // Check for account lockout $lockout = $this->checkAccountLockout($username); if (is_wp_error($lockout)) { return $this->error( $lockout->get_error_message(), 'account_locked', 429 ); } // Attempt login $user = wp_signon([ 'user_login' => $username, 'user_email' => $username, 'user_password' => $password, 'remember' => $remember ], false); if (is_wp_error($user)) { // Track failed attempt $this->trackFailedLogin($username); return $this->error( 'Invalid username or password', 'login_failed', 401 ); } // Clear failed attempts on success $this->clearFailedAttempts($username); // Set auth cookie with remember me flag wp_set_current_user($user->ID); wp_set_auth_cookie($user->ID, $remember); // Store session fingerprint for hijacking protection $this->storeSessionFingerprint($user->ID, $request); // Trigger WordPress login action do_action('wp_login', $user->user_login, $user); // Audit log $this->auditLog('user_login', [ 'user_id' => $user->ID, 'remember' => $remember ]); return $this->success([ 'message' => 'Login successful', 'user' => $this->formatUserData($user), 'redirect' => $this->getLoginRedirect($user) ]); } /** * Handle logout request */ public function handleLogout(WP_REST_Request $request): WP_REST_Response { $user_id = get_current_user_id(); // Clear session fingerprint $this->clearSessionFingerprint($user_id); // Audit log $this->auditLog('user_logout', ['user_id' => $user_id]); // WordPress logout wp_logout(); return $this->success([ 'message' => 'Logged out successfully' ]); } /** * Get current authentication status */ public function getAuthStatus(WP_REST_Request $request): WP_REST_Response { if (!is_user_logged_in()) { return $this->success([ 'authenticated' => false ]); } $user = wp_get_current_user(); return $this->success([ 'authenticated' => true, 'user' => $this->formatUserData($user) ]); } /** * Request password reset */ public function requestPasswordReset(WP_REST_Request $request): WP_REST_Response { $email = $request->get_param('user_email')??''; $email = sanitize_email($email); $token = $request->get_param('cf-turnstile-response')??''; if (!$this->verifyTurnstile($token)){ return $this->error('Security verification failed', 'turnstile_failed', 403); } // Use WordPress's built-in function $result = retrieve_password($email); // Log but don't expose if (is_wp_error($result)) { $this->auditLog('password_reset_failed', [ 'email' => $email, 'reason' => $result->get_error_code() ]); } else { $this->auditLog('password_reset_sent', [ 'email' => $email ]); } return $this->success([ 'message' => 'If an account exists with this email, you will receive a password reset link.' ]); } /** * Complete password reset with token */ public function resetPassword(WP_REST_Request $request): WP_REST_Response { $key = sanitize_text_field($request->get_param('key')); $login = sanitize_text_field($request->get_param('login')); $password = $request->get_param('password'); if (empty($key) || empty($login)) { return $this->error('Invalid reset link', 'invalid_key', 400); } // Use WordPress's native key verification $user = check_password_reset_key($key, $login); if (is_wp_error($user)) { return $this->error( 'Invalid or expired reset link', 'invalid_token', 400 ); } // Validate password strength $validation = $this->validatePassword($password); if (is_wp_error($validation)) { return $this->error( $validation->get_error_message(), 'weak_password', 400 ); } // Reset the password reset_password($user, $password); // Log them in wp_set_current_user($user->ID); wp_set_auth_cookie($user->ID, true); // Store session fingerprint $this->storeSessionFingerprint($user->ID, $request); // Audit log $this->auditLog('password_reset_complete', [ 'user_id' => $user->ID ]); return $this->success([ 'message' => 'Password reset successfully', 'redirect' => home_url('/dash') ]); } /** * Refresh session (extends session duration) */ public function refreshSession(WP_REST_Request $request): WP_REST_Response { $user_id = get_current_user_id(); // Validate session fingerprint if (!$this->validateSessionFingerprint($user_id, $request)) { wp_logout(); return $this->unauthorized('Session validation failed'); } // Refresh auth cookie wp_set_auth_cookie($user_id, true); return $this->success([ 'message' => 'Session refreshed' ]); } public function handleRegister(WP_REST_Request $request): WP_REST_Response { $data = $request->get_json_params(); // Duplicate submission check if (!$this->checkRequestId($data['request_id'] ?? '')) { return $this->error('Duplicate request detected', 'duplicate_request', 409); } // Verify Turnstile if (!$this->verifyTurnstile($data['cf-turnstile-response'] ?? '')) { return $this->error('Security verification failed', 'turnstile_failed', 403); } $name = sanitize_text_field($data['name'] ?? ''); $email = sanitize_email($data['email'] ?? ''); $user_type = sanitize_text_field($data['user_select'] ?? 'subscriber'); // Validate fields if (empty($name)) { return $this->error('Name is required', 'missing_name', 400, 'name'); } if (empty($email)) { return $this->error('Email is required', 'missing_email', 400, 'email'); } // Spam prevention if ($user_type === 'subscriber' && count(JVB_USER) > 0) { $registerable = array_filter(JVB_USER, fn($config) => $config['can_register'] ?? false); if (!empty($registerable)) { return $this->error('Please select a valid account type', 'invalid_user_type', 400, 'user_select'); } } // Check if role can register if ($user_type !== 'subscriber') { if (!isset(JVB_USER[$user_type]) || empty(JVB_USER[$user_type]['can_register'])) { return $this->error('Invalid account type', 'invalid_user_type', 400, 'user_select'); } } // Check if email exists if (email_exists($email)) { return $this->error('Email already registered', 'duplicate_email', 400, 'email'); } // Allow WP plugins to add registration errors $errors = new WP_Error(); $errors = apply_filters('registration_errors', $errors, $email, $email); if ($errors->has_errors()) { return $this->error( $errors->get_error_message(), $errors->get_error_code(), 400 ); } // Create user $user_id = wp_create_user($email, wp_generate_password(), $email); if (is_wp_error($user_id)) { return $this->error($user_id->get_error_message(), 'user_creation_failed', 500); } // Update user data wp_update_user([ 'ID' => $user_id, 'display_name' => $name, 'first_name' => strtok($name,' ') ]); // Set role $user = new WP_User($user_id); if ($user_type === 'subscriber') { $user->set_role('subscriber'); } else { $role = JVB_USER[$user_type]['role'] ?? 'subscriber'; $user->set_role($role); // Check if needs approval if (Features::forMembership()->has('memberVerified') && in_array($role, JVB_MEMBERSHIP['memberVerified'] ?? [])) { $user->add_cap('skip_moderation', false); update_user_meta($user_id, BASE . 'pending_approval', true); } } if (Features::forUser($user_type)->has('namedDirectory')) { $upload_dir = wp_upload_dir(); $user_directory = $user_type.'/'.$user_id; $target_dir = $upload_dir['basedir'].'/'.$user_directory; wp_mkdir_p($target_dir); } // Save additional fields update_user_meta($user_id, BASE . 'user_type', $user_type); // Process additional fields from form foreach ($data as $key => $value) { if (in_array($key, ['name', 'email', 'action', 'request_id', 'user_select', 'cf-turnstile-response'])) { continue; } update_user_meta($user_id, BASE . $key, sanitize_text_field($value)); } // Handle token handlers do_action('jvbUserRegistered', $user_id, $email, $data); return $this->success([ 'message' => 'Registration successful! Check your email.', 'user_id' => $user_id ]); } /************************************************************** HELPERS **************************************************************/ /** * Format user data for response */ protected function formatUserData(WP_User $user): array { return [ 'id' => $user->ID, 'username' => $user->user_login, 'email' => $user->user_email, 'display_name' => $user->display_name, 'roles' => $user->roles, 'capabilities' => [ 'manage_options' => user_can($user, 'manage_options'), 'skip_moderation' => user_can($user, 'skip_moderation'), // Add other relevant capabilities ] ]; } /** * Get login redirect URL based on user role */ protected function getLoginRedirect(WP_User $user): string { if (user_can($user, 'manage_options')) { return admin_url(); } // Redirect to dashboard for members if (function_exists('isOurPeople') && isOurPeople()) { return home_url('/dash'); } return home_url(); } /** * Validate password strength */ protected function validatePassword(string $password): bool|WP_Error { if (strlen($password) < 8) { return new WP_Error( 'weak_password', 'Password must be at least 8 characters long' ); } // Add additional strength requirements as needed // - Must contain uppercase // - Must contain number // - Must contain special character return true; } /** * Check if account is locked out due to failed attempts */ protected function checkAccountLockout(string $username): bool|WP_Error { if (!defined('JVB_MAX_LOGIN_ATTEMPTS')) { return true; } $cache_key = 'login_attempts_' . md5($username); $attempts = $this->cache->get($cache_key); if (!$attempts || !isset($attempts['count'])) { return true; } if ($attempts['count'] >= JVB_MAX_LOGIN_ATTEMPTS) { $lockout_duration = defined('JVB_LOCKOUT_DURATION') ? JVB_LOCKOUT_DURATION : 15 * MINUTE_IN_SECONDS; $time_remaining = $lockout_duration - (time() - $attempts['timestamp']); if ($time_remaining > 0) { return new WP_Error( 'account_locked', sprintf( 'Too many failed login attempts. Please try again in %d minutes.', ceil($time_remaining / 60) ) ); } // Lockout expired - clear attempts $this->cache->delete($cache_key); return true; } return true; } /** * Track failed login attempt */ protected function trackFailedLogin(string $username): void { $cache_key = 'login_attempts_' . md5($username); $attempts = $this->cache->get($cache_key) ?: [ 'count' => 0, 'timestamp' => time() ]; $attempts['count']++; $attempts['timestamp'] = time(); $lockout_duration = defined('JVB_LOCKOUT_DURATION') ? JVB_LOCKOUT_DURATION : 15 * MINUTE_IN_SECONDS; $this->cache->set($cache_key, $attempts, $lockout_duration); // Audit log $this->auditLog('failed_login', [ 'username' => $username, 'attempts' => $attempts['count'] ]); } /** * Clear failed login attempts on successful login */ protected function clearFailedAttempts(string $username): void { $cache_key = 'login_attempts_' . md5($username); $this->cache->delete($cache_key); } public function checkEmailExists(WP_REST_Request $request): WP_REST_Response { $data = $request->get_json_params(); $email = sanitize_email($data['email'] ?? ''); return $this->success([ 'exists' => is_email($email) && email_exists($email) ]); } /*********************************************************************** * HELPER METHODS ***********************************************************************/ protected function checkRequestId(string $request_id): bool { if (empty($request_id)) { return true; } $cache_key = 'request_' . $request_id; if (get_transient($cache_key)) { return false; } set_transient($cache_key, true, 60); return true; } protected function verifyTurnstile(string $token): bool { if (!Features::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) { return true; } if (empty($token)) { return false; } return JVB()->connect('cloudflare')->verifyTurnstile($token); } /** * Helper to return error response */ protected function error(string $message, string $code, int $status = 400, ?string $field = null): WP_REST_Response { if ($this->requestId) { delete_transient('request_'.$this->requestId); $this->requestId = null; } return parent::error($message, $code, $status, $field); } }