Files
nextcloud-server-mirror/lib/private/legacy/OC_User.php
Git'Fellow 1c9e5205f9 fix(auth): keep remember-me cookies pointing at a live session token
The remember-me cookie outlives the session token it refers to in two
cases, and cookie login then fails on every request.

A session token created as DO_NOT_REMEMBER (user_oidc, Apache login) is
removed by the cleanup job after session_lifetime, while the cookies last
remember_login_cookie_lifetime. loginWithApache() now creates its token
as REMEMBER, and createRememberMeToken() marks a DO_NOT_REMEMBER token
as REMEMBER for other callers with the same mismatch.

ISession::regenerateId(true, true) moves the token to the new session id
but left nc_session_id pointing at the old one (password protected share
unlock, Talk password rooms). The cookie is now rewritten with the new id.

Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2026-09-22 12:22:00 +02:00

316 lines
9.2 KiB
PHP

<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
use OC\Authentication\Token\IProvider;
use OC\SystemConfig;
use OC\User\Database;
use OC\User\DisabledUserException;
use OC\User\Session;
use OCP\App\IAppManager;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\Authentication\Exceptions\WipeTokenException;
use OCP\Authentication\IApacheBackend;
use OCP\Authentication\IProvideUserSecretBackend;
use OCP\Authentication\Token\IToken;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IGroupManager;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Server;
use OCP\Session\Exceptions\SessionNotAvailableException;
use OCP\User\Events\BeforeUserLoggedInEvent;
use OCP\UserInterface;
use Psr\Log\LoggerInterface;
/**
* This class provides wrapper methods for user management. Multiple backends are
* supported. User management operations are delegated to the configured backend for
* execution.
*
* Note that &run is deprecated and won't work anymore.
*
* Hooks provided:
* post_login(uid)
* logout()
*/
class OC_User {
public static $_setupedBackends = [];
// bool, stores if a user want to access a resource anonymously, e.g if they open a public link
private static $incognitoMode = false;
/**
* Adds the backend to the list of used backends
*
* @param string|UserInterface $backend default: database The backend to use for user management
* @return bool
* @deprecated 32.0.0 Use IUserManager::registerBackend instead
*
* Set the User Authentication Module
*/
public static function useBackend($backend = 'database') {
if ($backend instanceof UserInterface) {
Server::get(IUserManager::class)->registerBackend($backend);
} else {
// You'll never know what happens
if ($backend === null || !is_string($backend)) {
$backend = 'database';
}
// Load backend
switch ($backend) {
case 'database':
case 'mysql':
case 'sqlite':
Server::get(LoggerInterface::class)->debug('Adding user backend ' . $backend . '.', ['app' => 'core']);
Server::get(IUserManager::class)->registerBackend(new Database());
break;
case 'dummy':
Server::get(IUserManager::class)->registerBackend(new \Test\Util\User\Dummy());
break;
default:
Server::get(LoggerInterface::class)->debug('Adding default user backend ' . $backend . '.', ['app' => 'core']);
$className = 'OC_USER_' . strtoupper($backend);
Server::get(IUserManager::class)->registerBackend(new $className());
break;
}
}
return true;
}
/**
* remove all used backends
* @deprecated 32.0.0 Use IUserManager::clearBackends instead
*/
public static function clearBackends() {
Server::get(IUserManager::class)->clearBackends();
}
/**
* setup the configured backends in config.php
* @suppress PhanDeprecatedFunction
*/
public static function setupBackends() {
Server::get(IAppManager::class)->loadApps(['prelogin']);
$backends = Server::get(SystemConfig::class)->getValue('user_backends', []);
if (isset($backends['default']) && !$backends['default']) {
// clear default backends
self::clearBackends();
}
foreach ($backends as $i => $config) {
if (!is_array($config)) {
continue;
}
$class = $config['class'];
$arguments = $config['arguments'];
if (class_exists($class)) {
if (!in_array($i, self::$_setupedBackends, true)) {
// make a reflection object
$reflectionObj = new ReflectionClass($class);
// use Reflection to create a new instance, using the $args
$backend = $reflectionObj->newInstanceArgs($arguments);
self::useBackend($backend);
self::$_setupedBackends[] = $i;
} else {
Server::get(LoggerInterface::class)->debug('User backend ' . $class . ' already initialized.', ['app' => 'core']);
}
} else {
Server::get(LoggerInterface::class)->error('User backend ' . $class . ' not found.', ['app' => 'core']);
}
}
}
/**
* Try to login a user, assuming authentication
* has already happened (e.g. via Single Sign On).
*
* Log in a user and regenerate a new session.
*/
public static function loginWithApache(IApacheBackend $backend): bool {
$uid = $backend->getCurrentUserId();
$run = true;
if ($uid) {
if (self::getUser() !== $uid) {
self::setUserId($uid);
/** @var Session $userSession */
$userSession = Server::get(IUserSession::class);
$dispatcher = Server::get(IEventDispatcher::class);
$request = Server::get(IRequest::class);
$user = $userSession->getUser();
if (!$user) {
// Should not happen except from bad code or configuration
throw new \OCP\User\Exceptions\UserNotFoundException('User ' . $uid . ' not found');
}
if (!$user->isEnabled()) {
$message = Server::get(IFactory::class)->get('lib')->t('Account disabled');
throw new DisabledUserException($message);
}
$password = null;
if ($backend instanceof IProvideUserSecretBackend) {
$password = $backend->getCurrentUserSecret();
}
$dispatcher->dispatchTyped(new BeforeUserLoggedInEvent($uid, $password, $backend));
$userSession->createSessionToken($request, $uid, $uid, $password, IToken::REMEMBER);
$userSession->completeLogin(
$user,
['loginName' => $uid, 'password' => $password ?? ''],
regenerateSessionId:false,
);
$userSession->createRememberMeToken($user);
if (empty($password)) {
$tokenProvider = Server::get(IProvider::class);
try {
$token = $tokenProvider->getToken($userSession->getSession()->getId());
$token->setScope([
IToken::SCOPE_SKIP_PASSWORD_VALIDATION => true,
IToken::SCOPE_FILESYSTEM => true,
]);
$tokenProvider->updateToken($token);
} catch (InvalidTokenException|WipeTokenException|SessionNotAvailableException) {
// swallow the exceptions as we do not deal with them here
// simply skip updating the token when is it missing
}
}
}
return true;
}
return false;
}
/**
* Verify with Apache whether user is authenticated.
*
* @return bool|null
* true: authenticated
* false: not authenticated
* null: not handled / no backend available
*/
public static function handleApacheAuth(): ?bool {
$backend = self::findFirstActiveUsedBackend();
if ($backend) {
Server::get(IAppManager::class)->loadApps();
//setup extra user backends
self::setupBackends();
/** @var Session $session */
$session = Server::get(IUserSession::class);
$session->unsetMagicInCookie();
return self::loginWithApache($backend);
}
return null;
}
/**
* Sets user id for session and triggers emit
*/
public static function setUserId(?string $uid): void {
$userSession = Server::get(IUserSession::class);
$userManager = Server::get(IUserManager::class);
if ($user = $userManager->get($uid)) {
$userSession->setUser($user);
} else {
Server::get(ISession::class)->set('user_id', $uid);
}
}
/**
* Set incognito mode, e.g. if a user wants to open a public link
*/
public static function setIncognitoMode(bool $status): void {
self::$incognitoMode = $status;
}
/**
* Get incognito mode status
*/
public static function isIncognitoMode(): bool {
return self::$incognitoMode;
}
/**
* Returns the current logout URL valid for the currently logged-in user
* @return non-empty-string
*/
public static function getLogoutUrl(IURLGenerator $urlGenerator): string {
return $urlGenerator->getLogoutUrl();
}
/**
* Check if the user is an admin user
*
* @param string $uid uid of the admin
*/
public static function isAdminUser(string $uid): bool {
$user = Server::get(IUserManager::class)->get($uid);
$isAdmin = $user && Server::get(IGroupManager::class)->isAdmin($user->getUID());
return $isAdmin && self::$incognitoMode === false;
}
/**
* get the user id of the user currently logged in.
*
* @return string|false uid or false
*/
public static function getUser(): string|false {
$uid = Server::get(ISession::class)?->get('user_id');
if (!is_null($uid) && self::$incognitoMode === false) {
return $uid;
} else {
return false;
}
}
/**
* Set password
*
* @param string $uid The username
* @param string $password The new password
* @param string $recoveryPassword for the encryption app to reset encryption keys
*
* Change the password of a user
*/
public static function setPassword(string $uid, string $password, ?string $recoveryPassword = null): bool {
$user = Server::get(IUserManager::class)->get($uid);
if ($user) {
return $user->setPassword($password, $recoveryPassword);
} else {
return false;
}
}
/**
* Returns the first active backend from self::$_usedBackends.
*
* @return IApacheBackend|null if no backend active, otherwise OCP\Authentication\IApacheBackend
*/
private static function findFirstActiveUsedBackend(): ?IApacheBackend {
foreach (Server::get(IUserManager::class)->getBackends() as $backend) {
if ($backend instanceof IApacheBackend) {
if ($backend->isSessionActive()) {
return $backend;
}
}
}
return null;
}
}