HEX
Server: Apache
System: Linux server1.ariadata.co 4.18.0-553.120.1.el8_10.x86_64 #1 SMP Mon Apr 20 18:04:27 EDT 2026 x86_64
User: nepi (1051)
PHP: 7.4.33
Disabled: exec,passthru,shell_exec,system,show_source,popen,proc_open
Upload Files
File: /home/nepi/public_html/src/core/Directus/Authentication/Provider.php
<?php

namespace Directus\Authentication;

use Directus\Application\Application;

use Directus\Authentication\Exception\ExpiredTokenException;
use Directus\Authentication\Exception\InvalidOTPException;
use Directus\Authentication\Exception\InvalidTokenException;
use Directus\Authentication\Exception\InvalidUserCredentialsException;
use Directus\Authentication\Exception\Missing2FAPasswordException;
use Directus\Authentication\Exception\UserInactiveException;
use Directus\Authentication\Exception\UserNotAuthenticatedException;
use Directus\Authentication\Exception\UserNotFoundException;
use Directus\Authentication\Exception\UserWithEmailNotFoundException;
use Directus\Authentication\Exception\UserSuspendedException;
use Directus\Database\TableGatewayFactory;
use Directus\Database\Schema\SchemaManager;
use Directus\Database\TableGateway\DirectusActivityTableGateway;
use Directus\Database\TableGateway\DirectusUsersTableGateway;
use Directus\Authentication\User\Provider\UserProviderInterface;
use Directus\Authentication\User\UserInterface;
use Directus\Exception\Exception;
use function Directus\get_api_project_from_request;
use function Directus\get_project_config;
use function Directus\get_directus_setting;
use Directus\Util\ArrayUtils;
use Directus\Util\DateTimeUtils;
use Directus\Util\JWTUtils;
use PragmaRX\Google2FA\Google2FA;

class Provider
{
    /**
     * The user ID of the public API user.
     *
     * @var integer
     */
    const PUBLIC_USER_ID = 0;

    /**
     * Whether the user has been authenticated or not
     *
     * @var bool
     */
    protected $authenticated = false;

    /**
     * Authenticated user information
     *
     * @var UserInterface
     */
    protected $user;

    /**
     * User Provider
     *
     * @var UserProviderInterface
     */
    protected $userProvider;

    /**
     * @var string
     */
    protected $secretKey;

    /**
     * @var string
     */
    protected $publicKey;

    /**
     * JWT time to live in minutes
     *
     * @var int
     */
    protected $ttl;

    public function __construct(UserProviderInterface $userProvider, array $options = [])
    {
        if (!isset($options['secret_key']) || !is_string($options['secret_key'])) {
            throw new Exception('auth: secret key is required and it must be a string');
        }

        $ttl = get_directus_setting('auth_token_ttl', ArrayUtils::get($options, 'auth.ttl', 20));
        if (!is_numeric($ttl)) {
            throw new Exception('ttl must be a number');
        }

        $this->userProvider = $userProvider;
        $this->user = null;
        $this->secretKey = $options['secret_key'];
        $this->publicKey = ArrayUtils::get($options, 'public_key');
        $this->ttl = (int) $ttl;
    }

    /**
     * @throws UserNotAuthenticatedException
     */
    protected function enforceUserIsAuthenticated()
    {
        if (!$this->check()) {
            throw new UserNotAuthenticatedException('Attempting to inspect a non-authenticated user');
        }
    }

    /**
     * Signing In a user
     *
     * Creating the user token and resetting previous token
     *
     * @param array $credentials
     *
     * @return UserInterface
     *
     * @throws InvalidUserCredentialsException
     * @throws Missing2FAPasswordException
     * @throws UserInactiveException
     */
    public function login(array $credentials)
    {
        $email = ArrayUtils::get($credentials, 'email');
        $password = ArrayUtils::get($credentials, 'password');
        $otp = ArrayUtils::get($credentials, 'otp');

        $user = null;
        if ($email && $password) {
            // Verify Credentials
            $user = $this->findUserWithCredentials($email, $password, $otp);
            $this->setUser($user);
        }

        return $user;
    }

    /**
     * Finds a user with the given conditions
     *
     * @param array $conditions
     * @return User\User
     *
     * @throws UserInactiveException
     * @throws UserNotFoundException
     */
    public function findUserWithConditions(array $conditions)
    {
        $user = $this->userProvider->findWhere($conditions);

        $this->validateUser($user);

        return $user;
    }

    /**
     * Returns a user if the credentials matches
     *
     * @param string $email
     * @param string $password
     *
     * @return UserInterface
     *
     * @throws InvalidUserCredentialsException
     * @throws InvalidOTPException
     * @throws Missing2FAPasswordException
     */
    public function findUserWithCredentials($email, $password, $otp = null)
    {
        $hookEmitter = Application::getInstance()->getContainer()->get('hook_emitter');
	    
        try {
            $user = $this->findUserWithEmail($email);
        } catch (UserWithEmailNotFoundException $e) {
            throw new InvalidUserCredentialsException();
        }

        // Verify that the user has an id (exists), it returns empty user object otherwise
        if (!password_verify($password, $user->get('password'))) {
			
			$hookEmitter->run('auth.fail', [$user, 'app']);
            $this->recordActivityAndCheckLoginAttempt($user);
            throw new InvalidUserCredentialsException();
        }

        $tfa_secret = $user->get2FASecret();

        if ($tfa_secret) {
            $ga = new Google2FA();

            if ($otp == null) {
                throw new Missing2FAPasswordException();
            }

            if (!$ga->verifyKey($tfa_secret, $otp, 2)) {
                throw new InvalidOTPException();
            }
        }

        $this->user = $user;
        
        $hookEmitter->run('auth.success', [$user, 'app']);

        return $user;
    }

    /**
     * Record invalid credentials action and throw exception if the maximum invalid login attempts reached.
     */
    public function recordActivityAndCheckLoginAttempt($user)
    {
        $userId = $user->get('id');
        $activityTableGateway = TableGatewayFactory::create(SchemaManager::COLLECTION_ACTIVITY, ['acl' => false]);

        // Added this before calculation as system may throw the exception here.
        $activityTableGateway->recordAction($userId, SchemaManager::COLLECTION_USERS, DirectusActivityTableGateway::ACTION_INVALID_CREDENTIALS);

        $loginAttemptsAllowed = get_directus_setting('login_attempts_allowed');

        if (!empty($loginAttemptsAllowed)) {

            // We added 'Invalid credentials' entry before this condition so need to increase this counter with 1
            $totalLoginAttemptsAllowed = $loginAttemptsAllowed + 1;

            $invalidLoginAttempts = $activityTableGateway->getInvalidLoginAttempts($userId, $totalLoginAttemptsAllowed);
            if (!empty($invalidLoginAttempts)) {
                $lastInvalidCredentialsEntry = current($invalidLoginAttempts);
                $firstInvalidCredentialsEntry = end($invalidLoginAttempts);

                $lastLoginAttempt = $activityTableGateway->getLastLoginOrStatusUpdateAttempt($userId);

                if (!empty($lastLoginAttempt) && !in_array($lastLoginAttempt['id'], range($firstInvalidCredentialsEntry['id'], $lastInvalidCredentialsEntry['id'])) &&  count($invalidLoginAttempts) > $loginAttemptsAllowed) {

                    $tableGateway = TableGatewayFactory::create(SchemaManager::COLLECTION_USERS, ['acl' => false]);
                    $update = [
                        'status' => DirectusUsersTableGateway::STATUS_SUSPENDED
                    ];
                    $tableGateway->update($update, ['id' => $userId]);
                    throw new UserSuspendedException();
                }
            }
        }
    }

    /**
     * Returns a user with the given email if exists
     * Otherwise throw an UserNotFoundException
     *
     * @param $email
     *
     * @return User\User
     *
     * @throws UserWithEmailNotFoundException
     */
    public function findUserWithEmail($email)
    {
        $user = $this->userProvider->findByEmail($email);

        try {
            $this->validateUser($user);
        } catch (UserNotFoundException $e) {
            throw new UserWithEmailNotFoundException($email);
        }

        return $user;
    }

    /**
     * Checks if the user is active
     *
     * @param UserInterface $user
     *
     * @return bool
     */
    public function isActive(UserInterface $user)
    {
        $userProvider = $this->userProvider;

        // TODO: Cast attributes values
        return $user->get('status') == $userProvider::STATUS_ACTIVE;
    }

    /**
     * Checks if the user is suspended
     *
     * @param UserInterface $user
     *
     * @return bool
     */
    public function isSuspended(UserInterface $user)
    {
        $userProvider = $this->userProvider;

        // TODO: Cast attributes values
        return $user->get('status') == $userProvider::STATUS_SUSPENDED;
    }

    /**
     * Authenticate an user using a JWT Token
     *
     * @param string $token
     * @param bool $ignoreOrigin
     *
     * @return UserInterface
     *
     * @throws InvalidTokenException
     */
    public function authenticateWithToken($token, $ignoreOrigin = false)
    {
        $payload = $this->getTokenPayload($token, $ignoreOrigin);
        if (!JWTUtils::hasPayloadType(JWTUtils::TYPE_AUTH, $payload)) {
            throw new InvalidTokenException();
        }

        $conditions = [
            'id' => $payload->id,
            // 'group' => $payload->group
        ];

        $user = $this->findUserWithConditions($conditions);

        if ($user) {
            $this->setUser($user);
        }

        return $user;
    }

    public function authenticateWithEmail($email)
    {
        $user = $this->userProvider->findByEmail($email);

        if (!$user) {
            throw new UserWithEmailNotFoundException($email);
        }

        $this->setUser($user);

        return $user;
    }

    /**
     * Authenticate an user using a private token
     *
     * @param string $token
     *
     * @return UserInterface
     *
     * @throws UserInactiveException
     */
    public function authenticateWithPrivateToken($token)
    {
        $user = $this->findUserWithConditions([
            'token' => $token
        ]);

        if ($user) {
            $this->setUser($user);
        }

        return $user;
    }

    /**
     * Force a user id to be the logged user
     *
     * TODO: Change this method name
     *
     * @param UserInterface $user The User account's ID.
     *
     * @return UserInterface
     *
     * @throws UserNotFoundException
     */
    public function forceUserLogin(UserInterface $user)
    {
        $this->setUser($user);

        return $user;
    }

    /**
     * Check if a user is logged in.
     *
     * @return boolean
     */
    public function check()
    {
        return $this->authenticated;
    }

    /**
     * Retrieve metadata about the currently logged in user.
     *
     * @param null|string $attribute
     *
     * @return mixed|array Authenticated user metadata.
     *
     * @throws  \Directus\Authentication\Exception\UserNotAuthenticatedException
     */
    public function getUserAttributes($attribute = null)
    {
        $this->enforceUserIsAuthenticated();
        $user = $this->user->toArray();

        if ($attribute !== null) {
            return array_key_exists($attribute, $user) ? $user[$attribute] : null;
        }

        return $user;
    }

    /**
     * Gets authenticated user object
     *
     * @return UserInterface|null
     */
    public function getUser()
    {
        return $this->user;
    }

    /**
     * Gets the user provider
     *
     * @return UserProviderInterface
     */
    public function getUserProvider()
    {
        return $this->userProvider;
    }

    /**
     * Generates a new access token
     *
     * @param UserInterface $user
     *
     * @param bool $needs2FA Whether the User needs 2FA
     *
     * @return string
     */
    public function generateAuthToken(UserInterface $user)
    {
        $payload = [
            'id' => (int) $user->getId(),
            // 'group' => $user->getGroupId(),
            'exp' => $this->getNewExpirationTime(),
            'ttl' => $this->ttl,
        ];


        return $this->generateToken(JWTUtils::TYPE_AUTH, $payload);
    }

    /**
     * Generates a new request token used to SSO to request an Access Token
     *
     * @param UserInterface $user
     *
     * @return string
     */
    public function generateRequestToken(UserInterface $user)
    {
        $payload = [
            'type' => 'request_token',
            'id' => (int) $user->getId(),
            // 'group' => (int) $user->getGroupId(),
            'exp' => time() + ($this->ttl * DateTimeUtils::MINUTE_IN_SECONDS),
            'ttl' => $this->ttl,
            'url' => \Directus\get_url(),
            'project' => \Directus\get_api_project_from_request()
        ];

        return $this->generateToken(JWTUtils::TYPE_SSO_REQUEST_TOKEN, $payload);
    }

    /**
     * Generates a new reset password token
     *
     * @param UserInterface $user
     *
     * @return string
     */
    public function generateResetPasswordToken(UserInterface $user)
    {
        $payload = [
            'id' => (int) $user->getId(),
            'email' => $user->getEmail(),
            // TODO: Separate time expiration for reset password token
            'exp' => $this->getNewExpirationTime(),
            'ttl' => $this->ttl
        ];

        return $this->generateToken(JWTUtils::TYPE_RESET_PASSWORD, $payload);
    }

    /**
     * Generate invitation token
     *
     * @param array $payload
     *
     * @return string
     */
    public function generateInvitationToken(array $payload)
    {
        return $this->generateToken(JWTUtils::TYPE_INVITATION, $payload);
    }

    /**
     * Generates a new JWT token
     *
     * @param string $type
     * @param array $payload
     *
     * @return string
     */
    public function generateToken($type, array $payload)
    {
        $payload['type'] = (string) $type;
        $payload['key'] = $this->getPublicKey();
        $payload['project'] = get_api_project_from_request();

        return JWTUtils::encode($payload, $this->getSecretKey($payload['project']), $this->getTokenAlgorithm());
    }

    /**
     * Refresh valid token expiration
     *
     * @param $token
     *
     * @return string
     *
     * @throws ExpiredTokenException
     * @throws InvalidTokenException
     */
    public function refreshToken($token, $needs2FA = false)
    {
        $payload = $this->getTokenPayload($token);

        if (!JWTUtils::hasPayloadType(JWTUtils::TYPE_AUTH, $payload)) {
            throw new InvalidTokenException();
        }
        $payload->exp = $this->getNewExpirationTime();

        $payload->needs2FA = $needs2FA;

        return JWTUtils::encode($payload, $this->getSecretKey(get_api_project_from_request()), $this->getTokenAlgorithm());
    }

    /**
     * Checks if the payload matches the public key
     *
     * @param object $payload
     *
     * @return bool
     */
    public function hasPayloadPublicKey($payload)
    {
        return JWTUtils::hasPayloadKey($this->getPublicKey(), $payload);
    }

    /**
     * Checks if the payload matches the project name
     *
     * @param object $payload
     *
     * @return bool
     */
    public function hasPayloadProjectName($payload)
    {
        return JWTUtils::hasPayloadProjectName(get_api_project_from_request(), $payload);
    }

    /**
     * Checks if the origin of the token in from this project configuration
     *
     * @param object $payload
     *
     * @return bool
     */
    public function isPayloadLocal($payload)
    {
        return $this->hasPayloadPublicKey($payload) && $this->hasPayloadProjectName($payload);
    }

    /**
     * Throws an exception if the payload origin doesn't match this project configuration
     *
     * @param object $payload
     *
     * @throws InvalidTokenException
     */
    public function validatePayloadOrigin($payload)
    {
        if (!$this->isPayloadLocal($payload)) {
            throw new InvalidTokenException();
        }
    }

    /**
     * Run the hashing algorithm on a password and salt value.
     *
     * @param  string $password
     *
     * @return string
     */
    public function hashPassword($password)
    {
        // TODO: Create a library to hash/verify passwords up to the user which algorithm to use
        return password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
    }

    /**
     * Authentication secret key
     *
     * @param string $project
     *
     * @return string
     */
    public function getSecretKey($project)
    {
        if ($project) {
            $config = get_project_config($project);

            return $config->get('auth.secret_key');
        }

        return $this->secretKey;
    }

    /**
     * Authentication public key
     *
     * @return string
     */
    public function getPublicKey()
    {
        return $this->publicKey;
    }

    /**
     * @return int
     */
    public function getNewExpirationTime()
    {
        return time() + ($this->ttl * DateTimeUtils::MINUTE_IN_SECONDS);
    }

    /**
     * Verifies and Returns the payload if valid
     *
     * @param string $token
     * @param bool $ignoreOrigin
     *
     * @return object
     *
     * @throws InvalidTokenException
     */
    protected function getTokenPayload($token, $ignoreOrigin = false)
    {
        $projectName = $ignoreOrigin ? JWTUtils::getPayload($token, 'project') : null;
        $payload = JWTUtils::decode($token, $this->getSecretKey($projectName), [$this->getTokenAlgorithm()]);

        if ($ignoreOrigin !== true && !$this->isPayloadLocal($payload)) {
            // Empty payload, log this as debug?
            throw new InvalidTokenException();
        }

        return $payload;
    }

    /**
     * Return the token encoding algorithm
     *
     * @return string
     */
    protected function getTokenAlgorithm()
    {
        return 'HS256';
    }

    /**
     * Sets the given user as authenticated
     *
     * @param UserInterface $user
     *
     * @throws UserInactiveException
     * @throws UserNotFoundException
     */
    protected function setUser(UserInterface $user)
    {
        $this->validateUser($user);

        $this->authenticated = true;
        $this->user = $user;
    }

    /**
     * Validates an user object
     *
     * @param UserInterface|null $user
     *
     * @throws UserInactiveException
     * @throws UserNotFoundException
     */
    protected function validateUser($user)
    {
        if (!($user instanceof UserInterface) || !$user->getId()) {
            throw new UserNotFoundException();
        }

        if ($this->isSuspended($user)) {
            throw new UserSuspendedException();
        }

        if (!$this->isActive($user)) {
            throw new UserInactiveException();
        }
    }
}