Merge branch 'auth_review' into development
This commit is contained in:
commit
bf56254077
16 changed files with 537 additions and 555 deletions
|
@ -5,6 +5,7 @@ namespace BookStack\Auth\Access;
|
|||
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;
|
||||
|
@ -149,6 +150,7 @@ class LoginService
|
|||
* 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
|
||||
{
|
||||
|
|
|
@ -10,6 +10,7 @@ use BookStack\Exceptions\UserUpdateException;
|
|||
use BookStack\Facades\Activity;
|
||||
use BookStack\Uploads\UserAvatars;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
|
@ -61,7 +62,7 @@ class UserRepo
|
|||
$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'] ?? '';
|
||||
|
||||
|
@ -126,7 +127,7 @@ class UserRepo
|
|||
}
|
||||
|
||||
if (!empty($data['password'])) {
|
||||
$user->password = bcrypt($data['password']);
|
||||
$user->password = Hash::make($data['password']);
|
||||
}
|
||||
|
||||
if (!empty($data['language'])) {
|
||||
|
|
|
@ -14,9 +14,9 @@ use Illuminate\Http\Request;
|
|||
|
||||
class ConfirmEmailController extends Controller
|
||||
{
|
||||
protected $emailConfirmationService;
|
||||
protected $loginService;
|
||||
protected $userRepo;
|
||||
protected EmailConfirmationService $emailConfirmationService;
|
||||
protected LoginService $loginService;
|
||||
protected UserRepo $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
|
|
|
@ -4,24 +4,11 @@ namespace BookStack\Http\Controllers\Auth;
|
|||
|
||||
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.
|
||||
*
|
||||
|
@ -33,6 +20,14 @@ class ForgotPasswordController extends Controller
|
|||
$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.
|
||||
*
|
||||
|
@ -49,7 +44,7 @@ class ForgotPasswordController extends Controller
|
|||
// 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')
|
||||
);
|
||||
|
||||
|
|
|
@ -8,31 +8,14 @@ use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
|||
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;
|
||||
|
@ -48,21 +31,6 @@ class LoginController extends Controller
|
|||
|
||||
$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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,29 +66,15 @@ class LoginController extends Controller
|
|||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
|
@ -134,24 +88,62 @@ class LoginController extends Controller
|
|||
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),
|
||||
|
@ -160,29 +152,12 @@ class LoginController extends Controller
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
@ -216,22 +191,6 @@ class LoginController extends Controller
|
|||
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
|
||||
|
@ -271,20 +230,4 @@ class LoginController extends Controller
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,42 +5,20 @@ namespace BookStack\Http\Controllers\Auth;
|
|||
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.
|
||||
*/
|
||||
|
@ -55,23 +33,6 @@ class RegisterController extends Controller
|
|||
$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()],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -114,22 +75,18 @@ class RegisterController extends Controller
|
|||
|
||||
$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()],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,65 +3,87 @@
|
|||
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 = '/';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
public function __construct(LoginService $loginService)
|
||||
{
|
||||
$this->middleware('guest');
|
||||
$this->middleware('guard:standard');
|
||||
|
||||
$this->loginService = $loginService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the password reset view for the given token.
|
||||
* If no token is present, display the link request form.
|
||||
*/
|
||||
public function showResetForm(Request $request)
|
||||
{
|
||||
$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.
|
||||
|
|
|
@ -9,7 +9,7 @@ use Illuminate\Support\Str;
|
|||
|
||||
class Saml2Controller extends Controller
|
||||
{
|
||||
protected $samlService;
|
||||
protected Saml2Service $samlService;
|
||||
|
||||
/**
|
||||
* Saml2Controller constructor.
|
||||
|
|
|
@ -16,9 +16,9 @@ use Laravel\Socialite\Contracts\User as SocialUser;
|
|||
|
||||
class SocialController extends Controller
|
||||
{
|
||||
protected $socialAuthService;
|
||||
protected $registrationService;
|
||||
protected $loginService;
|
||||
protected SocialAuthService $socialAuthService;
|
||||
protected RegistrationService $registrationService;
|
||||
protected LoginService $loginService;
|
||||
|
||||
/**
|
||||
* SocialController constructor.
|
||||
|
@ -28,7 +28,7 @@ class SocialController extends Controller
|
|||
RegistrationService $registrationService,
|
||||
LoginService $loginService
|
||||
) {
|
||||
$this->middleware('guest')->only(['getRegister', 'postRegister']);
|
||||
$this->middleware('guest')->only(['register']);
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->registrationService = $registrationService;
|
||||
$this->loginService = $loginService;
|
||||
|
|
92
app/Http/Controllers/Auth/ThrottlesLogins.php
Normal file
92
app/Http/Controllers/Auth/ThrottlesLogins.php
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -11,12 +11,13 @@ use Exception;
|
|||
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.
|
||||
|
@ -66,7 +67,7 @@ class UserInviteController extends Controller
|
|||
}
|
||||
|
||||
$user = $this->userRepo->getById($userId);
|
||||
$user->password = bcrypt($request->get('password'));
|
||||
$user->password = Hash::make($request->get('password'));
|
||||
$user->email_confirmed = true;
|
||||
$user->save();
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
"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",
|
||||
|
|
75
composer.lock
generated
75
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"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",
|
||||
|
@ -58,16 +58,16 @@
|
|||
},
|
||||
{
|
||||
"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": {
|
||||
|
@ -146,9 +146,9 @@
|
|||
"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",
|
||||
|
@ -2154,67 +2154,6 @@
|
|||
},
|
||||
"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",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"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",
|
||||
|
|
|
@ -3,13 +3,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;
|
||||
|
||||
|
@ -33,68 +27,6 @@ class AuthTest extends 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');
|
||||
|
@ -104,108 +36,6 @@ class AuthTest extends TestCase
|
|||
$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'));
|
||||
|
||||
$user->email = 'barry@example.com';
|
||||
|
||||
$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');
|
||||
|
||||
$user->email = 'barry@example.com';
|
||||
|
||||
$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();
|
||||
|
@ -225,96 +55,6 @@ class AuthTest extends TestCase
|
|||
$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', [
|
||||
'email' => 'admin@admin.com',
|
||||
]);
|
||||
$resp->assertRedirect('/password/email');
|
||||
|
||||
$resp = $this->get('/password/email');
|
||||
$resp->assertSee('A password reset link will be sent to admin@admin.com if that email address is found in the system.');
|
||||
|
||||
$this->assertDatabaseHas('password_resets', [
|
||||
'email' => 'admin@admin.com',
|
||||
]);
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::query()->where('email', '=', 'admin@admin.com')->first();
|
||||
|
||||
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', [
|
||||
'email' => 'admin@admin.com',
|
||||
'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', [
|
||||
'email' => 'barry@admin.com',
|
||||
]);
|
||||
$resp->assertSee('A password reset link will be sent to barry@admin.com 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', [
|
||||
'email' => 'barry@admin.com',
|
||||
'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');
|
||||
|
@ -393,6 +133,19 @@ class AuthTest extends TestCase
|
|||
$this->assertFalse(auth()->check());
|
||||
}
|
||||
|
||||
public function test_login_attempts_are_rate_limited()
|
||||
{
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$resp = $this->login('bennynotexisting@example.com', 'pw123');
|
||||
}
|
||||
$resp = $this->followRedirects($resp);
|
||||
$resp->assertSee('These credentials do not match our records.');
|
||||
|
||||
// Check the fifth attempt provides a lockout response
|
||||
$resp = $this->followRedirects($this->login('bennynotexisting@example.com', 'pw123'));
|
||||
$resp->assertSee('Too many login attempts. Please try again in');
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a login.
|
||||
*/
|
||||
|
|
177
tests/Auth/RegistrationTest.php
Normal file
177
tests/Auth/RegistrationTest.php
Normal file
|
@ -0,0 +1,177 @@
|
|||
<?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'));
|
||||
|
||||
$user->email = 'barry@example.com';
|
||||
|
||||
$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');
|
||||
|
||||
$user->email = 'barry@example.com';
|
||||
|
||||
$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.');
|
||||
}
|
||||
}
|
101
tests/Auth/ResetPasswordTest.php
Normal file
101
tests/Auth/ResetPasswordTest.php
Normal file
|
@ -0,0 +1,101 @@
|
|||
<?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', [
|
||||
'email' => 'admin@admin.com',
|
||||
]);
|
||||
$resp->assertRedirect('/password/email');
|
||||
|
||||
$resp = $this->get('/password/email');
|
||||
$resp->assertSee('A password reset link will be sent to admin@admin.com if that email address is found in the system.');
|
||||
|
||||
$this->assertDatabaseHas('password_resets', [
|
||||
'email' => 'admin@admin.com',
|
||||
]);
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::query()->where('email', '=', 'admin@admin.com')->first();
|
||||
|
||||
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', [
|
||||
'email' => 'admin@admin.com',
|
||||
'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', [
|
||||
'email' => 'barry@admin.com',
|
||||
]);
|
||||
$resp->assertSee('A password reset link will be sent to barry@admin.com 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', [
|
||||
'email' => 'barry@admin.com',
|
||||
'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.');
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue