1<?php
2/**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9namespace Piwik\Plugins\Login;
10
11use Piwik\AuthResult;
12use Piwik\Auth\Password;
13use Piwik\Common;
14use Piwik\Date;
15use Piwik\DbHelper;
16use Piwik\Piwik;
17use Piwik\Plugins\UsersManager\Model;
18use Piwik\Plugins\UsersManager\UsersManager;
19
20class Auth implements \Piwik\Auth
21{
22    protected $login;
23    protected $token_auth;
24    protected $hashedPassword;
25
26    /**
27     * @var Model
28     */
29    private $userModel;
30
31    /**
32     * @var Password
33     */
34    private $passwordHelper;
35
36    public function __construct()
37    {
38        $this->userModel      = new Model();
39        $this->passwordHelper = new Password();
40    }
41
42    /**
43     * Authentication module's name, e.g., "Login"
44     *
45     * @return string
46     */
47    public function getName()
48    {
49        return 'Login';
50    }
51
52    /**
53     * Authenticates user
54     *
55     * @return AuthResult
56     */
57    public function authenticate()
58    {
59        try {
60            if (!empty($this->hashedPassword)) {
61                return $this->authenticateWithPassword($this->login, $this->getTokenAuthSecret());
62            } elseif (is_null($this->login)) {
63                return $this->authenticateWithToken($this->token_auth);
64            } elseif (!empty($this->login)) {
65                return $this->authenticateWithLoginAndToken($this->token_auth, $this->login);
66            }
67        } catch (\Zend_Db_Statement_Exception $e) {
68            // user_token_auth table might not yet exist when updating to Matomo 4
69            if (strpos($e->getMessage(), 'user_token_auth') && !DbHelper::tableExists(Common::prefixTable('user_token_auth'))) {
70                return new AuthResult(AuthResult::SUCCESS, 'anonymous', 'anonymous');
71            }
72
73            throw $e;
74        }
75
76        return new AuthResult(AuthResult::FAILURE, $this->login, $this->token_auth);
77    }
78
79    private function authenticateWithPassword($login, $passwordHash)
80    {
81        $user = $this->userModel->getUser($login);
82
83        if (empty($user['login'])) {
84            return new AuthResult(AuthResult::FAILURE, $login, null);
85        }
86
87        if ($this->passwordHelper->verify($passwordHash, $user['password'])) {
88            if ($this->passwordHelper->needsRehash($user['password'])) {
89                $newPasswordHash = $this->passwordHelper->hash($passwordHash);
90
91                $this->userModel->updateUser($login, $newPasswordHash, $user['email']);
92            }
93            $this->token_auth = null; // make sure to generate a random token
94
95            return $this->authenticationSuccess($user);
96        }
97
98        return new AuthResult(AuthResult::FAILURE, $login, null);
99    }
100
101    private function authenticateWithToken($token)
102    {
103        $user = $this->userModel->getUserByTokenAuth($token);
104
105        if (!empty($user['login'])) {
106            $this->userModel->setTokenAuthWasUsed($token, Date::now()->getDatetime());
107            return $this->authenticationSuccess($user);
108        }
109
110        return new AuthResult(AuthResult::FAILURE, null, $token);
111    }
112
113    private function authenticateWithLoginAndToken($token, $login)
114    {
115        $user = $this->userModel->getUserByTokenAuth($token);
116
117        if (!empty($user['login']) && $user['login'] === $login) {
118            $this->userModel->setTokenAuthWasUsed($token, Date::now()->getDatetime());
119            return $this->authenticationSuccess($user);
120        }
121
122        return new AuthResult(AuthResult::FAILURE, $login, $token);
123    }
124
125    private function authenticationSuccess(array $user)
126    {
127        if (empty($this->token_auth)) {
128            $this->token_auth = $this->userModel->generateRandomTokenAuth();
129            // we generated one randomly which will then be stored in the session and used across the session
130        }
131
132        $isSuperUser = (int) $user['superuser_access'];
133        $code = $isSuperUser ? AuthResult::SUCCESS_SUPERUSER_AUTH_CODE : AuthResult::SUCCESS;
134
135        return new AuthResult($code, $user['login'], $this->token_auth);
136    }
137
138    /**
139     * Returns the login of the user being authenticated.
140     *
141     * @return string
142     */
143    public function getLogin()
144    {
145        return $this->login;
146    }
147
148    /**
149     * Accessor to set login name
150     *
151     * @param string $login user login
152     */
153    public function setLogin($login)
154    {
155        $this->login = $login;
156    }
157
158    /**
159     * Returns the secret used to calculate a user's token auth.
160     *
161     * @return string
162     */
163    public function getTokenAuthSecret()
164    {
165        return $this->hashedPassword;
166    }
167
168    /**
169     * Accessor to set authentication token
170     *
171     * @param string $token_auth authentication token
172     */
173    public function setTokenAuth($token_auth)
174    {
175        $this->token_auth = $token_auth;
176    }
177
178    /**
179     * Sets the password to authenticate with.
180     *
181     * @param string $password
182     */
183    public function setPassword($password)
184    {
185        if (empty($password)) {
186            $this->hashedPassword = null;
187        } else {
188            $this->hashedPassword = UsersManager::getPasswordHash($password);
189        }
190    }
191
192    /**
193     * Sets the password hash to use when authentication.
194     *
195     * @param string $passwordHash The password hash.
196     */
197    public function setPasswordHash($passwordHash)
198    {
199        if ($passwordHash === null) {
200            $this->hashedPassword = null;
201            return;
202        }
203
204        // check that the password hash is valid (sanity check)
205        UsersManager::checkPasswordHash($passwordHash, Piwik::translate('Login_ExceptionPasswordMD5HashExpected'));
206
207        $this->hashedPassword = $passwordHash;
208    }
209
210    // for tests
211    public function getTokenAuth()
212    {
213        return $this->token_auth;
214    }
215}
216