2016-09-16 06:19:40 +02:00
< ? php
2022-12-29 19:41:57 +01:00
2017-10-21 08:40:00 +02:00
/**
* LoginController . php
2020-01-31 07:32:04 +01:00
* Copyright ( c ) 2020 james @ firefly - iii . org
2017-10-21 08:40:00 +02:00
*
2019-10-02 06:37:26 +02:00
* This file is part of Firefly III ( https :// github . com / firefly - iii ) .
2017-10-21 08:40:00 +02:00
*
2019-10-02 06:37:26 +02:00
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
2017-10-21 08:40:00 +02:00
*
2019-10-02 06:37:26 +02:00
* This program is distributed in the hope that it will be useful ,
2017-10-21 08:40:00 +02:00
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
2019-10-02 06:37:26 +02:00
* GNU Affero General Public License for more details .
2017-10-21 08:40:00 +02:00
*
2019-10-02 06:37:26 +02:00
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
2017-10-21 08:40:00 +02:00
*/
2017-09-14 17:40:02 +02:00
declare ( strict_types = 1 );
2016-09-16 06:19:40 +02:00
namespace FireflyIII\Http\Controllers\Auth ;
2025-05-24 05:52:31 +02:00
use Carbon\Carbon ;
2026-01-19 20:17:16 +01:00
use FireflyIII\Events\Security\System\UnknownUserTriedLogin ;
use FireflyIII\Events\Security\User\UserFailedLoginAttempt ;
2026-01-23 13:58:57 +01:00
use FireflyIII\Events\Security\User\UserSuccessfullyLoggedIn ;
2021-09-18 10:20:19 +02:00
use FireflyIII\Exceptions\FireflyException ;
2016-09-16 06:19:40 +02:00
use FireflyIII\Http\Controllers\Controller ;
2020-01-01 15:34:38 +01:00
use FireflyIII\Providers\RouteServiceProvider ;
2024-12-12 07:09:17 +01:00
use FireflyIII\Repositories\User\UserRepositoryInterface ;
2026-01-19 20:17:16 +01:00
use FireflyIII\Support\Facades\FireflyConfig ;
2025-12-17 19:27:59 +01:00
use FireflyIII\Support\Facades\Steam ;
2025-10-05 12:59:43 +02:00
use FireflyIII\User ;
2021-09-18 10:26:12 +02:00
use Illuminate\Contracts\Foundation\Application ;
2020-03-17 15:01:00 +01:00
use Illuminate\Contracts\View\Factory ;
2021-09-18 10:26:12 +02:00
use Illuminate\Contracts\View\View ;
2016-09-16 06:19:40 +02:00
use Illuminate\Foundation\Auth\AuthenticatesUsers ;
2021-07-23 06:26:42 +02:00
use Illuminate\Foundation\Auth\ThrottlesLogins ;
2023-11-04 11:31:14 +01:00
use Illuminate\Http\JsonResponse ;
2020-03-17 15:01:00 +01:00
use Illuminate\Http\RedirectResponse ;
2017-09-14 16:13:25 +02:00
use Illuminate\Http\Request ;
2021-09-18 10:20:19 +02:00
use Illuminate\Http\Response ;
2021-09-18 10:26:12 +02:00
use Illuminate\Routing\Redirector ;
2025-10-05 12:59:43 +02:00
use Illuminate\Support\Facades\Cookie ;
2025-02-16 19:32:50 +01:00
use Illuminate\Support\Facades\DB ;
2023-04-01 07:04:42 +02:00
use Illuminate\Support\Facades\Log ;
2023-05-29 13:56:55 +02:00
use Illuminate\Validation\ValidationException ;
2025-10-05 12:57:58 +02:00
use Psr\Container\ContainerExceptionInterface ;
use Psr\Container\NotFoundExceptionInterface ;
2025-05-24 06:19:07 +02:00
use Symfony\Component\HttpFoundation\Response as ResponseAlias ;
2017-09-14 16:13:25 +02:00
2017-12-17 14:06:14 +01:00
/**
* Class LoginController
2017-12-17 18:23:10 +01:00
*
* 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 .
2017-12-17 14:06:14 +01:00
*/
2016-09-16 06:19:40 +02:00
class LoginController extends Controller
{
2022-10-30 14:24:19 +01:00
use AuthenticatesUsers ;
use ThrottlesLogins ;
2016-09-16 06:19:40 +02:00
/**
2017-09-09 22:03:27 +02:00
* Where to redirect users after login .
2016-09-16 06:19:40 +02:00
*/
2026-01-23 15:09:50 +01:00
protected string $redirectTo = RouteServiceProvider :: HOME ;
2024-12-12 07:09:17 +01:00
private UserRepositoryInterface $repository ;
2021-06-12 07:39:16 +02:00
2026-01-23 15:14:29 +01:00
private string $username = 'email' ;
2016-09-16 07:22:57 +02:00
/**
2017-09-09 22:03:27 +02:00
* Create a new controller instance .
2016-12-28 18:49:30 +01:00
*/
2017-09-09 22:03:27 +02:00
public function __construct ()
2016-12-28 18:49:30 +01:00
{
2017-09-12 18:24:42 +02:00
parent :: __construct ();
2017-09-09 22:03:27 +02:00
$this -> middleware ( 'guest' ) -> except ( 'logout' );
2024-12-12 07:09:17 +01:00
$this -> repository = app ( UserRepositoryInterface :: class );
2016-12-28 18:49:30 +01:00
}
2021-03-28 11:46:23 +02:00
2017-11-22 20:20:57 +01:00
/**
2020-01-01 15:34:38 +01:00
* Handle a login request to the application .
2018-07-21 08:06:24 +02:00
*
2020-08-14 09:59:56 +02:00
* @ throws ValidationException
2017-11-22 20:20:57 +01:00
*/
2026-01-23 15:09:50 +01:00
public function login ( Request $request ) : JsonResponse | RedirectResponse
2017-11-22 20:20:57 +01:00
{
2024-10-10 06:17:48 +02:00
$username = $request -> get ( $this -> username ());
Log :: channel ( 'audit' ) -> info ( sprintf ( 'User is trying to login using "%s"' , $username ));
2025-11-09 09:07:14 +01:00
Log :: debug ( 'User is trying to login.' );
2024-03-09 19:46:16 +01:00
2024-03-09 08:13:53 +01:00
try {
$this -> validateLogin ( $request );
2025-05-04 13:47:00 +02:00
} catch ( ValidationException ) {
2024-10-10 06:17:48 +02:00
// basic validation exception.
// report the failed login to the user if the count is 2 or 5.
// TODO here be warning.
2026-01-23 16:18:42 +01:00
return redirect ( route ( 'login' )) -> withErrors ([ $this -> username => trans ( 'auth.failed' )]) -> onlyInput ( $this -> username );
2024-03-09 08:13:53 +01:00
}
2025-11-09 09:07:14 +01:00
Log :: debug ( 'Login data is present.' );
2017-11-22 20:20:57 +01:00
2023-12-20 19:35:52 +01:00
// Copied directly from AuthenticatesUsers, but with logging added:
2017-11-22 20:20:57 +01:00
// 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.
2023-11-05 16:55:16 +01:00
if ( $this -> hasTooManyLoginAttempts ( $request )) {
2024-01-09 20:48:17 +01:00
Log :: channel ( 'audit' ) -> warning ( sprintf ( 'Login for user "%s" was locked out.' , $request -> get ( $this -> username ())));
2025-11-09 09:07:14 +01:00
Log :: error ( sprintf ( 'Login for user "%s" was locked out.' , $request -> get ( $this -> username ())));
2017-11-22 20:20:57 +01:00
$this -> fireLockoutEvent ( $request );
2021-04-07 07:28:43 +02:00
$this -> sendLockoutResponse ( $request );
2017-11-22 20:20:57 +01:00
}
2023-12-20 19:35:52 +01:00
// Copied directly from AuthenticatesUsers, but with logging added:
2017-11-22 20:20:57 +01:00
if ( $this -> attemptLogin ( $request )) {
2021-06-20 18:33:17 +02:00
Log :: channel ( 'audit' ) -> info ( sprintf ( 'User "%s" has been logged in.' , $request -> get ( $this -> username ())));
2025-11-09 09:07:14 +01:00
Log :: debug ( sprintf ( 'Redirect after login is %s.' , $this -> redirectPath ()));
2019-03-24 14:48:12 +01:00
2020-08-29 12:10:13 +02:00
// if you just logged in, it can't be that you have a valid 2FA cookie.
2021-10-13 05:57:11 +02:00
// send a custom login event because laravel will also fire a login event if a "remember me"-cookie
// restores the event.
2026-01-23 13:58:57 +01:00
event ( new UserSuccessfullyLoggedIn ( $this -> guard () -> user ()));
2025-12-19 16:34:41 +01:00
2019-08-17 06:35:45 +02:00
return $this -> sendLoginResponse ( $request );
2017-11-22 20:20:57 +01:00
}
2025-11-09 09:07:14 +01:00
Log :: warning ( 'Login attempt failed.' );
2026-01-23 15:09:50 +01:00
$username = ( string ) $request -> get ( $this -> username ());
2024-12-14 21:55:42 +01:00
$user = $this -> repository -> findByEmail ( $username );
2025-05-24 05:40:20 +02:00
if ( ! $user instanceof User ) {
2024-12-12 07:09:17 +01:00
// send event to owner.
2026-01-19 20:17:16 +01:00
event ( new UnknownUserTriedLogin ( $username ));
2024-12-12 07:09:17 +01:00
}
2025-05-24 05:40:20 +02:00
if ( $user instanceof User ) {
2026-01-19 20:17:16 +01:00
event ( new UserFailedLoginAttempt ( $user ));
2024-12-14 07:13:01 +01:00
}
2017-11-22 20:20:57 +01:00
2023-12-20 19:35:52 +01:00
// Copied directly from AuthenticatesUsers, but with logging added:
2017-11-22 20:20:57 +01:00
// If the login attempt was unsuccessful we will increment the number of attempts
2024-06-15 12:33:56 +02:00
// to log in and redirect the user back to the login form. Of course, when this
2017-11-22 20:20:57 +01:00
// user surpasses their maximum number of attempts they will get locked out.
$this -> incrementLoginAttempts ( $request );
2024-01-09 20:48:17 +01:00
Log :: channel ( 'audit' ) -> warning ( sprintf ( 'Login failed. Attempt for user "%s" failed.' , $request -> get ( $this -> username ())));
2019-08-17 06:35:45 +02:00
2020-10-26 19:15:57 +01:00
$this -> sendFailedLoginResponse ( $request );
2023-11-04 12:10:17 +01:00
2023-12-20 19:35:52 +01:00
// @noinspection PhpUnreachableStatementInspection
2025-10-05 12:57:58 +02:00
return response () -> json ();
2017-11-22 20:20:57 +01:00
}
2017-09-14 16:13:25 +02:00
/**
2021-03-21 09:15:40 +01:00
* Log the user out of the application .
2017-09-14 16:13:25 +02:00
*/
2026-01-23 15:09:50 +01:00
public function logout ( Request $request ) : Redirector | RedirectResponse | Response
2017-09-14 16:13:25 +02:00
{
2026-01-23 15:14:29 +01:00
$authGuard = config ( 'firefly.authentication_guard' );
$logoutUrl = config ( 'firefly.custom_logout_url' );
2022-04-12 18:19:30 +02:00
if ( 'remote_user_guard' === $authGuard && '' !== $logoutUrl ) {
return redirect ( $logoutUrl );
2017-11-06 20:17:39 +01:00
}
2022-04-12 18:19:30 +02:00
if ( 'remote_user_guard' === $authGuard && '' === $logoutUrl ) {
2021-03-21 09:15:40 +01:00
session () -> flash ( 'error' , trans ( 'firefly.cant_logout_guard' ));
2017-09-14 16:13:25 +02:00
}
2021-03-21 09:15:40 +01:00
// also logout current 2FA tokens.
$cookieName = config ( 'google2fa.cookie_name' , 'google2fa_token' );
2025-05-24 05:40:20 +02:00
Cookie :: forget ( $cookieName );
2018-10-13 15:06:56 +02:00
2021-03-21 09:15:40 +01:00
$this -> guard () -> logout ();
2017-09-14 16:13:25 +02:00
2021-03-21 09:15:40 +01:00
$request -> session () -> invalidate ();
2020-05-29 06:07:03 +02:00
2021-03-21 09:15:40 +01:00
$request -> session () -> regenerateToken ();
2020-05-29 06:07:03 +02:00
2023-11-05 08:15:17 +01:00
$this -> loggedOut ( $request );
2021-03-21 09:15:40 +01:00
2026-01-23 15:09:50 +01:00
return $request -> wantsJson () ? new Response ( '' , ResponseAlias :: HTTP_NO_CONTENT ) : redirect ( '/' );
2017-09-14 16:13:25 +02:00
}
2020-01-01 15:34:38 +01:00
2020-06-11 17:55:38 +02:00
/**
2021-03-21 09:15:40 +01:00
* Show the application ' s login form .
2020-06-11 17:55:38 +02:00
*
2023-12-20 19:35:52 +01:00
* @ return Application | Factory | Redirector | RedirectResponse | View
2021-05-24 08:54:58 +02:00
*
2021-09-18 10:20:19 +02:00
* @ throws FireflyException
2025-10-05 12:57:58 +02:00
* @ throws ContainerExceptionInterface
* @ throws NotFoundExceptionInterface
2020-06-11 17:55:38 +02:00
*/
2026-01-23 15:09:50 +01:00
public function showLoginForm ( Request $request ) : Factory | Redirector | RedirectResponse | View
2020-06-11 17:55:38 +02:00
{
2026-02-15 11:25:12 +01:00
if ( 'remote_user_guard' === config ( 'auth.defaults.guard' )) {
$message = sprintf ( 'Firefly III is configured to use the "remote user guard", but was unable to link you to a user. Are you sure the "%s" header is in place?' , config ( 'auth.guard_header' ));
return view ( 'errors.error' , [ 'message' => $message ]);
}
2021-03-21 09:15:40 +01:00
Log :: channel ( 'audit' ) -> info ( 'Show login form (1.1).' );
2020-06-11 17:55:38 +02:00
2026-01-23 15:14:29 +01:00
$count = DB :: table ( 'users' ) -> count ();
$guard = config ( 'auth.defaults.guard' );
$title = ( string ) trans ( 'firefly.login_page_title' );
2021-06-12 07:39:16 +02:00
if ( 0 === $count && 'web' === $guard ) {
return redirect ( route ( 'register' ));
}
// is allowed to register, etc.
2025-12-20 07:06:47 +01:00
$singleUserMode = FireflyConfig :: get ( 'single_user_mode' , config ( 'firefly.configuration.single_user_mode' )) -> data ;
2021-03-21 09:15:40 +01:00
$allowRegistration = true ;
$allowReset = true ;
if ( true === $singleUserMode && $count > 0 ) {
$allowRegistration = false ;
}
2020-06-11 17:55:38 +02:00
2021-03-21 09:15:40 +01:00
// single user mode is ignored when the user is not using eloquent:
2021-06-12 07:39:16 +02:00
if ( 'web' !== $guard ) {
2021-03-21 09:15:40 +01:00
$allowRegistration = false ;
$allowReset = false ;
}
2020-06-11 17:55:38 +02:00
2026-01-23 15:14:29 +01:00
$email = $request -> old ( 'email' );
$remember = $request -> old ( 'remember' );
2020-06-11 17:55:38 +02:00
2026-01-23 15:14:29 +01:00
$storeInCookie = config ( 'google2fa.store_in_cookie' , false );
2021-03-21 09:15:40 +01:00
if ( false !== $storeInCookie ) {
$cookieName = config ( 'google2fa.cookie_name' , 'google2fa_token' );
2026-01-23 15:14:29 +01:00
Cookie :: queue ( Cookie :: make ( $cookieName , 'invalid-' . Carbon :: now () -> getTimestamp ()));
2020-06-11 17:55:38 +02:00
}
2026-01-23 15:14:29 +01:00
$usernameField = $this -> username ();
2021-03-28 11:46:23 +02:00
2026-01-23 15:09:50 +01:00
return view ( 'auth.login' , [
'allowRegistration' => $allowRegistration ,
'email' => $email ,
'remember' => $remember ,
'allowReset' => $allowReset ,
'title' => $title ,
2026-01-23 15:14:29 +01:00
'usernameField' => $usernameField ,
2026-01-23 15:09:50 +01:00
]);
2020-06-11 17:55:38 +02:00
}
2025-12-17 19:27:59 +01:00
2026-02-06 13:55:17 +01:00
/**
* Get the login username to be used by the controller .
*/
public function username () : string
{
return $this -> username ;
}
/**
* Get the failed login response instance .
*
* @ SuppressWarnings ( " PHPMD.UnusedFormalParameter " )
*
* @ throws ValidationException
*/
protected function sendFailedLoginResponse ( Request $request ) : void
{
2026-02-10 13:39:55 +01:00
$exception = ValidationException :: withMessages ([ $this -> username () => [ trans ( 'auth.failed' )]]);
2026-02-06 13:55:17 +01:00
$exception -> redirectTo = route ( 'login' );
throw $exception ;
}
2025-12-17 19:27:59 +01:00
/**
* Send the response after the user was authenticated .
*
2025-12-19 16:34:41 +01:00
* @ return JsonResponse | RedirectResponse
2025-12-17 19:27:59 +01:00
*/
protected function sendLoginResponse ( Request $request )
{
$request -> session () -> regenerate ();
$this -> clearLoginAttempts ( $request );
if ( $response = $this -> authenticated ( $request , $this -> guard () -> user ())) {
return $response ;
}
$path = Steam :: getSafeUrl ( session () -> pull ( 'url.intended' , route ( 'index' )), route ( 'index' ));
Log :: debug ( sprintf ( 'SafeURL is %s' , $path ));
2025-12-19 16:34:41 +01:00
2025-12-17 19:27:59 +01:00
return $request -> wantsJson () ? new JsonResponse ([], 204 ) : redirect () -> to ( $path );
}
2016-09-16 06:19:40 +02:00
}