2018-10-17 14:19:58 +02:00
< ? php
2019-10-11 01:21:41 +02:00
2018-10-17 14:19:58 +02:00
/**
* @ file / src / Core / Authentication . php
*/
namespace Friendica\Core ;
2019-05-13 07:36:09 +02:00
use Friendica\App ;
2018-10-17 14:19:58 +02:00
use Friendica\BaseObject ;
2019-12-03 21:18:26 +01:00
use Friendica\Database\DBA ;
use Friendica\Model\User ;
2019-07-24 02:03:08 +02:00
use Friendica\Network\HTTPException\ForbiddenException ;
2019-12-03 21:18:26 +01:00
use Friendica\Util\DateTimeFormat ;
use Friendica\Util\Network ;
use Friendica\Util\Strings ;
2018-10-17 14:19:58 +02:00
/**
2019-10-11 01:21:41 +02:00
* Handle Authentification , Session and Cookies
*/
2018-10-17 14:19:58 +02:00
class Authentication extends BaseObject
{
2019-12-03 21:18:26 +01:00
/**
* Attempts to authenticate using OpenId
*
* @ param string $openid_url OpenID URL string
* @ param bool $remember Whether to set the session remember flag
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function openIdAuthentication ( $openid_url , $remember )
{
$noid = Config :: get ( 'system' , 'no_openid' );
$a = self :: getApp ();
// if it's an email address or doesn't resolve to a URL, fail.
if ( $noid || strpos ( $openid_url , '@' ) || ! Network :: isUrlValid ( $openid_url )) {
notice ( L10n :: t ( 'Login failed.' ) . EOL );
$a -> internalRedirect ();
// NOTREACHED
}
// Otherwise it's probably an openid.
try {
$openid = new LightOpenID ( $a -> getHostName ());
$openid -> identity = $openid_url ;
Session :: set ( 'openid' , $openid_url );
Session :: set ( 'remember' , $remember );
$openid -> returnUrl = $a -> getBaseURL ( true ) . '/openid' ;
$openid -> optional = [ 'namePerson/friendly' , 'contact/email' , 'namePerson' , 'namePerson/first' , 'media/image/aspect11' , 'media/image/default' ];
System :: externalRedirect ( $openid -> authUrl ());
} catch ( Exception $e ) {
notice ( L10n :: t ( 'We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.' ) . '<br /><br >' . L10n :: t ( 'The error message was:' ) . ' ' . $e -> getMessage ());
}
}
/**
* Attempts to authenticate using login / password
*
* @ param string $username User name
* @ param string $password Clear password
* @ param bool $remember Whether to set the session remember flag
* @ param string $openid_identity OpenID identity
* @ param string $openid_server OpenID URL
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function passwordAuthentication ( $username , $password , $remember , $openid_identity , $openid_server )
{
$record = null ;
$addon_auth = [
'username' => $username ,
'password' => $password ,
'authenticated' => 0 ,
'user_record' => null
];
$a = self :: getApp ();
/*
* 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 )) {
throw new Exception ( L10n :: t ( 'Login failed.' ));
}
} else {
$record = DBA :: selectFirst (
'user' ,
[],
[ 'uid' => User :: getIdFromPasswordAuthentication ( $username , $password )]
);
}
} catch ( Exception $e ) {
Logger :: warning ( 'authenticate: failed login attempt' , [ 'action' => 'login' , 'username' => Strings :: escapeTags ( $username ), 'ip' => $_SERVER [ 'REMOTE_ADDR' ]]);
info ( 'Login failed. Please check your credentials.' . EOL );
$a -> internalRedirect ();
}
if ( ! $remember ) {
Authentication :: setCookie ( 0 ); // 0 means delete on browser exit
}
// if we haven't failed up this point, log them in.
Session :: set ( 'remember' , $remember );
Session :: set ( 'last_login_date' , DateTimeFormat :: utcNow ());
if ( ! empty ( $openid_identity ) || ! empty ( $openid_server )) {
DBA :: update ( 'user' , [ 'openid' => $openid_identity , 'openidserver' => $openid_server ], [ 'uid' => $record [ 'uid' ]]);
}
Session :: setAuthenticatedForUser ( $a , $record , true , true );
$return_path = Session :: get ( 'return_path' , '' );
Session :: remove ( 'return_path' );
$a -> internalRedirect ( $return_path );
}
/**
* @ brief Tries to auth the user from the cookie or session
*
* @ todo Should be moved to Friendica\Core\Session when it ' s created
*/
public static function sessionAuth ()
{
$a = self :: getApp ();
// When the "Friendica" cookie is set, take the value to authenticate and renew the cookie.
if ( isset ( $_COOKIE [ " Friendica " ])) {
$data = json_decode ( $_COOKIE [ " Friendica " ]);
if ( isset ( $data -> uid )) {
$user = DBA :: selectFirst (
'user' ,
[],
[
'uid' => $data -> uid ,
'blocked' => false ,
'account_expired' => false ,
'account_removed' => false ,
'verified' => true ,
]
);
if ( DBA :: isResult ( $user )) {
if ( ! hash_equals (
Authentication :: getCookieHashForUser ( $user ),
$data -> hash
)) {
Logger :: log ( " Hash for user " . $data -> uid . " doesn't fit. " );
Authentication :: deleteSession ();
$a -> internalRedirect ();
}
// Renew the cookie
// Expires after 7 days by default,
// can be set via system.auth_cookie_lifetime
$authcookiedays = Config :: get ( 'system' , 'auth_cookie_lifetime' , 7 );
Authentication :: setCookie ( $authcookiedays * 24 * 60 * 60 , $user );
// Do the authentification if not done by now
if ( ! isset ( $_SESSION ) || ! isset ( $_SESSION [ 'authenticated' ])) {
Session :: setAuthenticatedForUser ( $a , $user );
if ( Config :: get ( 'system' , 'paranoia' )) {
$_SESSION [ 'addr' ] = $data -> ip ;
}
}
}
}
}
if ( ! empty ( $_SESSION [ 'authenticated' ])) {
if ( ! empty ( $_SESSION [ 'visitor_id' ]) && empty ( $_SESSION [ 'uid' ])) {
$contact = DBA :: selectFirst ( 'contact' , [], [ 'id' => $_SESSION [ 'visitor_id' ]]);
if ( DBA :: isResult ( $contact )) {
self :: getApp () -> contact = $contact ;
}
}
if ( ! empty ( $_SESSION [ 'uid' ])) {
// already logged in user returning
$check = Config :: get ( 'system' , 'paranoia' );
// extra paranoia - if the IP changed, log them out
if ( $check && ( $_SESSION [ 'addr' ] != $_SERVER [ 'REMOTE_ADDR' ])) {
Logger :: log ( 'Session address changed. Paranoid setting in effect, blocking session. ' .
$_SESSION [ 'addr' ] . ' != ' . $_SERVER [ 'REMOTE_ADDR' ]);
Authentication :: deleteSession ();
$a -> internalRedirect ();
}
$user = DBA :: selectFirst (
'user' ,
[],
[
'uid' => $_SESSION [ 'uid' ],
'blocked' => false ,
'account_expired' => false ,
'account_removed' => false ,
'verified' => true ,
]
);
if ( ! DBA :: isResult ( $user )) {
Authentication :: deleteSession ();
$a -> internalRedirect ();
}
// 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 ;
if ( empty ( $_SESSION [ 'last_login_date' ])) {
$_SESSION [ 'last_login_date' ] = DateTimeFormat :: utcNow ();
}
if ( strcmp ( DateTimeFormat :: utc ( 'now - 12 hours' ), $_SESSION [ 'last_login_date' ]) > 0 ) {
$_SESSION [ 'last_login_date' ] = DateTimeFormat :: utcNow ();
$login_refresh = true ;
}
Session :: setAuthenticatedForUser ( $a , $user , false , false , $login_refresh );
}
}
}
2018-10-17 14:19:58 +02:00
/**
* @ brief Calculate the hash that is needed for the " Friendica " cookie
*
* @ param array $user Record from " user " table
*
* @ return string Hashed data
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-17 14:19:58 +02:00
*/
2018-10-17 21:30:41 +02:00
public static function getCookieHashForUser ( $user )
2018-10-17 14:19:58 +02:00
{
2019-10-11 01:21:41 +02:00
return hash_hmac (
" sha256 " ,
2019-10-12 01:57:04 +02:00
hash_hmac ( " sha256 " , $user [ " password " ], $user [ " prvkey " ]),
2019-10-11 01:21:41 +02:00
Config :: get ( " system " , " site_prvkey " )
);
2018-10-17 14:19:58 +02:00
}
/**
* @ brief Set the " Friendica " cookie
*
2019-01-06 22:06:53 +01:00
* @ param int $time
2018-10-17 14:19:58 +02:00
* @ param array $user Record from " user " table
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-17 14:19:58 +02:00
*/
2018-10-17 21:30:41 +02:00
public static function setCookie ( $time , $user = [])
2018-10-17 14:19:58 +02:00
{
if ( $time != 0 ) {
$time = $time + time ();
}
if ( $user ) {
2019-10-11 01:21:41 +02:00
$value = json_encode ([
" uid " => $user [ " uid " ],
2018-10-17 21:30:41 +02:00
" hash " => self :: getCookieHashForUser ( $user ),
2019-10-16 14:35:14 +02:00
" ip " => ( $_SERVER [ 'REMOTE_ADDR' ] ? ? '' ) ? : '0.0.0.0'
2019-10-11 01:21:41 +02:00
]);
2018-10-17 14:19:58 +02:00
} else {
$value = " " ;
}
2019-08-15 17:23:35 +02:00
setcookie ( " Friendica " , $value , $time , " / " , " " , ( Config :: get ( 'system' , 'ssl_policy' ) == App\BaseURL :: SSL_POLICY_FULL ), true );
2018-10-17 14:19:58 +02:00
}
/**
* @ brief Kills the " Friendica " cookie and all session data
*/
2018-10-17 21:30:41 +02:00
public static function deleteSession ()
2018-10-17 14:19:58 +02:00
{
2018-10-17 21:30:41 +02:00
self :: setCookie ( - 3600 ); // make sure cookie is deleted on browser close, as a security measure
2018-10-17 14:19:58 +02:00
session_unset ();
session_destroy ();
}
2019-05-13 07:36:09 +02:00
public static function twoFactorCheck ( $uid , App $a )
{
// Check user setting, if 2FA disabled return
if ( ! PConfig :: get ( $uid , '2fa' , 'verified' )) {
return ;
}
// Check current path, if 2fa authentication module return
2019-07-24 02:03:08 +02:00
if ( $a -> argc > 0 && in_array ( $a -> argv [ 0 ], [ '2fa' , 'view' , 'help' , 'api' , 'proxy' , 'logout' ])) {
2019-05-13 07:36:09 +02:00
return ;
}
// Case 1: 2FA session present and valid: return
if ( Session :: get ( '2fa' )) {
return ;
}
// Case 2: No valid 2FA session: redirect to code verification page
2019-07-24 02:03:08 +02:00
if ( $a -> isAjax ()) {
throw new ForbiddenException ();
} else {
$a -> internalRedirect ( '2fa' );
}
2019-05-13 07:36:09 +02:00
}
2018-10-17 14:19:58 +02:00
}