Skip to content

Commit f1864eb

Browse files
chore: Add PR's requested changes
1 parent 34a6926 commit f1864eb

9 files changed

Lines changed: 164 additions & 3 deletions

File tree

resources/js/email_verification/email_verification.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const EmailVerificationPage = ({
127127
siteKey={captchaPublicKey}
128128
options={{ responseFieldName: "cf-turnstile-response" }}
129129
onSuccess={onChangeCaptchaProvider}
130+
onExpire={() => { captcha.current?.reset(); }}
130131
/>
131132
{captchaConfirmation && (
132133
<div className={styles.error_label}>

resources/js/forgot_password/forgot_password.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ const ForgotPasswordPage = ({
143143
siteKey={captchaPublicKey}
144144
options={{ responseFieldName: "cf-turnstile-response" }}
145145
onSuccess={onChangeCaptchaProvider}
146+
onExpire={() => { captcha.current?.reset(); }}
146147
/>
147148
{captchaConfirmation && (
148149
<div className={styles.error_label}>

resources/js/login/login.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ const PasswordInputForm = ({
8585
shouldShowCaptcha,
8686
captchaPublicKey,
8787
onChangeCaptchaProvider,
88+
onExpireCaptchaProvider,
8889
handleEmitOtpAction,
8990
forgotPasswordAction,
9091
loginAttempts,
@@ -183,12 +184,14 @@ const PasswordInputForm = ({
183184
<input type="hidden" value={userNameValue} id="username" name="username"/>
184185
<input type="hidden" value={csrfToken} id="_token" name="_token"/>
185186
<input type="hidden" value="password" id="flow" name="flow"/>
187+
<input type="hidden" value={loginAttempts} id="login_attempts" name="login_attempts"/>
186188
{shouldShowCaptcha() && captchaPublicKey &&
187189
<Turnstile
188190
className={styles.turnstile}
189191
siteKey={captchaPublicKey}
190192
options={{ responseFieldName: "cf-turnstile-response" }}
191193
onSuccess={onChangeCaptchaProvider}
194+
onExpire={onExpireCaptchaProvider}
192195
/>
193196
}
194197
<ExistingAccountActions
@@ -214,6 +217,7 @@ const OTPInputForm = ({
214217
shouldShowCaptcha,
215218
captchaPublicKey,
216219
onChangeCaptchaProvider,
220+
onExpireCaptchaProvider,
217221
onReset,
218222
loginAttempts
219223
}) => {
@@ -264,12 +268,14 @@ const OTPInputForm = ({
264268
<input type="hidden" value="otp" id="flow" name="flow"/>
265269
<input type="hidden" value={otpCode} id="password" name="password"/>
266270
<input type="hidden" value="email" id="connection" name="connection"/>
271+
<input type="hidden" value={loginAttempts} id="login_attempts" name="login_attempts"/>
267272
{shouldShowCaptcha() && captchaPublicKey &&
268273
<Turnstile
269274
className={styles.turnstile}
270275
siteKey={captchaPublicKey}
271276
options={{ responseFieldName: "cf-turnstile-response" }}
272277
onSuccess={onChangeCaptchaProvider}
278+
onExpire={onExpireCaptchaProvider}
273279
/>
274280
}
275281
</form>
@@ -474,6 +480,7 @@ class LoginPage extends React.Component {
474480
this.handleDelete = this.handleDelete.bind(this);
475481
this.onAuthenticate = this.onAuthenticate.bind(this);
476482
this.onChangeCaptchaProvider = this.onChangeCaptchaProvider.bind(this);
483+
this.onExpireCaptchaProvider = this.onExpireCaptchaProvider.bind(this);
477484
this.onUserPasswordChange = this.onUserPasswordChange.bind(this);
478485
this.onOTPCodeChange = this.onOTPCodeChange.bind(this);
479486
this.shouldShowCaptcha = this.shouldShowCaptcha.bind(this);
@@ -562,6 +569,10 @@ class LoginPage extends React.Component {
562569
this.setState({ ...this.state, captcha_value: value });
563570
}
564571

572+
onExpireCaptchaProvider() {
573+
this.setState({ ...this.state, captcha_value: '' });
574+
}
575+
565576
onHandleUserNameChange(ev) {
566577
let { value, id } = ev.target;
567578
this.setState({ ...this.state, user_name: value });
@@ -833,6 +844,7 @@ class LoginPage extends React.Component {
833844
shouldShowCaptcha={this.shouldShowCaptcha}
834845
captchaPublicKey={this.props.captchaPublicKey}
835846
onChangeCaptchaProvider={this.onChangeCaptchaProvider}
847+
onExpireCaptchaProvider={this.onExpireCaptchaProvider}
836848
handleEmitOtpAction={this.handleEmitOtpAction}
837849
forgotPasswordAction={this.props.forgotPasswordAction}
838850
loginAttempts={this.props?.loginAttempts}
@@ -870,6 +882,7 @@ class LoginPage extends React.Component {
870882
shouldShowCaptcha={this.shouldShowCaptcha}
871883
captchaPublicKey={this.props.captchaPublicKey}
872884
onChangeCaptchaProvider={this.onChangeCaptchaProvider}
885+
onExpireCaptchaProvider={this.onExpireCaptchaProvider}
873886
onReset={this.handleDelete}
874887
loginAttempts={this.props?.loginAttempts}
875888
/>

resources/js/reset_password/reset_password.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ const ResetPasswordPage = ({
178178
siteKey={captchaPublicKey}
179179
options={{ responseFieldName: "cf-turnstile-response" }}
180180
onSuccess={onChangeCaptchaProvider}
181+
onExpire={() => { captcha.current?.reset(); }}
181182
/>
182183
{captchaConfirmation && (
183184
<div className={styles.error_label}>

resources/js/set_password/set_password.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ const SetPasswordPage = ({
287287
siteKey={captchaPublicKey}
288288
options={{ responseFieldName: "cf-turnstile-response" }}
289289
onSuccess={onChangeCaptchaProvider}
290+
onExpire={() => { captcha.current?.reset(); }}
290291
/>
291292
{captchaConfirmation && (
292293
<div className={styles.error_label}>

resources/js/signup/signup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ const SignUpPage = ({
281281
siteKey={captchaPublicKey}
282282
options={{ responseFieldName: "cf-turnstile-response" }}
283283
onSuccess={onChangeCaptchaProvider}
284+
onExpire={() => { captcha.current?.reset(); }}
284285
/>
285286
{captchaConfirmation && (
286287
<div className={styles.error_label}>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php namespace Tests;
2+
/**
3+
* Copyright 2026 OpenStack Foundation
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
**/
14+
15+
use Illuminate\Support\Facades\Session;
16+
17+
/**
18+
* Class TurnstileProtectedControllersTest
19+
*
20+
* Smoke tests verifying that cf-turnstile-response is always required on the
21+
* five auth endpoints that gate every submission behind Turnstile (unlike
22+
* UserController::postLogin, which only activates the rule above a threshold).
23+
*/
24+
final class TurnstileProtectedControllersTest extends BrowserKitTestCase
25+
{
26+
protected function prepareForTests(): void
27+
{
28+
parent::prepareForTests();
29+
Session::start();
30+
}
31+
32+
private function sessionHasValidationError(string $field): bool
33+
{
34+
$errors = $this->app['session']->driver()->get('errors');
35+
return $errors !== null && $errors->has($field);
36+
}
37+
38+
private function postWithSession(string $url, array $data = []): void
39+
{
40+
$this->call('GET', $url);
41+
$this->call('POST', $url, array_merge(['_token' => Session::token()], $data));
42+
}
43+
44+
// -------------------------------------------------------------------------
45+
// RegisterController
46+
// -------------------------------------------------------------------------
47+
48+
public function testRegisterRequiresTurnstileToken(): void
49+
{
50+
$this->postWithSession('/auth/register', [
51+
'first_name' => 'Test',
52+
'last_name' => 'User',
53+
'email' => 'turnstile-test@example.com',
54+
'country_iso_code'=> 'US',
55+
'password' => 'Abcd1234!',
56+
'password_confirmation' => 'Abcd1234!',
57+
// cf-turnstile-response intentionally omitted
58+
]);
59+
60+
$this->assertTrue(
61+
$this->sessionHasValidationError('cf-turnstile-response'),
62+
'RegisterController must require cf-turnstile-response'
63+
);
64+
}
65+
66+
// -------------------------------------------------------------------------
67+
// ForgotPasswordController
68+
// -------------------------------------------------------------------------
69+
70+
public function testForgotPasswordRequiresTurnstileToken(): void
71+
{
72+
$this->postWithSession('/auth/password/email', [
73+
'email' => 'anyone@example.com',
74+
// cf-turnstile-response intentionally omitted
75+
]);
76+
77+
$this->assertTrue(
78+
$this->sessionHasValidationError('cf-turnstile-response'),
79+
'ForgotPasswordController must require cf-turnstile-response'
80+
);
81+
}
82+
83+
// -------------------------------------------------------------------------
84+
// ResetPasswordController
85+
// -------------------------------------------------------------------------
86+
87+
public function testResetPasswordRequiresTurnstileToken(): void
88+
{
89+
$this->postWithSession('/auth/password/reset', [
90+
'token' => 'any-reset-token',
91+
'password' => 'Abcd1234!',
92+
'password_confirmation' => 'Abcd1234!',
93+
// cf-turnstile-response intentionally omitted
94+
]);
95+
96+
$this->assertTrue(
97+
$this->sessionHasValidationError('cf-turnstile-response'),
98+
'ResetPasswordController must require cf-turnstile-response'
99+
);
100+
}
101+
102+
// -------------------------------------------------------------------------
103+
// PasswordSetController
104+
// -------------------------------------------------------------------------
105+
106+
public function testPasswordSetRequiresTurnstileToken(): void
107+
{
108+
$this->postWithSession('/auth/password/set', [
109+
'token' => 'any-set-token',
110+
'first_name' => 'Test',
111+
'last_name' => 'User',
112+
'country_iso_code' => 'US',
113+
'password' => 'Abcd1234!',
114+
'password_confirmation' => 'Abcd1234!',
115+
// cf-turnstile-response intentionally omitted
116+
]);
117+
118+
$this->assertTrue(
119+
$this->sessionHasValidationError('cf-turnstile-response'),
120+
'PasswordSetController must require cf-turnstile-response'
121+
);
122+
}
123+
124+
// -------------------------------------------------------------------------
125+
// EmailVerificationController
126+
// -------------------------------------------------------------------------
127+
128+
public function testEmailVerificationResendRequiresTurnstileToken(): void
129+
{
130+
$this->postWithSession('/auth/verification', [
131+
'email' => 'anyone@example.com',
132+
// cf-turnstile-response intentionally omitted
133+
]);
134+
135+
$this->assertTrue(
136+
$this->sessionHasValidationError('cf-turnstile-response'),
137+
'EmailVerificationController must require cf-turnstile-response'
138+
);
139+
}
140+
}

tests/UserLoginTurnstileTest.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
* Class UserLoginTurnstileTest
2222
*
2323
* Covers Cloudflare Turnstile integration in UserController::postLogin():
24-
* - cf-turnstile-response required when login_failed_attempt >= threshold
24+
* - cf-turnstile-response required when login_attempts (from request body) >= threshold
2525
* - threshold gating (before / at boundary)
26-
* - server-side attempt lookup for existing vs. unknown users
26+
* - unknown username defaults to zero attempts (no captcha required)
2727
* - login screen emits Turnstile JS config after a failed attempt
2828
* - expired or unsolved token is rejected
2929
*/
@@ -41,6 +41,9 @@ protected function prepareForTests(): void
4141
parent::prepareForTests();
4242
$this->testEmail = env('TEST_USER_EMAIL');
4343
$this->testPassword = env('TEST_USER_PASSWORD');
44+
if (empty($this->testEmail) || empty($this->testPassword)) {
45+
$this->markTestSkipped('TEST_USER_EMAIL and TEST_USER_PASSWORD env vars are required.');
46+
}
4447
Session::start();
4548
}
4649

webpack.common.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ module.exports = {
8787
{
8888
// Fix for webpack 5 + ESM packages (e.g. @marsidev/react-turnstile) that import
8989
// react/jsx-runtime without the .js extension, which webpack 5 requires in strict ESM mode.
90-
test: /\.m?js/,
90+
test: /\.m?js$/,
9191
include: /node_modules/,
9292
resolve: { fullySpecified: false }
9393
},

0 commit comments

Comments
 (0)