2018-10-17 08:19:58 -04:00
< ? php
/**
2020-02-08 11:16:42 -05:00
* @ copyright Copyright ( C ) 2020 , Friendica
*
* @ license GNU AGPL version 3 or any later version
*
* 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 .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* 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 />.
*
2018-10-17 08:19:58 -04:00
*/
2020-09-30 10:53:18 -04:00
namespace Friendica\Security ;
2018-10-17 08:19:58 -04:00
2019-12-03 16:29:37 -05:00
use Exception ;
2019-05-13 01:36:09 -04:00
use Friendica\App ;
2020-01-19 15:29:36 -05:00
use Friendica\Core\Config\IConfig ;
2020-01-19 16:23:44 -05:00
use Friendica\Core\PConfig\IPConfig ;
2019-12-08 16:45:34 -05:00
use Friendica\Core\Hook ;
use Friendica\Core\Session ;
use Friendica\Core\System ;
2019-12-03 16:29:37 -05:00
use Friendica\Database\Database ;
2019-12-03 15:18:26 -05:00
use Friendica\Database\DBA ;
2019-12-15 19:35:26 -05:00
use Friendica\DI ;
2019-12-03 15:18:26 -05:00
use Friendica\Model\User ;
2019-12-03 16:29:37 -05:00
use Friendica\Network\HTTPException ;
2019-12-03 15:18:26 -05:00
use Friendica\Util\DateTimeFormat ;
use Friendica\Util\Network ;
use Friendica\Util\Strings ;
2019-12-03 16:29:37 -05:00
use LightOpenID ;
2020-01-18 14:59:39 -05:00
use Friendica\Core\L10n ;
2019-12-03 16:29:37 -05:00
use Psr\Log\LoggerInterface ;
2018-10-17 08:19:58 -04:00
/**
2020-09-30 10:53:18 -04:00
* Handle Authentication , Session and Cookies
2019-10-10 19:21:41 -04:00
*/
2019-12-03 16:29:37 -05:00
class Authentication
2018-10-17 08:19:58 -04:00
{
2020-01-19 15:29:36 -05:00
/** @var IConfig */
2019-12-03 16:29:37 -05:00
private $config ;
2019-12-15 18:30:39 -05:00
/** @var App\Mode */
private $mode ;
2019-12-03 16:29:37 -05:00
/** @var App\BaseURL */
private $baseUrl ;
/** @var L10n */
private $l10n ;
/** @var Database */
private $dba ;
/** @var LoggerInterface */
private $logger ;
2019-12-08 16:45:34 -05:00
/** @var User\Cookie */
private $cookie ;
2019-12-09 18:44:56 -05:00
/** @var Session\ISession */
private $session ;
2020-01-19 16:23:44 -05:00
/** @var IPConfig */
2020-01-18 10:50:57 -05:00
private $pConfig ;
2019-12-03 16:29:37 -05:00
/**
* Authentication constructor .
*
2020-01-19 15:29:36 -05:00
* @ param IConfig $config
* @ param App\Mode $mode
* @ param App\BaseURL $baseUrl
* @ param L10n $l10n
* @ param Database $dba
* @ param LoggerInterface $logger
* @ param User\Cookie $cookie
2019-12-09 18:44:56 -05:00
* @ param Session\ISession $session
2020-01-19 16:23:44 -05:00
* @ param IPConfig $pConfig
2019-12-03 16:29:37 -05:00
*/
2020-01-19 16:23:44 -05:00
public function __construct ( IConfig $config , App\Mode $mode , App\BaseURL $baseUrl , L10n $l10n , Database $dba , LoggerInterface $logger , User\Cookie $cookie , Session\ISession $session , IPConfig $pConfig )
2019-12-03 16:29:37 -05:00
{
$this -> config = $config ;
2020-01-18 10:50:57 -05:00
$this -> mode = $mode ;
2019-12-03 16:29:37 -05:00
$this -> baseUrl = $baseUrl ;
$this -> l10n = $l10n ;
$this -> dba = $dba ;
$this -> logger = $logger ;
2020-01-18 10:50:57 -05:00
$this -> cookie = $cookie ;
2019-12-09 18:44:56 -05:00
$this -> session = $session ;
2020-01-18 10:50:57 -05:00
$this -> pConfig = $pConfig ;
2019-12-03 16:29:37 -05:00
}
/**
2020-01-19 01:05:23 -05:00
* Tries to auth the user from the cookie or session
2019-12-03 16:29:37 -05:00
*
* @ param App $a The Friendica Application context
*
* @ throws HttpException\InternalServerErrorException In case of Friendica internal exceptions
* @ throws Exception In case of general exceptions ( like SQL Grammar )
*/
2019-12-08 16:45:34 -05:00
public function withSession ( App $a )
2019-12-03 16:29:37 -05:00
{
2019-12-08 16:45:34 -05:00
$data = $this -> cookie -> getData ();
2019-12-03 16:29:37 -05:00
2019-12-08 16:45:34 -05:00
// When the "Friendica" cookie is set, take the value to authenticate and renew the cookie.
2019-12-09 16:47:08 -05:00
if ( isset ( $data -> uid )) {
2019-12-08 16:45:34 -05:00
$user = $this -> dba -> selectFirst (
'user' ,
[],
[
'uid' => $data -> uid ,
'blocked' => false ,
'account_expired' => false ,
'account_removed' => false ,
'verified' => true ,
]
);
2019-12-09 18:44:56 -05:00
if ( $this -> dba -> isResult ( $user )) {
2019-12-08 16:45:34 -05:00
if ( ! $this -> cookie -> check ( $data -> hash ,
$user [ 'password' ] ? ? '' ,
2019-12-20 17:15:21 -05:00
$user [ 'prvkey' ] ? ? '' )) {
2019-12-08 16:45:34 -05:00
$this -> logger -> notice ( " Hash doesn't fit. " , [ 'user' => $data -> uid ]);
2019-12-29 21:52:56 -05:00
$this -> session -> clear ();
2020-01-08 19:51:54 -05:00
$this -> cookie -> clear ();
2019-12-08 16:45:34 -05:00
$this -> baseUrl -> redirect ();
}
2019-12-03 16:29:37 -05:00
2019-12-08 16:45:34 -05:00
// Renew the cookie
2019-12-20 17:15:21 -05:00
$this -> cookie -> set ( $user [ 'uid' ], $user [ 'password' ], $user [ 'prvkey' ]);
2019-12-03 16:29:37 -05:00
2019-12-08 16:45:34 -05:00
// Do the authentification if not done by now
2019-12-09 18:44:56 -05:00
if ( ! $this -> session -> get ( 'authenticated' )) {
2019-12-08 16:45:34 -05:00
$this -> setForUser ( $a , $user );
2019-12-03 16:29:37 -05:00
2019-12-08 16:45:34 -05:00
if ( $this -> config -> get ( 'system' , 'paranoia' )) {
2019-12-09 18:44:56 -05:00
$this -> session -> set ( 'addr' , $data -> ip );
2019-12-03 16:29:37 -05:00
}
}
}
}
2019-12-09 18:44:56 -05:00
if ( $this -> session -> get ( 'authenticated' )) {
if ( $this -> session -> get ( 'visitor_id' ) && ! $this -> session -> get ( 'uid' )) {
$contact = $this -> dba -> selectFirst ( 'contact' , [], [ 'id' => $this -> session -> get ( 'visitor_id' )]);
2019-12-03 16:29:37 -05:00
if ( $this -> dba -> isResult ( $contact )) {
$a -> contact = $contact ;
}
}
2019-12-09 18:44:56 -05:00
if ( $this -> session -> get ( 'uid' )) {
2019-12-03 16:29:37 -05:00
// already logged in user returning
$check = $this -> config -> get ( 'system' , 'paranoia' );
// extra paranoia - if the IP changed, log them out
2019-12-09 18:44:56 -05:00
if ( $check && ( $this -> session -> get ( 'addr' ) != $_SERVER [ 'REMOTE_ADDR' ])) {
2019-12-03 16:29:37 -05:00
$this -> logger -> notice ( 'Session address changed. Paranoid setting in effect, blocking session. ' , [
2019-12-09 18:44:56 -05:00
'addr' => $this -> session -> get ( 'addr' ),
2019-12-03 16:29:37 -05:00
'remote_addr' => $_SERVER [ 'REMOTE_ADDR' ]]
);
2019-12-29 21:52:56 -05:00
$this -> session -> clear ();
2019-12-03 16:29:37 -05:00
$this -> baseUrl -> redirect ();
}
$user = $this -> dba -> selectFirst (
'user' ,
[],
[
2019-12-09 18:44:56 -05:00
'uid' => $this -> session -> get ( 'uid' ),
2019-12-03 16:29:37 -05:00
'blocked' => false ,
'account_expired' => false ,
'account_removed' => false ,
'verified' => true ,
]
);
if ( ! $this -> dba -> isResult ( $user )) {
2019-12-29 21:52:56 -05:00
$this -> session -> clear ();
2019-12-03 16:29:37 -05:00
$this -> baseUrl -> redirect ();
}
// Make sure to refresh the last login time for the user if the user
// stays logged in for a long time, e.g. with "Remember Me"
$login_refresh = false ;
2019-12-09 18:44:56 -05:00
if ( ! $this -> session -> get ( 'last_login_date' )) {
$this -> session -> set ( 'last_login_date' , DateTimeFormat :: utcNow ());
2019-12-03 16:29:37 -05:00
}
2019-12-09 18:44:56 -05:00
if ( strcmp ( DateTimeFormat :: utc ( 'now - 12 hours' ), $this -> session -> get ( 'last_login_date' )) > 0 ) {
$this -> session -> set ( 'last_login_date' , DateTimeFormat :: utcNow ());
2019-12-03 16:29:37 -05:00
$login_refresh = true ;
}
$this -> setForUser ( $a , $user , false , false , $login_refresh );
}
}
}
2019-12-03 15:18:26 -05:00
/**
* Attempts to authenticate using OpenId
*
* @ param string $openid_url OpenID URL string
* @ param bool $remember Whether to set the session remember flag
2019-12-03 16:29:37 -05:00
*
* @ throws HttpException\InternalServerErrorException In case of Friendica internal exceptions
2019-12-03 15:18:26 -05:00
*/
2019-12-03 16:29:37 -05:00
public function withOpenId ( string $openid_url , bool $remember )
2019-12-03 15:18:26 -05:00
{
2019-12-03 16:29:37 -05:00
$noid = $this -> config -> get ( 'system' , 'no_openid' );
2019-12-03 15:18:26 -05:00
// if it's an email address or doesn't resolve to a URL, fail.
if ( $noid || strpos ( $openid_url , '@' ) || ! Network :: isUrlValid ( $openid_url )) {
2020-07-23 02:25:01 -04:00
notice ( $this -> l10n -> t ( 'Login failed.' ));
2019-12-03 16:29:37 -05:00
$this -> baseUrl -> redirect ();
2019-12-03 15:18:26 -05:00
}
// Otherwise it's probably an openid.
try {
2019-12-03 16:29:37 -05:00
$openid = new LightOpenID ( $this -> baseUrl -> getHostname ());
2019-12-03 15:18:26 -05:00
$openid -> identity = $openid_url ;
2019-12-09 18:44:56 -05:00
$this -> session -> set ( 'openid' , $openid_url );
$this -> session -> set ( 'remember' , $remember );
2019-12-03 16:29:37 -05:00
$openid -> returnUrl = $this -> baseUrl -> get ( true ) . '/openid' ;
$openid -> optional = [ 'namePerson/friendly' , 'contact/email' , 'namePerson' , 'namePerson/first' , 'media/image/aspect11' , 'media/image/default' ];
2019-12-03 15:18:26 -05:00
System :: externalRedirect ( $openid -> authUrl ());
} catch ( Exception $e ) {
2019-12-03 16:29:37 -05:00
notice ( $this -> l10n -> t ( 'We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.' ) . '<br /><br >' . $this -> l10n -> t ( 'The error message was:' ) . ' ' . $e -> getMessage ());
2019-12-03 15:18:26 -05:00
}
}
/**
* Attempts to authenticate using login / password
*
2019-12-03 16:29:37 -05:00
* @ param App $a The Friendica Application context
* @ param string $username User name
* @ param string $password Clear password
* @ param bool $remember Whether to set the session remember flag
*
* @ throws HttpException\InternalServerErrorException In case of Friendica internal exceptions
* @ throws Exception A general Exception ( like SQL Grammar exceptions )
2019-12-03 15:18:26 -05:00
*/
2019-12-03 16:29:37 -05:00
public function withPassword ( App $a , string $username , string $password , bool $remember )
2019-12-03 15:18:26 -05:00
{
$record = null ;
$addon_auth = [
2019-12-03 16:29:37 -05:00
'username' => $username ,
'password' => $password ,
2019-12-03 15:18:26 -05:00
'authenticated' => 0 ,
2019-12-03 16:29:37 -05:00
'user_record' => null
2019-12-03 15:18:26 -05:00
];
/*
* An addon indicates successful login by setting 'authenticated' to non - zero value and returning a user record
* Addons should never set 'authenticated' except to indicate success - as hooks may be chained
* and later addons should not interfere with an earlier one that succeeded .
*/
Hook :: callAll ( 'authenticate' , $addon_auth );
try {
if ( $addon_auth [ 'authenticated' ]) {
$record = $addon_auth [ 'user_record' ];
if ( empty ( $record )) {
2019-12-03 16:29:37 -05:00
throw new Exception ( $this -> l10n -> t ( 'Login failed.' ));
2019-12-03 15:18:26 -05:00
}
} else {
2019-12-03 16:29:37 -05:00
$record = $this -> dba -> selectFirst (
2019-12-03 15:18:26 -05:00
'user' ,
[],
[ 'uid' => User :: getIdFromPasswordAuthentication ( $username , $password )]
);
}
} catch ( Exception $e ) {
2019-12-03 16:29:37 -05:00
$this -> logger -> warning ( 'authenticate: failed login attempt' , [ 'action' => 'login' , 'username' => Strings :: escapeTags ( $username ), 'ip' => $_SERVER [ 'REMOTE_ADDR' ]]);
2020-07-23 02:25:01 -04:00
notice ( $this -> l10n -> t ( 'Login failed. Please check your credentials.' ));
2019-12-03 16:29:37 -05:00
$this -> baseUrl -> redirect ();
2019-12-03 15:18:26 -05:00
}
if ( ! $remember ) {
2019-12-08 16:45:34 -05:00
$this -> cookie -> clear ();
2019-12-03 15:18:26 -05:00
}
// if we haven't failed up this point, log them in.
2019-12-09 18:44:56 -05:00
$this -> session -> set ( 'remember' , $remember );
$this -> session -> set ( 'last_login_date' , DateTimeFormat :: utcNow ());
2019-12-03 15:18:26 -05:00
2019-12-09 18:44:56 -05:00
$openid_identity = $this -> session -> get ( 'openid_identity' );
$openid_server = $this -> session -> get ( 'openid_server' );
2019-12-03 16:29:37 -05:00
2019-12-03 15:18:26 -05:00
if ( ! empty ( $openid_identity ) || ! empty ( $openid_server )) {
2019-12-03 16:29:37 -05:00
$this -> dba -> update ( 'user' , [ 'openid' => $openid_identity , 'openidserver' => $openid_server ], [ 'uid' => $record [ 'uid' ]]);
2019-12-03 15:18:26 -05:00
}
2019-12-03 16:29:37 -05:00
$this -> setForUser ( $a , $record , true , true );
2019-12-03 15:18:26 -05:00
2019-12-09 18:44:56 -05:00
$return_path = $this -> session -> get ( 'return_path' , '' );
$this -> session -> remove ( 'return_path' );
2019-12-03 15:18:26 -05:00
2019-12-03 16:29:37 -05:00
$this -> baseUrl -> redirect ( $return_path );
2019-12-03 15:18:26 -05:00
}
/**
2020-01-19 01:05:23 -05:00
* Sets the provided user ' s authenticated session
2019-12-03 16:29:37 -05:00
*
* @ param App $a The Friendica application context
* @ param array $user_record The current " user " record
* @ param bool $login_initial
* @ param bool $interactive
* @ param bool $login_refresh
2019-12-03 15:18:26 -05:00
*
2019-12-03 16:29:37 -05:00
* @ throws HTTPException\InternalServerErrorException In case of Friendica specific exceptions
* @ throws Exception In case of general Exceptions ( like SQL Grammar exceptions )
2019-12-03 15:18:26 -05:00
*/
2019-12-03 16:29:37 -05:00
public function setForUser ( App $a , array $user_record , bool $login_initial = false , bool $interactive = false , bool $login_refresh = false )
2019-12-03 15:18:26 -05:00
{
2019-12-09 18:44:56 -05:00
$this -> session -> setMultiple ([
2019-12-03 16:29:37 -05:00
'uid' => $user_record [ 'uid' ],
'theme' => $user_record [ 'theme' ],
2020-01-18 10:50:57 -05:00
'mobile-theme' => $this -> pConfig -> get ( $user_record [ 'uid' ], 'system' , 'mobile_theme' ),
2019-12-03 16:29:37 -05:00
'authenticated' => 1 ,
'page_flags' => $user_record [ 'page-flags' ],
'my_url' => $this -> baseUrl -> get () . '/profile/' . $user_record [ 'nickname' ],
'my_address' => $user_record [ 'nickname' ] . '@' . substr ( $this -> baseUrl -> get (), strpos ( $this -> baseUrl -> get (), '://' ) + 3 ),
'addr' => ( $_SERVER [ 'REMOTE_ADDR' ] ? ? '' ) ? : '0.0.0.0'
]);
Session :: setVisitorsContacts ();
$member_since = strtotime ( $user_record [ 'register_date' ]);
2019-12-09 18:44:56 -05:00
$this -> session -> set ( 'new_member' , time () < ( $member_since + ( 60 * 60 * 24 * 14 )));
2019-12-03 16:29:37 -05:00
if ( strlen ( $user_record [ 'timezone' ])) {
date_default_timezone_set ( $user_record [ 'timezone' ]);
$a -> timezone = $user_record [ 'timezone' ];
}
2019-12-03 15:18:26 -05:00
2019-12-03 16:29:37 -05:00
$masterUid = $user_record [ 'uid' ];
2019-12-03 15:18:26 -05:00
2019-12-09 18:44:56 -05:00
if ( $this -> session -> get ( 'submanage' )) {
$user = $this -> dba -> selectFirst ( 'user' , [ 'uid' ], [ 'uid' => $this -> session -> get ( 'submanage' )]);
2019-12-03 16:29:37 -05:00
if ( $this -> dba -> isResult ( $user )) {
$masterUid = $user [ 'uid' ];
}
}
2019-12-03 15:18:26 -05:00
2019-12-03 16:29:37 -05:00
$a -> identities = User :: identities ( $masterUid );
2019-12-03 15:18:26 -05:00
2019-12-03 16:29:37 -05:00
if ( $login_initial ) {
$this -> logger -> info ( 'auth_identities: ' . print_r ( $a -> identities , true ));
}
2019-12-03 15:18:26 -05:00
2019-12-03 16:29:37 -05:00
if ( $login_refresh ) {
$this -> logger -> info ( 'auth_identities refresh: ' . print_r ( $a -> identities , true ));
2019-12-03 15:18:26 -05:00
}
2019-12-03 16:29:37 -05:00
$contact = $this -> dba -> selectFirst ( 'contact' , [], [ 'uid' => $user_record [ 'uid' ], 'self' => true ]);
if ( $this -> dba -> isResult ( $contact )) {
$a -> contact = $contact ;
$a -> cid = $contact [ 'id' ];
2019-12-09 18:44:56 -05:00
$this -> session -> set ( 'cid' , $a -> cid );
2019-12-03 16:29:37 -05:00
}
2019-12-03 15:18:26 -05:00
2019-12-03 16:29:37 -05:00
header ( 'X-Account-Management-Status: active; name="' . $user_record [ 'username' ] . '"; id="' . $user_record [ 'nickname' ] . '"' );
2019-12-03 15:18:26 -05:00
2019-12-03 16:29:37 -05:00
if ( $login_initial || $login_refresh ) {
$this -> dba -> update ( 'user' , [ 'login_date' => DateTimeFormat :: utcNow ()], [ 'uid' => $user_record [ 'uid' ]]);
2019-12-03 15:18:26 -05:00
2019-12-03 16:29:37 -05:00
// Set the login date for all identities of the user
$this -> dba -> update ( 'user' , [ 'login_date' => DateTimeFormat :: utcNow ()],
[ 'parent-uid' => $masterUid , 'account_removed' => false ]);
}
2019-12-03 15:18:26 -05:00
2019-12-03 16:29:37 -05:00
if ( $login_initial ) {
/*
* If the user specified to remember the authentication , then set a cookie
* that expires after one week ( the default is when the browser is closed ) .
* The cookie will be renewed automatically .
* The week ensures that sessions will expire after some inactivity .
2020-09-30 10:53:18 -04:00
*/
2019-12-09 18:44:56 -05:00
if ( $this -> session -> get ( 'remember' )) {
2019-12-15 17:46:56 -05:00
$this -> logger -> info ( 'Injecting cookie for remembered user ' . $user_record [ 'nickname' ]);
2019-12-20 17:15:21 -05:00
$this -> cookie -> set ( $user_record [ 'uid' ], $user_record [ 'password' ], $user_record [ 'prvkey' ]);
2019-12-09 18:44:56 -05:00
$this -> session -> remove ( 'remember' );
2019-12-03 15:18:26 -05:00
}
}
2019-12-03 16:29:37 -05:00
$this -> twoFactorCheck ( $user_record [ 'uid' ], $a );
2018-10-17 08:19:58 -04:00
2019-12-03 16:29:37 -05:00
if ( $interactive ) {
if ( $user_record [ 'login_date' ] <= DBA :: NULL_DATETIME ) {
info ( $this -> l10n -> t ( 'Welcome %s' , $user_record [ 'username' ]));
info ( $this -> l10n -> t ( 'Please upload a profile photo.' ));
2019-10-27 09:56:27 -04:00
$this -> baseUrl -> redirect ( 'settings/profile/photo/new' );
2019-12-03 16:29:37 -05:00
}
2018-10-17 08:19:58 -04:00
}
2019-12-03 16:29:37 -05:00
$a -> user = $user_record ;
if ( $login_initial ) {
Hook :: callAll ( 'logged_in' , $a -> user );
2018-10-17 08:19:58 -04:00
2019-12-15 19:35:26 -05:00
if ( DI :: module () -> getName () !== 'home' && $this -> session -> exists ( 'return_path' )) {
2019-12-09 18:44:56 -05:00
$this -> baseUrl -> redirect ( $this -> session -> get ( 'return_path' ));
2019-12-03 16:29:37 -05:00
}
}
2018-10-17 08:19:58 -04:00
}
/**
2019-12-03 16:29:37 -05:00
* @ param int $uid The User Identified
* @ param App $a The Friendica Application context
*
* @ throws HTTPException\ForbiddenException In case the two factor authentication is forbidden ( e . g . for AJAX calls )
2018-10-17 08:19:58 -04:00
*/
2019-12-03 16:29:37 -05:00
private function twoFactorCheck ( int $uid , App $a )
2019-05-13 01:36:09 -04:00
{
// Check user setting, if 2FA disabled return
2020-01-18 10:50:57 -05:00
if ( ! $this -> pConfig -> get ( $uid , '2fa' , 'verified' )) {
2019-05-13 01:36:09 -04:00
return ;
}
// Check current path, if 2fa authentication module return
2019-07-23 20:03:08 -04:00
if ( $a -> argc > 0 && in_array ( $a -> argv [ 0 ], [ '2fa' , 'view' , 'help' , 'api' , 'proxy' , 'logout' ])) {
2019-05-13 01:36:09 -04:00
return ;
}
// Case 1: 2FA session present and valid: return
2019-12-09 18:44:56 -05:00
if ( $this -> session -> get ( '2fa' )) {
2019-05-13 01:36:09 -04:00
return ;
}
// Case 2: No valid 2FA session: redirect to code verification page
2019-12-15 18:30:39 -05:00
if ( $this -> mode -> isAjax ()) {
2019-12-03 16:29:37 -05:00
throw new HTTPException\ForbiddenException ();
2019-07-23 20:03:08 -04:00
} else {
2019-12-15 18:28:31 -05:00
$this -> baseUrl -> redirect ( '2fa' );
2019-07-23 20:03:08 -04:00
}
2019-05-13 01:36:09 -04:00
}
2018-10-17 08:19:58 -04:00
}