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 / SessionStore.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
SessionStore.php
358 lines
1 <?php
2
3 namespace LLAR\Core\MfaFlow;
4
5 use LLAR\Core\Config;
6
7 if ( ! defined( 'ABSPATH' ) ) {
8 exit;
9 }
10
11 /**
12 * MFA flow session and OTP storage in transients.
13 */
14 class SessionStore {
15
16 /**
17 * Set or clear the MFA state cookie.
18 *
19 * @param string $value State value; empty string clears the cookie.
20 */
21 public static function set_state_cookie( $value ) {
22 $expire = ( '' === $value ) ? time() - 3600 : time() + 600;
23 setcookie( 'llar_mfa_state', $value, $expire, '/', '', is_ssl(), true );
24 }
25
26 /**
27 * Save session after handshake.
28 *
29 * @param string $token From API.
30 * @param string $secret From API.
31 * @param string $username Login name.
32 * @param int $user_id Optional. User ID if known.
33 * @param string $redirect_to Optional. URL to redirect after login.
34 * @param string $cancel_url Optional. URL for MFA app cancel.
35 * @param string $provider_id Optional. Provider id (e.g. 'llar').
36 * @param bool $is_pre_authenticated True if password was already validated at handshake.
37 * @param bool $remember_me True if user checked "Remember Me" on login form.
38 * @return bool True if saved.
39 */
40 public function save_session( $token, $secret, $username, $user_id = 0, $redirect_to = '', $cancel_url = '', $provider_id = '', $is_pre_authenticated = false, $remember_me = false ) {
41 if ( ! is_string( $token ) || '' === $token ) {
42 return false;
43 }
44 $ttl = defined( 'LLA_MFA_SESSION_TTL' ) ? (int) LLA_MFA_SESSION_TTL : 600;
45 $ttl = $ttl > 0 ? $ttl : 600;
46
47 $data = array(
48 'token' => $token,
49 'secret' => $secret,
50 'username' => $username,
51 'user_id' => (int) $user_id,
52 'redirect_to' => is_string( $redirect_to ) ? $redirect_to : '',
53 'cancel_url' => is_string( $cancel_url ) ? $cancel_url : '',
54 'provider_id' => is_string( $provider_id ) ? $provider_id : 'llar',
55 'is_pre_authenticated' => (bool) $is_pre_authenticated,
56 'remember_me' => (bool) $remember_me,
57 'created' => time(),
58 );
59
60 $key = LLA_MFA_FLOW_TRANSIENT_SESSION_PREFIX . $token;
61 return (bool) set_transient( $key, $data, $ttl );
62 }
63
64 /**
65 * Get session by token.
66 *
67 * @param string $token Session token.
68 * @return array|null Session data or null if not found.
69 */
70 public function get_session( $token ) {
71 if ( ! is_string( $token ) || '' === $token ) {
72 return null;
73 }
74 $key = LLA_MFA_FLOW_TRANSIENT_SESSION_PREFIX . $token;
75 $data = get_transient( $key );
76 if ( false === $data || ! is_array( $data ) ) {
77 return null;
78 }
79 return $data;
80 }
81
82 /**
83 * Delete session by token.
84 *
85 * @param string $token Session token.
86 */
87 public function delete_session( $token ) {
88 if ( ! is_string( $token ) || '' === $token ) {
89 return;
90 }
91 delete_transient( LLA_MFA_FLOW_TRANSIENT_SESSION_PREFIX . $token );
92 delete_transient( LLA_MFA_FLOW_TRANSIENT_SEND_SECRET_PREFIX . $token );
93 $this->delete_otp( $token );
94 }
95
96 /**
97 * Save callback state for token (CSRF protection for anonymous users).
98 * State is stored server-side and mirrored in a HttpOnly cookie.
99 *
100 * @param string $state Random state string.
101 * @param string $token MFA session token.
102 * @return bool
103 */
104 public function save_callback_state( $state, $token ) {
105 if ( ! is_string( $state ) || '' === $state || ! is_string( $token ) || '' === $token ) {
106 return false;
107 }
108 $ttl = defined( 'LLA_MFA_SESSION_TTL' ) ? (int) LLA_MFA_SESSION_TTL : 600;
109 $ttl = $ttl > 0 ? $ttl : 600;
110 $key = ( defined( 'LLA_MFA_FLOW_TRANSIENT_STATE_PREFIX' ) ? LLA_MFA_FLOW_TRANSIENT_STATE_PREFIX : 'llar_mfa_state_' ) . $state;
111 return (bool) set_transient( $key, $token, $ttl );
112 }
113
114 /**
115 * Consume callback state (one-time). Returns true if state matches token.
116 *
117 * @param string $state Cookie state value.
118 * @param string $token MFA session token from request.
119 * @return bool
120 */
121 public function consume_callback_state( $state, $token ) {
122 if ( ! is_string( $state ) || '' === $state || ! is_string( $token ) || '' === $token ) {
123 return false;
124 }
125 $key = ( defined( 'LLA_MFA_FLOW_TRANSIENT_STATE_PREFIX' ) ? LLA_MFA_FLOW_TRANSIENT_STATE_PREFIX : 'llar_mfa_state_' ) . $state;
126 $value = get_transient( $key );
127 if ( false === $value || ! is_string( $value ) || $value !== $token ) {
128 return false;
129 }
130 delete_transient( $key );
131 return true;
132 }
133
134 /**
135 * Resolve token by callback state without consuming it.
136 *
137 * @param string $state Cookie state value.
138 * @return string|null
139 */
140 public function get_callback_token( $state ) {
141 if ( ! is_string( $state ) || '' === $state ) {
142 return null;
143 }
144 $key = ( defined( 'LLA_MFA_FLOW_TRANSIENT_STATE_PREFIX' ) ? LLA_MFA_FLOW_TRANSIENT_STATE_PREFIX : 'llar_mfa_state_' ) . $state;
145 $value = get_transient( $key );
146 return ( false !== $value && is_string( $value ) && '' !== $value ) ? $value : null;
147 }
148
149 /**
150 * Save OTP hash for token (for callback verification).
151 *
152 * @param string $token Session token.
153 * @param string $code OTP code.
154 * @return bool
155 */
156 public function save_otp( $token, $code ) {
157 if ( ! is_string( $token ) || '' === $token || ! is_string( $code ) || '' === $code ) {
158 return false;
159 }
160 $ttl = defined( 'LLA_MFA_FLOW_OTP_TTL' ) ? (int) LLA_MFA_FLOW_OTP_TTL : 180;
161 $key = LLA_MFA_FLOW_TRANSIENT_OTP_PREFIX . $token;
162 return (bool) set_transient( $key, $this->hash_otp_code( $code ), $ttl );
163 }
164
165 /**
166 * Get OTP hash for token (read-only).
167 *
168 * @param string $token Session token.
169 * @return string|null OTP hash value or null.
170 */
171 public function get_otp( $token ) {
172 if ( ! is_string( $token ) || '' === $token ) {
173 return null;
174 }
175 $key = LLA_MFA_FLOW_TRANSIENT_OTP_PREFIX . $token;
176 $hash = get_transient( $key );
177 return ( false !== $hash && is_string( $hash ) ) ? $hash : null;
178 }
179
180 /**
181 * Get OTP hash for token and delete it (one-time use). Use in callback to prevent OTP reuse.
182 *
183 * @param string $token Session token.
184 * @return string|null OTP hash value or null if not found or already consumed.
185 */
186 public function get_otp_once( $token ) {
187 if ( ! is_string( $token ) || '' === $token ) {
188 return null;
189 }
190
191 $lock_name = 'llar_mfa_otp_lock_' . hash( 'sha256', $token );
192 if ( ! add_option( $lock_name, time(), '', 'no' ) ) {
193 return null;
194 }
195
196 try {
197 $transient_key = LLA_MFA_FLOW_TRANSIENT_OTP_PREFIX . $token;
198 $hash = get_transient( $transient_key );
199 if ( false === $hash || ! is_string( $hash ) ) {
200 return null;
201 }
202
203 // For DB-backed transients consume OTP atomically via low-level DELETE.
204 if ( ! wp_using_ext_object_cache() ) {
205 global $wpdb;
206
207 $option_name = '_transient_' . $transient_key;
208 $deleted = $wpdb->query(
209 $wpdb->prepare(
210 'DELETE FROM ' . $wpdb->options . ' WHERE option_name = %s',
211 $option_name
212 )
213 );
214
215 if ( 1 !== (int) $deleted ) {
216 return null;
217 }
218
219 // Best-effort cleanup of the timeout row.
220 $timeout_name = '_transient_timeout_' . $transient_key;
221 $wpdb->query(
222 $wpdb->prepare(
223 'DELETE FROM ' . $wpdb->options . ' WHERE option_name = %s',
224 $timeout_name
225 )
226 );
227
228 return $hash;
229 }
230
231 $this->delete_otp( $token );
232 return $hash;
233 } finally {
234 delete_option( $lock_name );
235 }
236 }
237
238 /**
239 * Verify provided OTP against stored one-time hash and consume it.
240 *
241 * @param string $token Session token.
242 * @param string $code User-provided OTP code.
243 * @return bool
244 */
245 public function verify_otp_once( $token, $code ) {
246 if ( ! is_string( $token ) || '' === $token || ! is_string( $code ) || '' === $code ) {
247 return false;
248 }
249
250 $stored_hash = $this->get_otp_once( $token );
251 if ( null === $stored_hash ) {
252 return false;
253 }
254
255 $expected_hash = $this->hash_otp_code( $code );
256 if ( hash_equals( (string) $stored_hash, (string) $expected_hash ) ) {
257 return true;
258 }
259
260 // Backward compatibility: accept plaintext OTP that may still be in transient during rollout.
261 return hash_equals( (string) $stored_hash, (string) $code );
262 }
263
264 /**
265 * Hash OTP code with server-side pepper before storing/comparing.
266 *
267 * @param string $code OTP code.
268 * @return string
269 */
270 private function hash_otp_code( $code ) {
271 return hash_hmac( 'sha256', (string) $code, $this->get_otp_pepper() );
272 }
273
274 /**
275 * Resolve server-side pepper for OTP HMAC.
276 *
277 * @return string
278 */
279 private function get_otp_pepper() {
280 if ( function_exists( 'wp_salt' ) ) {
281 $salt = wp_salt( 'auth' );
282 if ( is_string( $salt ) && '' !== $salt ) {
283 return $salt;
284 }
285 }
286 if ( defined( 'AUTH_SALT' ) ) {
287 $auth_salt = constant( 'AUTH_SALT' );
288 if ( is_string( $auth_salt ) && '' !== $auth_salt ) {
289 return $auth_salt;
290 }
291 }
292 if ( defined( 'AUTH_KEY' ) ) {
293 $auth_key = constant( 'AUTH_KEY' );
294 if ( is_string( $auth_key ) && '' !== $auth_key ) {
295 return $auth_key;
296 }
297 }
298 return 'llar-mfa-otp-fallback-pepper';
299 }
300
301 /**
302 * Save send_email secret for token (validates POST send_code request body).
303 * Secret is the same as session secret: from MFA app handshake response, used for verify and send_code.
304 *
305 * @param string $token Session token.
306 * @param string $secret Secret from handshake response (MFA app).
307 * @return bool
308 */
309 public function save_send_email_secret( $token, $secret ) {
310 if ( ! is_string( $token ) || '' === $token || ! is_string( $secret ) || '' === $secret ) {
311 return false;
312 }
313 $ttl = defined( 'LLA_MFA_SESSION_TTL' ) ? (int) LLA_MFA_SESSION_TTL : 600;
314 $ttl = $ttl > 0 ? $ttl : 600;
315 $key = LLA_MFA_FLOW_TRANSIENT_SEND_SECRET_PREFIX . $token;
316 return (bool) set_transient( $key, $secret, $ttl );
317 }
318
319 /**
320 * Get send_email secret for token.
321 *
322 * @param string $token Session token.
323 * @return string|null Secret or null if not found.
324 */
325 public function get_send_email_secret( $token ) {
326 if ( ! is_string( $token ) || '' === $token ) {
327 return null;
328 }
329 $key = LLA_MFA_FLOW_TRANSIENT_SEND_SECRET_PREFIX . $token;
330 $secret = get_transient( $key );
331 return ( false !== $secret && is_string( $secret ) ) ? $secret : null;
332 }
333
334 /**
335 * Delete send_email secret for token (e.g. when session is deleted).
336 *
337 * @param string $token Session token.
338 */
339 public function delete_send_email_secret( $token ) {
340 if ( ! is_string( $token ) || '' === $token ) {
341 return;
342 }
343 delete_transient( LLA_MFA_FLOW_TRANSIENT_SEND_SECRET_PREFIX . $token );
344 }
345
346 /**
347 * Delete OTP for token.
348 *
349 * @param string $token Session token.
350 */
351 public function delete_otp( $token ) {
352 if ( ! is_string( $token ) || '' === $token ) {
353 return;
354 }
355 delete_transient( LLA_MFA_FLOW_TRANSIENT_OTP_PREFIX . $token );
356 }
357 }
358