PluginProbe ʕ •ᴥ•ʔ
Limit Login Attempts Security – Login Security, 2FA, Firewall, Brute Force Prevention / 3.2.4
Limit Login Attempts Security – Login Security, 2FA, Firewall, Brute Force Prevention v3.2.4
3.2.4 3.2.3 3.2.2 3.2.1 3.2.0 trunk 2.0.0 2.1.0 2.10.0 2.10.1 2.11.0 2.12.0 2.12.1 2.12.2 2.12.3 2.13.0 2.14.0 2.15.0 2.15.1 2.15.2 2.16.0 2.17.0 2.17.1 2.17.2 2.17.3 2.17.4 2.18.0 2.19.0 2.19.1 2.19.2 2.2.0 2.20.0 2.20.1 2.20.2 2.20.3 2.20.4 2.20.5 2.20.6 2.21.0 2.21.1 2.22.0 2.22.1 2.23.0 2.23.1 2.23.2 2.24.0 2.24.1 2.25.0 2.25.1 2.25.10 2.25.11 2.25.12 2.25.13 2.25.14 2.25.15 2.25.16 2.25.17 2.25.18 2.25.19 2.25.2 2.25.20 2.25.21 2.25.22 2.25.23 2.25.24 2.25.25 2.25.26 2.25.27 2.25.28 2.25.29 2.25.3 2.25.4 2.25.5 2.25.6 2.25.7 2.25.8 2.25.9 2.26.0 2.26.1 2.26.10 2.26.11 2.26.12 2.26.13 2.26.14 2.26.15 2.26.16 2.26.17 2.26.18 2.26.19 2.26.2 2.26.20 2.26.21 2.26.22 2.26.23 2.26.24 2.26.25 2.26.26 2.26.27 2.26.28 2.26.3 2.26.4 2.26.5 2.26.6 2.26.7 2.26.8 2.26.9 2.3.0 2.4.0 2.5.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.8.0 2.8.1 2.9.0 3.0.0 3.0.1 3.0.2 3.1.0
limit-login-attempts-reloaded / core / mfa-flow / CallbackHandler.php
limit-login-attempts-reloaded / core / mfa-flow Last commit date
Providers 2 weeks ago CallbackHandler.php 2 weeks ago MfaApiClient.php 2 weeks ago MfaFlowSendCode.php 2 weeks ago MfaProviderRegistry.php 2 weeks ago MfaRestApi.php 2 weeks ago SessionStore.php 2 weeks ago
CallbackHandler.php
261 lines
1 <?php
2
3 namespace LLAR\Core\MfaFlow;
4
5 use LLAR\Core\Helpers;
6 use LLAR\Core\LimitLoginAttempts;
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Handles MFA callback: llar_mfa=1&token=...&code=...
14 * Verifies session and OTP, calls API verify, then logs user in and redirects.
15 */
16 class CallbackHandler {
17 /**
18 * Record successful login in Cloud App for MFA-based auth.
19 *
20 * Note: MFA callback logs the user in via cookies and does not trigger `wp_login`.
21 *
22 * @param WP_User $user
23 * @param string $username
24 *
25 * @return void
26 */
27 private static function record_successful_login( $user, $username ) {
28 if ( empty( $username ) ) {
29 return;
30 }
31
32 if ( ! $user || ! is_object( $user ) || empty( $user->ID ) ) {
33 return;
34 }
35
36 if ( ! LimitLoginAttempts::$cloud_app ) {
37 return;
38 }
39
40 $clean_url = '';
41 if ( isset( $_SERVER['HTTP_REFERER'] ) ) {
42 $referer_url = $_SERVER['HTTP_REFERER'];
43 $referer_parsed = parse_url( $referer_url );
44 $clean_url = isset( $referer_parsed['path'] ) ? $referer_parsed['path'] : '';
45 $clean_url = trim( $clean_url, '/' );
46 }
47
48 $gateway = Helpers::detect_gateway();
49 $data = array(
50 'ip' => Helpers::get_all_ips(),
51 'login' => $username,
52 'user_id' => (int) $user->ID,
53 'gateway' => $gateway,
54 'roles' => isset( $user->roles ) ? $user->roles : array(),
55 'agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
56 'url' => $clean_url,
57 );
58
59 LimitLoginAttempts::$cloud_app->request( 'login', 'post', $data );
60 }
61
62 /**
63 * Run on init: if request has llar_mfa and token, handle callback or show enter-code form.
64 *
65 * @return void
66 */
67 public static function maybe_handle() {
68 // Do not treat send-code endpoint as MFA callback (it uses token in POST body).
69 $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '';
70 if ( $action === 'llar_mfa_flow_send_code' ) {
71 return;
72 }
73 if ( function_exists( 'rest_url' ) && isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], 'rest_route=' ) !== false && strpos( $_SERVER['REQUEST_URI'], 'llar/v1/mfa/send-code' ) !== false ) {
74 return;
75 }
76
77 $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : '';
78 $token_from_query = $token;
79 $code = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : '';
80 if ( '' === $code && isset( $_POST['code'] ) ) {
81 $code = sanitize_text_field( wp_unslash( $_POST['code'] ) );
82 }
83
84 $has_llar_mfa_param = ( isset( $_GET['llar_mfa'] ) && ( $_GET['llar_mfa'] === '1' || $_GET['llar_mfa'] === 'true' ) );
85 $cookie_state = isset( $_COOKIE['llar_mfa_state'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['llar_mfa_state'] ) ) : '';
86 if ( '' === $token && $has_llar_mfa_param && '' !== $cookie_state ) {
87 $store = new SessionStore();
88 $resolved_token = $store->get_callback_token( $cookie_state );
89 if ( is_string( $resolved_token ) && '' !== $resolved_token ) {
90 $token = $resolved_token;
91 }
92 }
93 $is_mfa_callback = $token !== '' && $has_llar_mfa_param;
94
95 if ( ! $is_mfa_callback ) {
96 return;
97 }
98
99 if ( $code === '' ) {
100 // If callback arrived without explicit token in URL, treat it as local MFA screen entry.
101 // Do not consume callback state yet; wait for a code submit.
102 if ( '' === $token_from_query ) {
103 return;
104 }
105 // Return from external MFA app with token only: try API verify; if not verified, redirect to login (no on-site code form).
106 self::try_verify_and_login( $token );
107 self::redirect_login( 'llar_mfa_session_expired' );
108 exit;
109 }
110
111 self::handle( $token, $code );
112 exit;
113 }
114
115 /**
116 * When we have token but no code (return from external app): call API verify; if is_verified, log user in and redirect.
117 *
118 * @param string $token Session token.
119 * @return void Exits on success; returns otherwise.
120 */
121 private static function try_verify_and_login( $token ) {
122 $store = new SessionStore();
123 $session = $store->get_session( $token );
124 $cookie = isset( $_COOKIE['llar_mfa_state'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['llar_mfa_state'] ) ) : '';
125 if ( ! $session || empty( $session['secret'] ) || empty( $session['username'] ) ) {
126 return;
127 }
128 $verify = $store->consume_callback_state( $cookie, $token );
129 SessionStore::set_state_cookie( '' );
130 if ( ! $verify ) {
131 $store->delete_session( $token );
132 self::redirect_login( 'llar_mfa_session_expired' );
133 exit;
134 }
135 if ( empty( $session['is_pre_authenticated'] ) ) {
136 return;
137 }
138 $provider_id = isset( $session['provider_id'] ) ? $session['provider_id'] : 'llar';
139 $provider = MfaProviderRegistry::get( $provider_id );
140 if ( ! $provider ) {
141 return;
142 }
143 $result = $provider->verify( $token, $session['secret'] );
144 if ( ! $result['success'] || empty( $result['data']['is_verified'] ) ) {
145 return;
146 }
147 $user_id = ! empty( $session['user_id'] ) ? (int) $session['user_id'] : 0;
148 $user = $user_id ? get_user_by( 'id', $user_id ) : get_user_by( 'login', $session['username'] );
149 if ( ! $user || ! is_a( $user, 'WP_User' ) ) {
150 return;
151 }
152 wp_clear_auth_cookie();
153 wp_set_current_user( $user->ID );
154 $remember_me = ! empty( $session['remember_me'] );
155 wp_set_auth_cookie( $user->ID, $remember_me );
156 self::record_successful_login( $user, $session['username'] );
157 $redirect_to = ! empty( $session['redirect_to'] ) ? $session['redirect_to'] : '';
158 $redirect_url = ( $redirect_to && self::is_safe_redirect( $redirect_to ) ) ? $redirect_to : admin_url();
159 wp_safe_redirect( $redirect_url );
160 $store->delete_session( $token );
161 exit;
162 }
163
164 /**
165 * Handle callback: load session, verify OTP and API, then login and redirect.
166 *
167 * @param string $token Session token.
168 * @param string $code User-entered OTP code.
169 */
170 public static function handle( $token, $code ) {
171 $store = new SessionStore();
172 $session = $store->get_session( $token );
173 $cookie = isset( $_COOKIE['llar_mfa_state'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['llar_mfa_state'] ) ) : '';
174 if ( ! $session || empty( $session['secret'] ) || empty( $session['username'] ) ) {
175 $store->delete_session( $token );
176 self::redirect_login( 'llar_mfa_session_expired' );
177 return;
178 }
179 $verify = $store->consume_callback_state( $cookie, $token );
180 SessionStore::set_state_cookie( '' );
181 if ( ! $verify ) {
182 $store->delete_session( $token );
183 self::redirect_login( 'llar_mfa_session_expired' );
184 return;
185 }
186
187 if ( ! $store->verify_otp_once( $token, $code ) ) {
188 $store->delete_session( $token );
189 self::redirect_login( 'llar_mfa_code_invalid' );
190 return;
191 }
192
193 $provider_id = isset( $session['provider_id'] ) ? $session['provider_id'] : 'llar';
194 $provider = MfaProviderRegistry::get( $provider_id );
195 if ( ! $provider ) {
196 $store->delete_session( $token );
197 self::redirect_login( 'llar_mfa_verify_failed' );
198 return;
199 }
200 $result = $provider->verify( $token, $session['secret'] );
201
202 if ( ! $result['success'] || empty( $result['data']['is_verified'] ) ) {
203 $store->delete_session( $token );
204 self::redirect_login( 'llar_mfa_verify_failed' );
205 return;
206 }
207
208 $user_id = ! empty( $session['user_id'] ) ? (int) $session['user_id'] : 0;
209 $user = $user_id ? get_user_by( 'id', $user_id ) : get_user_by( 'login', $session['username'] );
210
211 if ( ! $user || ! is_a( $user, 'WP_User' ) ) {
212 $store->delete_session( $token );
213 self::redirect_login( 'llar_mfa_user_invalid' );
214 return;
215 }
216
217 if ( empty( $session['is_pre_authenticated'] ) ) {
218 $store->delete_session( $token );
219 self::redirect_login( 'llar_mfa_pre_auth_required' );
220 return;
221 }
222
223 wp_clear_auth_cookie();
224 wp_set_current_user( $user->ID );
225 $remember_me = ! empty( $session['remember_me'] );
226 wp_set_auth_cookie( $user->ID, $remember_me );
227 self::record_successful_login( $user, $session['username'] );
228
229 $redirect_to = ! empty( $session['redirect_to'] ) ? $session['redirect_to'] : '';
230 $redirect_url = ( $redirect_to && self::is_safe_redirect( $redirect_to ) ) ? $redirect_to : admin_url();
231 wp_safe_redirect( $redirect_url );
232 $store->delete_session( $token );
233 exit;
234 }
235
236 /**
237 * Redirect to login with optional message key.
238 *
239 * @param string $msg_key Optional. Query arg for message.
240 */
241 private static function redirect_login( $msg_key = '' ) {
242 $url = wp_login_url();
243 if ( $msg_key ) {
244 $url = add_query_arg( 'llar_mfa_error', $msg_key, $url );
245 }
246 wp_safe_redirect( $url );
247 }
248
249 /**
250 * Check if redirect URL is safe (same host or allowed).
251 *
252 * @param string $url Redirect URL.
253 * @return bool
254 */
255 private static function is_safe_redirect( $url ) {
256 $allowed = wp_validate_redirect( $url, false );
257 return ( $allowed !== false );
258 }
259
260 }
261