OpenWebAuth moved to a separate class / Improved authentication handling

This commit is contained in:
Michael 2024-05-27 04:33:28 +00:00
parent b3c7e96b73
commit 55cec6c61d
14 changed files with 595 additions and 410 deletions

View file

@ -34,12 +34,12 @@ use Friendica\DI;
use Friendica\Model\User;
use Friendica\Network\HTTPException;
use Friendica\Security\TwoFactor\Repository\TrustedBrowser;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network;
use LightOpenID;
use Friendica\Core\L10n;
use Friendica\Core\Worker;
use Friendica\Model\Contact;
use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
/**
@ -146,7 +146,7 @@ class Authentication
$this->cookie->send();
// Do the authentication if not done by now
if (!$this->session->get('authenticated')) {
if (!$this->session->isAuthenticated()) {
$this->setForUser($a, $user);
if ($this->config->get('system', 'paranoia')) {
@ -156,46 +156,44 @@ class Authentication
}
}
if ($this->session->get('authenticated')) {
if ($this->session->get('visitor_id') && !$this->session->get('uid')) {
$contact = $this->dba->selectFirst('contact', ['id'], ['id' => $this->session->get('visitor_id')]);
if ($this->dba->isResult($contact)) {
$a->setContactId($contact['id']);
}
if ($this->session->isVisitor()) {
$contact = $this->dba->selectFirst('contact', ['id'], ['id' => $this->session->get('visitor_id')]);
if ($this->dba->isResult($contact)) {
$a->setContactId($contact['id']);
}
}
if ($this->session->get('uid')) {
// already logged in user returning
$check = $this->config->get('system', 'paranoia');
// extra paranoia - if the IP changed, log them out
if ($check && ($this->session->get('addr') != $this->remoteAddress)) {
$this->logger->notice('Session address changed. Paranoid setting in effect, blocking session. ', [
'addr' => $this->session->get('addr'),
'remote_addr' => $this->remoteAddress
]
);
$this->session->clear();
$this->baseUrl->redirect();
}
$user = $this->dba->selectFirst(
'user',
[],
[
'uid' => $this->session->get('uid'),
'blocked' => false,
'account_expired' => false,
'account_removed' => false,
'verified' => true,
]
if ($this->session->isAuthenticated()) {
// already logged in user returning
$check = $this->config->get('system', 'paranoia');
// extra paranoia - if the IP changed, log them out
if ($check && ($this->session->get('addr') != $this->remoteAddress)) {
$this->logger->notice('Session address changed. Paranoid setting in effect, blocking session. ', [
'addr' => $this->session->get('addr'),
'remote_addr' => $this->remoteAddress
]
);
if (!$this->dba->isResult($user)) {
$this->session->clear();
$this->baseUrl->redirect();
}
$this->setForUser($a, $user);
$this->session->clear();
$this->baseUrl->redirect();
}
$user = $this->dba->selectFirst(
'user',
[],
[
'uid' => $this->session->get('uid'),
'blocked' => false,
'account_expired' => false,
'account_removed' => false,
'verified' => true,
]
);
if (!$this->dba->isResult($user)) {
$this->session->clear();
$this->baseUrl->redirect();
}
$this->setForUser($a, $user);
}
}
@ -446,4 +444,25 @@ class Authentication
$this->baseUrl->redirect('2fa');
}
}
/**
* Set the URL of an unauthenticated visitor
*
* @param string $url
* @return void
*/
public function setUnauthenticatedVisitor(string $url)
{
if (Strings::compareLink($this->session->get('visitor_home'), $url)) {
return;
}
$this->session->set('my_url', $url);
$this->session->set('authenticated', 0);
$remote_contact = Contact::getByURL($url, false, ['subscribe']);
if (!empty($remote_contact['subscribe'])) {
$this->session->set('remote_comment', $remote_contact['subscribe']);
}
}
}

View file

@ -0,0 +1,252 @@
<?php
/**
* @copyright Copyright (C) 2010-2024, the Friendica project
*
* @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/>.
*
*/
namespace Friendica\Security;
use Friendica\Core\Cache\Enum\Duration;
use Friendica\Core\Hook;
use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\OpenWebAuthToken;
use Friendica\Util\HTTPSignature;
use Friendica\Util\Network;
use Friendica\Util\Strings;
/**
* Authentication via OpenWebAuth
*/
class OpenWebAuth
{
/**
* Process the 'zrl' parameter and initiate the remote authentication.
*
* This method checks if the visitor has a public contact entry and
* redirects the visitor to his/her instance to start the magic auth (Authentication)
* process.
*
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/channel.php
*
* The implementation for Friendica sadly differs in some points from the one for Hubzilla:
* - Hubzilla uses the "zid" parameter, while for Friendica it had been replaced with "zrl"
* - There seem to be some reverse authentication (rmagic) that isn't implemented in Friendica at all
*
* It would be favourable to harmonize the two implementations.
*
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function zrlInit()
{
$my_url = DI::userSession()->getMyUrl();
$my_url = Network::isUrlValid($my_url);
if (empty($my_url) || DI::userSession()->getLocalUserId()) {
return;
}
$addr = $_GET['addr'] ?? $my_url;
$arr = ['zrl' => $my_url, 'url' => DI::args()->getCommand()];
Hook::callAll('zrl_init', $arr);
// Try to find the public contact entry of the visitor.
$contact = Contact::getByURL($my_url, null, ['id', 'url', 'gsid']);
if (empty($contact)) {
Logger::info('No contact record found', ['url' => $my_url]);
return;
}
if (DI::userSession()->getRemoteUserId() && DI::userSession()->getRemoteUserId() == $contact['id']) {
Logger::info('The visitor is already authenticated', ['url' => $my_url]);
return;
}
$gserver = DBA::selectFirst('gserver', ['url', 'authredirect'], ['id' => $contact['gsid']]);
if (empty($gserver) || empty($gserver['authredirect'])) {
Logger::info('No server record found or magic path not defined for server', ['id' => $contact['gsid'], 'gserver' => $gserver]);
return;
}
// Avoid endless loops
$cachekey = 'zrlInit:' . $my_url;
if (DI::cache()->get($cachekey)) {
Logger::info('URL ' . $my_url . ' already tried to authenticate.');
return;
} else {
DI::cache()->set($cachekey, true, Duration::MINUTE);
}
Logger::info('Not authenticated. Invoking reverse magic-auth', ['url' => $my_url]);
// Remove the "addr" parameter from the destination. It is later added as separate parameter again.
$addr_request = 'addr=' . urlencode($addr);
$query = rtrim(str_replace($addr_request, '', DI::args()->getQueryString()), '?&');
// The other instance needs to know where to redirect.
$dest = urlencode(DI::baseUrl() . '/' . $query);
if ($gserver['url'] != DI::baseUrl() && !strstr($dest, '/magic')) {
$magic_path = $gserver['authredirect'] . '?f=&rev=1&owa=1&dest=' . $dest . '&' . $addr_request;
Logger::info('Doing magic auth for visitor ' . $my_url . ' to ' . $magic_path);
System::externalRedirect($magic_path);
}
}
/**
* OpenWebAuth authentication.
*
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/zid.php
*
* @param string $token
*
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function init(string $token)
{
$a = DI::app();
// Clean old OpenWebAuthToken entries.
OpenWebAuthToken::purge('owt', '3 MINUTE');
// Check if the token we got is the same one
// we have stored in the database.
$visitor_handle = OpenWebAuthToken::getMeta('owt', 0, $token);
if ($visitor_handle === false) {
return;
}
$visitor = self::addVisitorCookieForHandle($visitor_handle);
if (empty($visitor)) {
return;
}
$arr = [
'visitor' => $visitor,
'url' => DI::args()->getQueryString()
];
/**
* @hooks magic_auth_success
* Called when a magic-auth was successful.
* * \e array \b visitor
* * \e string \b url
*/
Hook::callAll('magic_auth_success', $arr);
$a->setContactId($arr['visitor']['id']);
DI::sysmsg()->addInfo(DI::l10n()->t('OpenWebAuth: %1$s welcomes %2$s', DI::baseUrl()->getHost(), $visitor['name']));
Logger::info('OpenWebAuth: auth success from ' . $visitor['addr']);
}
/**
* Set the visitor cookies (see remote_user()) for the given handle
*
* @param string $handle Visitor handle
*
* @return array Visitor contact array
*/
public static function addVisitorCookieForHandle(string $handle): array
{
$a = DI::app();
// Try to find the public contact entry of the visitor.
$cid = Contact::getIdForURL($handle);
if (!$cid) {
Logger::info('Handle not found', ['handle' => $handle]);
return [];
}
$visitor = Contact::getById($cid);
// Authenticate the visitor.
DI::userSession()->setMultiple([
'authenticated' => 1,
'visitor_id' => $visitor['id'],
'visitor_handle' => $visitor['addr'],
'visitor_home' => $visitor['url'],
'my_url' => $visitor['url'],
'remote_comment' => $visitor['subscribe'],
]);
DI::userSession()->setVisitorsContacts($visitor['url']);
$a->setContactId($visitor['id']);
Logger::info('Authenticated visitor', ['url' => $visitor['url']]);
return $visitor;
}
/**
* Set the visitor cookies (see remote_user()) for signed HTTP requests
*
* @param array $server The content of the $_SERVER superglobal
* @return array Visitor contact array
* @throws InternalServerErrorException
*/
public static function addVisitorCookieForHTTPSigner(array $server): array
{
$requester = HTTPSignature::getSigner('', $server);
if (empty($requester)) {
return [];
}
return self::addVisitorCookieForHandle($requester);
}
/**
* Returns URL with URL-encoded zrl parameter
*
* @param string $url URL to enhance
* @param bool $force Either to force adding zrl parameter
*
* @return string URL with 'zrl' parameter or original URL in case of no Friendica profile URL
*/
public static function getZrlUrl(string $url, bool $force = false): string
{
if (!strlen($url)) {
return $url;
}
if (!strpos($url, '/profile/') && !$force) {
return $url;
}
if ($force && substr($url, -1, 1) !== '/') {
$url = $url . '/';
}
$achar = strpos($url, '?') ? '&' : '?';
$mine = DI::userSession()->getMyUrl();
if ($mine && !Strings::compareLink($mine, $url)) {
return $url . $achar . 'zrl=' . urlencode($mine);
}
return $url;
}
}