use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\Mfa\MfaSession;
use BookStack\Auth\User;
+use BookStack\Exceptions\LoginAttemptException;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Facades\Activity;
use BookStack\Facades\Theme;
* May interrupt the flow if extra authentication requirements are imposed.
*
* @throws StoppedAuthenticationException
+ * @throws LoginAttemptException
*/
public function attempt(array $credentials, string $method, bool $remember = false): bool
{
use BookStack\Facades\Activity;
use BookStack\Uploads\UserAvatars;
use Exception;
+use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
$user = new User();
$user->name = $data['name'];
$user->email = $data['email'];
- $user->password = bcrypt(empty($data['password']) ? Str::random(32) : $data['password']);
+ $user->password = Hash::make(empty($data['password']) ? Str::random(32) : $data['password']);
$user->email_confirmed = $emailConfirmed;
$user->external_auth_id = $data['external_auth_id'] ?? '';
}
if (!empty($data['password'])) {
- $user->password = bcrypt($data['password']);
+ $user->password = Hash::make($data['password']);
}
if (!empty($data['language'])) {
class ConfirmEmailController extends Controller
{
- protected $emailConfirmationService;
- protected $loginService;
- protected $userRepo;
+ protected EmailConfirmationService $emailConfirmationService;
+ protected LoginService $loginService;
+ protected UserRepo $userRepo;
/**
* Create a new controller instance.
use BookStack\Actions\ActivityType;
use BookStack\Http\Controllers\Controller;
-use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
class ForgotPasswordController extends Controller
{
- /*
- |--------------------------------------------------------------------------
- | Password Reset Controller
- |--------------------------------------------------------------------------
- |
- | This controller is responsible for handling password reset emails and
- | includes a trait which assists in sending these notifications from
- | your application to your users. Feel free to explore this trait.
- |
- */
- use SendsPasswordResetEmails;
-
/**
* Create a new controller instance.
*
$this->middleware('guard:standard');
}
+ /**
+ * Display the form to request a password reset link.
+ */
+ public function showLinkRequestForm()
+ {
+ return view('auth.passwords.email');
+ }
+
/**
* Send a reset link to the given user.
*
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
- $response = $this->broker()->sendResetLink(
+ $response = Password::broker()->sendResetLink(
$request->only('email')
);
use BookStack\Exceptions\LoginAttemptException;
use BookStack\Facades\Activity;
use BookStack\Http\Controllers\Controller;
-use Illuminate\Foundation\Auth\AuthenticatesUsers;
+use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
- /*
- |--------------------------------------------------------------------------
- | Login Controller
- |--------------------------------------------------------------------------
- |
- | This controller handles authenticating users for the application and
- | redirecting them to your home screen. The controller uses a trait
- | to conveniently provide its functionality to your applications.
- |
- */
- use AuthenticatesUsers {
- logout as traitLogout;
- }
-
- /**
- * Redirection paths.
- */
- protected $redirectTo = '/';
- protected $redirectPath = '/';
+ use ThrottlesLogins;
protected SocialAuthService $socialAuthService;
protected LoginService $loginService;
$this->socialAuthService = $socialAuthService;
$this->loginService = $loginService;
-
- $this->redirectPath = url('/');
- }
-
- public function username()
- {
- return config('auth.method') === 'standard' ? 'email' : 'username';
- }
-
- /**
- * Get the needed authorization credentials from the request.
- */
- protected function credentials(Request $request)
- {
- return $request->only('username', 'email', 'password');
}
/**
/**
* Handle a login request to the application.
- *
- * @param \Illuminate\Http\Request $request
- *
- * @throws \Illuminate\Validation\ValidationException
- *
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function login(Request $request)
{
$this->validateLogin($request);
$username = $request->get($this->username());
- // If the class is using the ThrottlesLogins trait, we can automatically throttle
- // the login attempts for this application. We'll key this by the username and
- // the IP address of the client making these requests into this application.
- if (
- method_exists($this, 'hasTooManyLoginAttempts') &&
- $this->hasTooManyLoginAttempts($request)
- ) {
- $this->fireLockoutEvent($request);
-
+ // Check login throttling attempts to see if they've gone over the limit
+ if ($this->hasTooManyLoginAttempts($request)) {
Activity::logFailedLogin($username);
-
return $this->sendLockoutResponse($request);
}
return $this->sendLoginAttemptExceptionResponse($exception, $request);
}
- // If the login attempt was unsuccessful we will increment the number of attempts
- // to login and redirect the user back to the login form. Of course, when this
- // user surpasses their maximum number of attempts they will get locked out.
+ // On unsuccessful login attempt, Increment login attempts for throttling and log failed login.
$this->incrementLoginAttempts($request);
-
Activity::logFailedLogin($username);
- return $this->sendFailedLoginResponse($request);
+ // Throw validation failure for failed login
+ throw ValidationException::withMessages([
+ $this->username() => [trans('auth.failed')],
+ ])->redirectTo('/login');
+ }
+
+ /**
+ * Logout user and perform subsequent redirect.
+ */
+ public function logout(Request $request)
+ {
+ Auth::guard()->logout();
+ $request->session()->invalidate();
+ $request->session()->regenerateToken();
+
+ $redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
+
+ return redirect($redirectUri);
+ }
+
+ /**
+ * Get the expected username input based upon the current auth method.
+ */
+ protected function username(): string
+ {
+ return config('auth.method') === 'standard' ? 'email' : 'username';
+ }
+
+ /**
+ * Get the needed authorization credentials from the request.
+ */
+ protected function credentials(Request $request): array
+ {
+ return $request->only('username', 'email', 'password');
+ }
+
+ /**
+ * Send the response after the user was authenticated.
+ * @return RedirectResponse
+ */
+ protected function sendLoginResponse(Request $request)
+ {
+ $request->session()->regenerate();
+ $this->clearLoginAttempts($request);
+
+ return redirect()->intended('/');
}
/**
* Attempt to log the user into the application.
- *
- * @param \Illuminate\Http\Request $request
- *
- * @return bool
*/
- protected function attemptLogin(Request $request)
+ protected function attemptLogin(Request $request): bool
{
return $this->loginService->attempt(
$this->credentials($request),
);
}
- /**
- * The user has been authenticated.
- *
- * @param \Illuminate\Http\Request $request
- * @param mixed $user
- *
- * @return mixed
- */
- protected function authenticated(Request $request, $user)
- {
- return redirect()->intended($this->redirectPath());
- }
/**
* Validate the user login request.
- *
- * @param \Illuminate\Http\Request $request
- *
- * @throws \Illuminate\Validation\ValidationException
- *
- * @return void
+ * @throws ValidationException
*/
- protected function validateLogin(Request $request)
+ protected function validateLogin(Request $request): void
{
$rules = ['password' => ['required', 'string']];
$authMethod = config('auth.method');
return redirect('/login');
}
- /**
- * Get the failed login response instance.
- *
- * @param \Illuminate\Http\Request $request
- *
- * @throws \Illuminate\Validation\ValidationException
- *
- * @return \Symfony\Component\HttpFoundation\Response
- */
- protected function sendFailedLoginResponse(Request $request)
- {
- throw ValidationException::withMessages([
- $this->username() => [trans('auth.failed')],
- ])->redirectTo('/login');
- }
-
/**
* Update the intended URL location from their previous URL.
* Ignores if not from the current app instance or if from certain
return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
}
-
- /**
- * Logout user and perform subsequent redirect.
- *
- * @param \Illuminate\Http\Request $request
- *
- * @return mixed
- */
- public function logout(Request $request)
- {
- $this->traitLogout($request);
-
- $redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
-
- return redirect($redirectUri);
- }
}
use BookStack\Auth\Access\LoginService;
use BookStack\Auth\Access\RegistrationService;
use BookStack\Auth\Access\SocialAuthService;
-use BookStack\Auth\User;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Http\Controllers\Controller;
-use Illuminate\Foundation\Auth\RegistersUsers;
+use Illuminate\Contracts\Validation\Validator as ValidatorContract;
use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\Password;
class RegisterController extends Controller
{
- /*
- |--------------------------------------------------------------------------
- | Register Controller
- |--------------------------------------------------------------------------
- |
- | This controller handles the registration of new users as well as their
- | validation and creation. By default this controller uses a trait to
- | provide this functionality without requiring any additional code.
- |
- */
- use RegistersUsers;
-
protected SocialAuthService $socialAuthService;
protected RegistrationService $registrationService;
protected LoginService $loginService;
- /**
- * Where to redirect users after login / registration.
- *
- * @var string
- */
- protected $redirectTo = '/';
- protected $redirectPath = '/';
-
/**
* Create a new controller instance.
*/
$this->socialAuthService = $socialAuthService;
$this->registrationService = $registrationService;
$this->loginService = $loginService;
-
- $this->redirectTo = url('/');
- $this->redirectPath = url('/');
- }
-
- /**
- * Get a validator for an incoming registration request.
- *
- * @return \Illuminate\Contracts\Validation\Validator
- */
- protected function validator(array $data)
- {
- return Validator::make($data, [
- 'name' => ['required', 'min:2', 'max:100'],
- 'email' => ['required', 'email', 'max:255', 'unique:users'],
- 'password' => ['required', Password::default()],
- ]);
}
/**
$this->showSuccessNotification(trans('auth.register_success'));
- return redirect($this->redirectPath());
+ return redirect('/');
}
/**
- * Create a new user instance after a valid registration.
- *
- * @param array $data
- *
- * @return User
+ * Get a validator for an incoming registration request.
*/
- protected function create(array $data)
+ protected function validator(array $data): ValidatorContract
{
- return User::create([
- 'name' => $data['name'],
- 'email' => $data['email'],
- 'password' => Hash::make($data['password']),
+ return Validator::make($data, [
+ 'name' => ['required', 'min:2', 'max:100'],
+ 'email' => ['required', 'email', 'max:255', 'unique:users'],
+ 'password' => ['required', Password::default()],
]);
}
}
namespace BookStack\Http\Controllers\Auth;
use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\LoginService;
+use BookStack\Auth\User;
use BookStack\Http\Controllers\Controller;
-use Illuminate\Foundation\Auth\ResetsPasswords;
+use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
+use Illuminate\Support\Str;
+use Illuminate\Validation\Rules\Password as PasswordRule;
class ResetPasswordController extends Controller
{
- /*
- |--------------------------------------------------------------------------
- | Password Reset Controller
- |--------------------------------------------------------------------------
- |
- | This controller is responsible for handling password reset requests
- | and uses a simple trait to include this behavior. You're free to
- | explore this trait and override any methods you wish to tweak.
- |
- */
- use ResetsPasswords;
+ protected LoginService $loginService;
- protected $redirectTo = '/';
+ public function __construct(LoginService $loginService)
+ {
+ $this->middleware('guest');
+ $this->middleware('guard:standard');
+
+ $this->loginService = $loginService;
+ }
/**
- * Create a new controller instance.
- *
- * @return void
+ * Display the password reset view for the given token.
+ * If no token is present, display the link request form.
*/
- public function __construct()
+ public function showResetForm(Request $request)
{
- $this->middleware('guest');
- $this->middleware('guard:standard');
+ $token = $request->route()->parameter('token');
+
+ return view('auth.passwords.reset')->with(
+ ['token' => $token, 'email' => $request->email]
+ );
+ }
+
+ /**
+ * Reset the given user's password.
+ */
+ public function reset(Request $request)
+ {
+ $request->validate([
+ 'token' => 'required',
+ 'email' => 'required|email',
+ 'password' => ['required', 'confirmed', PasswordRule::defaults()],
+ ]);
+
+ // Here we will attempt to reset the user's password. If it is successful we
+ // will update the password on an actual user model and persist it to the
+ // database. Otherwise we will parse the error and return the response.
+ $credentials = $request->only('email', 'password', 'password_confirmation', 'token');
+ $response = Password::broker()->reset($credentials, function (User $user, string $password) {
+ $user->password = Hash::make($password);
+ $user->setRememberToken(Str::random(60));
+ $user->save();
+
+ $this->loginService->login($user, auth()->getDefaultDriver());
+ });
+
+ // If the password was successfully reset, we will redirect the user back to
+ // the application's home authenticated view. If there is an error we can
+ // redirect them back to where they came from with their error message.
+ return $response === Password::PASSWORD_RESET
+ ? $this->sendResetResponse()
+ : $this->sendResetFailedResponse($request, $response);
}
/**
* Get the response for a successful password reset.
- *
- * @param Request $request
- * @param string $response
- *
- * @return \Illuminate\Http\Response
*/
- protected function sendResetResponse(Request $request, $response)
+ protected function sendResetResponse(): RedirectResponse
{
- $message = trans('auth.reset_password_success');
- $this->showSuccessNotification($message);
+ $this->showSuccessNotification(trans('auth.reset_password_success'));
$this->logActivity(ActivityType::AUTH_PASSWORD_RESET_UPDATE, user());
- return redirect($this->redirectPath())
- ->with('status', trans($response));
+ return redirect('/');
}
/**
* Get the response for a failed password reset.
- *
- * @param \Illuminate\Http\Request $request
- * @param string $response
- *
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
- protected function sendResetFailedResponse(Request $request, $response)
+ protected function sendResetFailedResponse(Request $request, string $response): RedirectResponse
{
// We show invalid users as invalid tokens as to not leak what
// users may exist in the system.
class Saml2Controller extends Controller
{
- protected $samlService;
+ protected Saml2Service $samlService;
/**
* Saml2Controller constructor.
class SocialController extends Controller
{
- protected $socialAuthService;
- protected $registrationService;
- protected $loginService;
+ protected SocialAuthService $socialAuthService;
+ protected RegistrationService $registrationService;
+ protected LoginService $loginService;
/**
* SocialController constructor.
RegistrationService $registrationService,
LoginService $loginService
) {
- $this->middleware('guest')->only(['getRegister', 'postRegister']);
+ $this->middleware('guest')->only(['register']);
$this->socialAuthService = $socialAuthService;
$this->registrationService = $registrationService;
$this->loginService = $loginService;
--- /dev/null
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use Illuminate\Cache\RateLimiter;
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+use Illuminate\Support\Str;
+use Illuminate\Validation\ValidationException;
+
+trait ThrottlesLogins
+{
+ /**
+ * Determine if the user has too many failed login attempts.
+ */
+ protected function hasTooManyLoginAttempts(Request $request): bool
+ {
+ return $this->limiter()->tooManyAttempts(
+ $this->throttleKey($request),
+ $this->maxAttempts()
+ );
+ }
+
+ /**
+ * Increment the login attempts for the user.
+ */
+ protected function incrementLoginAttempts(Request $request): void
+ {
+ $this->limiter()->hit(
+ $this->throttleKey($request),
+ $this->decayMinutes() * 60
+ );
+ }
+
+ /**
+ * Redirect the user after determining they are locked out.
+ * @throws ValidationException
+ */
+ protected function sendLockoutResponse(Request $request): \Symfony\Component\HttpFoundation\Response
+ {
+ $seconds = $this->limiter()->availableIn(
+ $this->throttleKey($request)
+ );
+
+ throw ValidationException::withMessages([
+ $this->username() => [trans('auth.throttle', [
+ 'seconds' => $seconds,
+ 'minutes' => ceil($seconds / 60),
+ ])],
+ ])->status(Response::HTTP_TOO_MANY_REQUESTS);
+ }
+
+ /**
+ * Clear the login locks for the given user credentials.
+ */
+ protected function clearLoginAttempts(Request $request): void
+ {
+ $this->limiter()->clear($this->throttleKey($request));
+ }
+
+ /**
+ * Get the throttle key for the given request.
+ */
+ protected function throttleKey(Request $request): string
+ {
+ return Str::transliterate(Str::lower($request->input($this->username())) . '|' . $request->ip());
+ }
+
+ /**
+ * Get the rate limiter instance.
+ */
+ protected function limiter(): RateLimiter
+ {
+ return app(RateLimiter::class);
+ }
+
+ /**
+ * Get the maximum number of attempts to allow.
+ */
+ public function maxAttempts(): int
+ {
+ return 5;
+ }
+
+ /**
+ * Get the number of minutes to throttle for.
+ */
+ public function decayMinutes(): int
+ {
+ return 1;
+ }
+}
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
+use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class UserInviteController extends Controller
{
- protected $inviteService;
- protected $userRepo;
+ protected UserInviteService $inviteService;
+ protected UserRepo $userRepo;
/**
* Create a new controller instance.
}
$user = $this->userRepo->getById($userId);
- $user->password = bcrypt($request->get('password'));
+ $user->password = Hash::make($request->get('password'));
$user->email_confirmed = true;
$user->save();
"laravel/framework": "^8.68",
"laravel/socialite": "^5.2",
"laravel/tinker": "^2.6",
- "laravel/ui": "^3.3",
"league/commonmark": "^1.6",
"league/flysystem-aws-s3-v3": "^1.0.29",
"league/html-to-markdown": "^5.0.0",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "4a0d254197dda8118685ec1a1eb10edf",
+ "content-hash": "1fed6278d440ef18af1ffa6ca7b29166",
"packages": [
{
"name": "aws/aws-crt-php",
},
{
"name": "aws/aws-sdk-php",
- "version": "3.236.0",
+ "version": "3.236.1",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "bff1f1ade00c758ea27f498baee1fa16901e5bfd"
+ "reference": "1e8d1abe7582968df16a2e7a87c5dcc51d0dfd1b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bff1f1ade00c758ea27f498baee1fa16901e5bfd",
- "reference": "bff1f1ade00c758ea27f498baee1fa16901e5bfd",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1e8d1abe7582968df16a2e7a87c5dcc51d0dfd1b",
+ "reference": "1e8d1abe7582968df16a2e7a87c5dcc51d0dfd1b",
"shasum": ""
},
"require": {
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
- "source": "https://github.com/aws/aws-sdk-php/tree/3.236.0"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.236.1"
},
- "time": "2022-09-26T18:13:07+00:00"
+ "time": "2022-09-27T18:19:10+00:00"
},
{
"name": "bacon/bacon-qr-code",
},
"time": "2022-03-23T12:38:24+00:00"
},
- {
- "name": "laravel/ui",
- "version": "v3.4.6",
- "source": {
- "type": "git",
- "url": "https://github.com/laravel/ui.git",
- "reference": "65ec5c03f7fee2c8ecae785795b829a15be48c2c"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/laravel/ui/zipball/65ec5c03f7fee2c8ecae785795b829a15be48c2c",
- "reference": "65ec5c03f7fee2c8ecae785795b829a15be48c2c",
- "shasum": ""
- },
- "require": {
- "illuminate/console": "^8.42|^9.0",
- "illuminate/filesystem": "^8.42|^9.0",
- "illuminate/support": "^8.82|^9.0",
- "illuminate/validation": "^8.42|^9.0",
- "php": "^7.3|^8.0"
- },
- "require-dev": {
- "orchestra/testbench": "^6.23|^7.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.x-dev"
- },
- "laravel": {
- "providers": [
- "Laravel\\Ui\\UiServiceProvider"
- ]
- }
- },
- "autoload": {
- "psr-4": {
- "Laravel\\Ui\\": "src/",
- "Illuminate\\Foundation\\Auth\\": "auth-backend/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Taylor Otwell",
- }
- ],
- "description": "Laravel UI utilities and presets.",
- "keywords": [
- "laravel",
- "ui"
- ],
- "support": {
- "source": "https://github.com/laravel/ui/tree/v3.4.6"
- },
- "time": "2022-05-20T13:38:08+00:00"
- },
{
"name": "league/commonmark",
"version": "1.6.7",
namespace Tests\Auth;
use BookStack\Auth\Access\Mfa\MfaSession;
-use BookStack\Auth\Role;
-use BookStack\Auth\User;
use BookStack\Entities\Models\Page;
-use BookStack\Notifications\ConfirmEmail;
-use BookStack\Notifications\ResetPassword;
-use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Notification;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
->assertSee('Log in');
}
- public function test_registration_showing()
- {
- // Ensure registration form is showing
- $this->setSettings(['registration-enabled' => 'true']);
- $resp = $this->get('/login');
- $this->withHtml($resp)->assertElementContains('a[href="' . url('/register') . '"]', 'Sign up');
- }
-
- public function test_normal_registration()
- {
- // Set settings and get user instance
- /** @var Role $registrationRole */
- $registrationRole = Role::query()->first();
- $this->setSettings(['registration-enabled' => 'true', 'registration-role' => $registrationRole->id]);
- /** @var User $user */
- $user = User::factory()->make();
-
- // Test form and ensure user is created
- $resp = $this->get('/register')
- ->assertSee('Sign Up');
- $this->withHtml($resp)->assertElementContains('form[action="' . url('/register') . '"]', 'Create Account');
-
- $resp = $this->post('/register', $user->only('password', 'name', 'email'));
- $resp->assertRedirect('/');
-
- $resp = $this->get('/');
- $resp->assertOk();
- $resp->assertSee($user->name);
-
- $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]);
-
- $user = User::query()->where('email', '=', $user->email)->first();
- $this->assertEquals(1, $user->roles()->count());
- $this->assertEquals($registrationRole->id, $user->roles()->first()->id);
- }
-
- public function test_empty_registration_redirects_back_with_errors()
- {
- // Set settings and get user instance
- $this->setSettings(['registration-enabled' => 'true']);
-
- // Test form and ensure user is created
- $this->get('/register');
- $this->post('/register', [])->assertRedirect('/register');
- $this->get('/register')->assertSee('The name field is required');
- }
-
- public function test_registration_validation()
- {
- $this->setSettings(['registration-enabled' => 'true']);
-
- $this->get('/register');
- $resp = $this->followingRedirects()->post('/register', [
- 'name' => '1',
- 'email' => '1',
- 'password' => '1',
- ]);
- $resp->assertSee('The name must be at least 2 characters.');
- $resp->assertSee('The email must be a valid email address.');
- $resp->assertSee('The password must be at least 8 characters.');
- }
-
public function test_sign_up_link_on_login()
{
$this->get('/login')->assertDontSee('Sign up');
$this->get('/login')->assertSee('Sign up');
}
- public function test_confirmed_registration()
- {
- // Fake notifications
- Notification::fake();
-
- // Set settings and get user instance
- $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
- $user = User::factory()->make();
-
- // Go through registration process
- $resp = $this->post('/register', $user->only('name', 'email', 'password'));
- $resp->assertRedirect('/register/confirm');
- $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
-
- // Ensure notification sent
- /** @var User $dbUser */
- $dbUser = User::query()->where('email', '=', $user->email)->first();
- Notification::assertSentTo($dbUser, ConfirmEmail::class);
-
- // Test access and resend confirmation email
- $resp = $this->login($user->email, $user->password);
- $resp->assertRedirect('/register/confirm/awaiting');
-
- $resp = $this->get('/register/confirm/awaiting');
- $this->withHtml($resp)->assertElementContains('form[action="' . url('/register/confirm/resend') . '"]', 'Resend');
-
- $this->get('/books')->assertRedirect('/login');
- $this->post('/register/confirm/resend', $user->only('email'));
-
- // Get confirmation and confirm notification matches
- $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
- Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) {
- return $notification->token === $emailConfirmation->token;
- });
-
- // Check confirmation email confirmation activation.
- $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/login');
- $this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.');
- $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]);
- $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
- }
-
- public function test_restricted_registration()
- {
- $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
- $user = User::factory()->make();
-
- // Go through registration process
- $this->post('/register', $user->only('name', 'email', 'password'))
- ->assertRedirect('/register');
- $resp = $this->get('/register');
- $resp->assertSee('That email domain does not have access to this application');
- $this->assertDatabaseMissing('users', $user->only('email'));
-
-
- $this->post('/register', $user->only('name', 'email', 'password'))
- ->assertRedirect('/register/confirm');
- $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
-
- $this->assertNull(auth()->user());
-
- $this->get('/')->assertRedirect('/login');
- $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password'));
- $resp->assertSee('Email Address Not Confirmed');
- $this->assertNull(auth()->user());
- }
-
- public function test_restricted_registration_with_confirmation_disabled()
- {
- $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']);
- $user = User::factory()->make();
-
- // Go through registration process
- $this->post('/register', $user->only('name', 'email', 'password'))
- ->assertRedirect('/register');
- $this->assertDatabaseMissing('users', $user->only('email'));
- $this->get('/register')->assertSee('That email domain does not have access to this application');
-
-
- $this->post('/register', $user->only('name', 'email', 'password'))
- ->assertRedirect('/register/confirm');
- $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
-
- $this->assertNull(auth()->user());
-
- $this->get('/')->assertRedirect('/login');
- $resp = $this->post('/login', $user->only('email', 'password'));
- $resp->assertRedirect('/register/confirm/awaiting');
- $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed');
- $this->assertNull(auth()->user());
- }
-
- public function test_registration_role_unset_by_default()
- {
- $this->assertFalse(setting('registration-role'));
-
- $resp = $this->asAdmin()->get('/settings/registration');
- $this->withHtml($resp)->assertElementContains('select[name="setting-registration-role"] option[value="0"][selected]', '-- None --');
- }
-
public function test_logout()
{
$this->asAdmin()->get('/')->assertOk();
$this->assertFalse($mfaSession->isVerifiedForUser($user));
}
- public function test_reset_password_flow()
- {
- Notification::fake();
-
- $resp = $this->get('/login');
- $this->withHtml($resp)->assertElementContains('a[href="' . url('/password/email') . '"]', 'Forgot Password?');
-
- $resp = $this->get('/password/email');
- $this->withHtml($resp)->assertElementContains('form[action="' . url('/password/email') . '"]', 'Send Reset Link');
-
- $resp = $this->post('/password/email', [
- ]);
- $resp->assertRedirect('/password/email');
-
- $resp = $this->get('/password/email');
- $resp->assertSee('A password reset link will be sent to
[email protected] if that email address is found in the system.');
-
- $this->assertDatabaseHas('password_resets', [
- ]);
-
- /** @var User $user */
-
- Notification::assertSentTo($user, ResetPassword::class);
- $n = Notification::sent($user, ResetPassword::class);
-
- $this->get('/password/reset/' . $n->first()->token)
- ->assertOk()
- ->assertSee('Reset Password');
-
- $resp = $this->post('/password/reset', [
- 'password' => 'randompass',
- 'password_confirmation' => 'randompass',
- 'token' => $n->first()->token,
- ]);
- $resp->assertRedirect('/');
-
- $this->get('/')->assertSee('Your password has been successfully reset');
- }
-
- public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery()
- {
- $this->get('/password/email');
- $resp = $this->followingRedirects()->post('/password/email', [
- ]);
- $resp->assertSee('A password reset link will be sent to
[email protected] if that email address is found in the system.');
- $resp->assertDontSee('We can\'t find a user');
-
- $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password');
- $resp = $this->post('/password/reset', [
- 'password' => 'randompass',
- 'password_confirmation' => 'randompass',
- 'token' => 'arandometokenvalue',
- ]);
- $resp->assertRedirect('/password/reset/arandometokenvalue');
-
- $this->get('/password/reset/arandometokenvalue')
- ->assertDontSee('We can\'t find a user')
- ->assertSee('The password reset token is invalid for this email address.');
- }
-
- public function test_reset_password_page_shows_sign_links()
- {
- $this->setSettings(['registration-enabled' => 'true']);
- $resp = $this->get('/password/email');
- $this->withHtml($resp)->assertElementContains('a', 'Log in')
- ->assertElementContains('a', 'Sign up');
- }
-
- public function test_reset_password_request_is_throttled()
- {
- $editor = $this->getEditor();
- Notification::fake();
- $this->get('/password/email');
- $this->followingRedirects()->post('/password/email', [
- 'email' => $editor->email,
- ]);
-
- $resp = $this->followingRedirects()->post('/password/email', [
- 'email' => $editor->email,
- ]);
- Notification::assertTimesSent(1, ResetPassword::class);
- $resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.');
- }
-
public function test_login_redirects_to_initially_requested_url_correctly()
{
config()->set('app.url', 'http://localhost');
$this->assertFalse(auth()->check());
}
+ public function test_login_attempts_are_rate_limited()
+ {
+ for ($i = 0; $i < 5; $i++) {
+ }
+ $resp = $this->followRedirects($resp);
+ $resp->assertSee('These credentials do not match our records.');
+
+ // Check the fifth attempt provides a lockout response
+ $resp->assertSee('Too many login attempts. Please try again in');
+ }
+
/**
* Perform a login.
*/
--- /dev/null
+<?php
+
+namespace Tests\Auth;
+
+use BookStack\Auth\Role;
+use BookStack\Auth\User;
+use BookStack\Notifications\ConfirmEmail;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Notification;
+use Tests\TestCase;
+
+class RegistrationTest extends TestCase
+{
+ public function test_confirmed_registration()
+ {
+ // Fake notifications
+ Notification::fake();
+
+ // Set settings and get user instance
+ $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
+ $user = User::factory()->make();
+
+ // Go through registration process
+ $resp = $this->post('/register', $user->only('name', 'email', 'password'));
+ $resp->assertRedirect('/register/confirm');
+ $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
+
+ // Ensure notification sent
+ /** @var User $dbUser */
+ $dbUser = User::query()->where('email', '=', $user->email)->first();
+ Notification::assertSentTo($dbUser, ConfirmEmail::class);
+
+ // Test access and resend confirmation email
+ $resp = $this->post('/login', ['email' => $user->email, 'password' => $user->password]);
+ $resp->assertRedirect('/register/confirm/awaiting');
+
+ $resp = $this->get('/register/confirm/awaiting');
+ $this->withHtml($resp)->assertElementContains('form[action="' . url('/register/confirm/resend') . '"]', 'Resend');
+
+ $this->get('/books')->assertRedirect('/login');
+ $this->post('/register/confirm/resend', $user->only('email'));
+
+ // Get confirmation and confirm notification matches
+ $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
+ Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) {
+ return $notification->token === $emailConfirmation->token;
+ });
+
+ // Check confirmation email confirmation activation.
+ $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/login');
+ $this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.');
+ $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]);
+ $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
+ }
+
+ public function test_restricted_registration()
+ {
+ $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
+ $user = User::factory()->make();
+
+ // Go through registration process
+ $this->post('/register', $user->only('name', 'email', 'password'))
+ ->assertRedirect('/register');
+ $resp = $this->get('/register');
+ $resp->assertSee('That email domain does not have access to this application');
+ $this->assertDatabaseMissing('users', $user->only('email'));
+
+
+ $this->post('/register', $user->only('name', 'email', 'password'))
+ ->assertRedirect('/register/confirm');
+ $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
+
+ $this->assertNull(auth()->user());
+
+ $this->get('/')->assertRedirect('/login');
+ $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password'));
+ $resp->assertSee('Email Address Not Confirmed');
+ $this->assertNull(auth()->user());
+ }
+
+ public function test_restricted_registration_with_confirmation_disabled()
+ {
+ $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']);
+ $user = User::factory()->make();
+
+ // Go through registration process
+ $this->post('/register', $user->only('name', 'email', 'password'))
+ ->assertRedirect('/register');
+ $this->assertDatabaseMissing('users', $user->only('email'));
+ $this->get('/register')->assertSee('That email domain does not have access to this application');
+
+
+ $this->post('/register', $user->only('name', 'email', 'password'))
+ ->assertRedirect('/register/confirm');
+ $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
+
+ $this->assertNull(auth()->user());
+
+ $this->get('/')->assertRedirect('/login');
+ $resp = $this->post('/login', $user->only('email', 'password'));
+ $resp->assertRedirect('/register/confirm/awaiting');
+ $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed');
+ $this->assertNull(auth()->user());
+ }
+
+ public function test_registration_role_unset_by_default()
+ {
+ $this->assertFalse(setting('registration-role'));
+
+ $resp = $this->asAdmin()->get('/settings/registration');
+ $this->withHtml($resp)->assertElementContains('select[name="setting-registration-role"] option[value="0"][selected]', '-- None --');
+ }
+
+ public function test_registration_showing()
+ {
+ // Ensure registration form is showing
+ $this->setSettings(['registration-enabled' => 'true']);
+ $resp = $this->get('/login');
+ $this->withHtml($resp)->assertElementContains('a[href="' . url('/register') . '"]', 'Sign up');
+ }
+
+ public function test_normal_registration()
+ {
+ // Set settings and get user instance
+ /** @var Role $registrationRole */
+ $registrationRole = Role::query()->first();
+ $this->setSettings(['registration-enabled' => 'true', 'registration-role' => $registrationRole->id]);
+ /** @var User $user */
+ $user = User::factory()->make();
+
+ // Test form and ensure user is created
+ $resp = $this->get('/register')
+ ->assertSee('Sign Up');
+ $this->withHtml($resp)->assertElementContains('form[action="' . url('/register') . '"]', 'Create Account');
+
+ $resp = $this->post('/register', $user->only('password', 'name', 'email'));
+ $resp->assertRedirect('/');
+
+ $resp = $this->get('/');
+ $resp->assertOk();
+ $resp->assertSee($user->name);
+
+ $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]);
+
+ $user = User::query()->where('email', '=', $user->email)->first();
+ $this->assertEquals(1, $user->roles()->count());
+ $this->assertEquals($registrationRole->id, $user->roles()->first()->id);
+ }
+
+ public function test_empty_registration_redirects_back_with_errors()
+ {
+ // Set settings and get user instance
+ $this->setSettings(['registration-enabled' => 'true']);
+
+ // Test form and ensure user is created
+ $this->get('/register');
+ $this->post('/register', [])->assertRedirect('/register');
+ $this->get('/register')->assertSee('The name field is required');
+ }
+
+ public function test_registration_validation()
+ {
+ $this->setSettings(['registration-enabled' => 'true']);
+
+ $this->get('/register');
+ $resp = $this->followingRedirects()->post('/register', [
+ 'name' => '1',
+ 'email' => '1',
+ 'password' => '1',
+ ]);
+ $resp->assertSee('The name must be at least 2 characters.');
+ $resp->assertSee('The email must be a valid email address.');
+ $resp->assertSee('The password must be at least 8 characters.');
+ }
+}
--- /dev/null
+<?php
+
+namespace Tests\Auth;
+
+use BookStack\Auth\User;
+use BookStack\Notifications\ResetPassword;
+use Illuminate\Support\Facades\Notification;
+use Tests\TestCase;
+
+class ResetPasswordTest extends TestCase
+{
+ public function test_reset_flow()
+ {
+ Notification::fake();
+
+ $resp = $this->get('/login');
+ $this->withHtml($resp)->assertElementContains('a[href="' . url('/password/email') . '"]', 'Forgot Password?');
+
+ $resp = $this->get('/password/email');
+ $this->withHtml($resp)->assertElementContains('form[action="' . url('/password/email') . '"]', 'Send Reset Link');
+
+ $resp = $this->post('/password/email', [
+ ]);
+ $resp->assertRedirect('/password/email');
+
+ $resp = $this->get('/password/email');
+ $resp->assertSee('A password reset link will be sent to
[email protected] if that email address is found in the system.');
+
+ $this->assertDatabaseHas('password_resets', [
+ ]);
+
+ /** @var User $user */
+
+ Notification::assertSentTo($user, ResetPassword::class);
+ $n = Notification::sent($user, ResetPassword::class);
+
+ $this->get('/password/reset/' . $n->first()->token)
+ ->assertOk()
+ ->assertSee('Reset Password');
+
+ $resp = $this->post('/password/reset', [
+ 'password' => 'randompass',
+ 'password_confirmation' => 'randompass',
+ 'token' => $n->first()->token,
+ ]);
+ $resp->assertRedirect('/');
+
+ $this->get('/')->assertSee('Your password has been successfully reset');
+ }
+
+ public function test_reset_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery()
+ {
+ $this->get('/password/email');
+ $resp = $this->followingRedirects()->post('/password/email', [
+ ]);
+ $resp->assertSee('A password reset link will be sent to
[email protected] if that email address is found in the system.');
+ $resp->assertDontSee('We can\'t find a user');
+
+ $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password');
+ $resp = $this->post('/password/reset', [
+ 'password' => 'randompass',
+ 'password_confirmation' => 'randompass',
+ 'token' => 'arandometokenvalue',
+ ]);
+ $resp->assertRedirect('/password/reset/arandometokenvalue');
+
+ $this->get('/password/reset/arandometokenvalue')
+ ->assertDontSee('We can\'t find a user')
+ ->assertSee('The password reset token is invalid for this email address.');
+ }
+
+ public function test_reset_page_shows_sign_links()
+ {
+ $this->setSettings(['registration-enabled' => 'true']);
+ $resp = $this->get('/password/email');
+ $this->withHtml($resp)->assertElementContains('a', 'Log in')
+ ->assertElementContains('a', 'Sign up');
+ }
+
+ public function test_reset_request_is_throttled()
+ {
+ $editor = $this->getEditor();
+ Notification::fake();
+ $this->get('/password/email');
+ $this->followingRedirects()->post('/password/email', [
+ 'email' => $editor->email,
+ ]);
+
+ $resp = $this->followingRedirects()->post('/password/email', [
+ 'email' => $editor->email,
+ ]);
+ Notification::assertTimesSent(1, ResetPassword::class);
+ $resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.');
+ }
+}