Auth.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  6. * @author Bart Visscher <bartv@thisnet.nl>
  7. * @author Christoph Wurst <christoph@owncloud.com>
  8. * @author Jakob Sack <mail@jakobsack.de>
  9. * @author Joas Schilling <coding@schilljs.com>
  10. * @author Lukas Reschke <lukas@statuscode.ch>
  11. * @author Markus Goetz <markus@woboq.com>
  12. * @author Michael Gapczynski <GapczynskiM@gmail.com>
  13. * @author Morris Jobke <hey@morrisjobke.de>
  14. * @author Thomas Müller <thomas.mueller@tmit.eu>
  15. * @author Vincent Petry <pvince81@owncloud.com>
  16. *
  17. * @license AGPL-3.0
  18. *
  19. * This code is free software: you can redistribute it and/or modify
  20. * it under the terms of the GNU Affero General Public License, version 3,
  21. * as published by the Free Software Foundation.
  22. *
  23. * This program is distributed in the hope that it will be useful,
  24. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  25. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  26. * GNU Affero General Public License for more details.
  27. *
  28. * You should have received a copy of the GNU Affero General Public License, version 3,
  29. * along with this program. If not, see <http://www.gnu.org/licenses/>
  30. *
  31. */
  32. namespace OCA\DAV\Connector\Sabre;
  33. use Exception;
  34. use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
  35. use OC\Authentication\TwoFactorAuth\Manager;
  36. use OC\Security\Bruteforce\Throttler;
  37. use OC\User\Session;
  38. use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
  39. use OCP\IRequest;
  40. use OCP\ISession;
  41. use Sabre\DAV\Auth\Backend\AbstractBasic;
  42. use Sabre\DAV\Exception\NotAuthenticated;
  43. use Sabre\DAV\Exception\ServiceUnavailable;
  44. use Sabre\HTTP\RequestInterface;
  45. use Sabre\HTTP\ResponseInterface;
  46. class Auth extends AbstractBasic {
  47. const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND';
  48. /** @var ISession */
  49. private $session;
  50. /** @var Session */
  51. private $userSession;
  52. /** @var IRequest */
  53. private $request;
  54. /** @var string */
  55. private $currentUser;
  56. /** @var Manager */
  57. private $twoFactorManager;
  58. /** @var Throttler */
  59. private $throttler;
  60. /**
  61. * @param ISession $session
  62. * @param Session $userSession
  63. * @param IRequest $request
  64. * @param Manager $twoFactorManager
  65. * @param Throttler $throttler
  66. * @param string $principalPrefix
  67. */
  68. public function __construct(ISession $session,
  69. Session $userSession,
  70. IRequest $request,
  71. Manager $twoFactorManager,
  72. Throttler $throttler,
  73. $principalPrefix = 'principals/users/') {
  74. $this->session = $session;
  75. $this->userSession = $userSession;
  76. $this->twoFactorManager = $twoFactorManager;
  77. $this->request = $request;
  78. $this->throttler = $throttler;
  79. $this->principalPrefix = $principalPrefix;
  80. // setup realm
  81. $defaults = new \OCP\Defaults();
  82. $this->realm = $defaults->getName();
  83. }
  84. /**
  85. * Whether the user has initially authenticated via DAV
  86. *
  87. * This is required for WebDAV clients that resent the cookies even when the
  88. * account was changed.
  89. *
  90. * @see https://github.com/owncloud/core/issues/13245
  91. *
  92. * @param string $username
  93. * @return bool
  94. */
  95. public function isDavAuthenticated($username) {
  96. return !is_null($this->session->get(self::DAV_AUTHENTICATED)) &&
  97. $this->session->get(self::DAV_AUTHENTICATED) === $username;
  98. }
  99. /**
  100. * Validates a username and password
  101. *
  102. * This method should return true or false depending on if login
  103. * succeeded.
  104. *
  105. * @param string $username
  106. * @param string $password
  107. * @return bool
  108. * @throws PasswordLoginForbidden
  109. */
  110. protected function validateUserPass($username, $password) {
  111. if ($this->userSession->isLoggedIn() &&
  112. $this->isDavAuthenticated($this->userSession->getUser()->getUID())
  113. ) {
  114. \OC_Util::setupFS($this->userSession->getUser()->getUID());
  115. $this->session->close();
  116. return true;
  117. } else {
  118. \OC_Util::setupFS(); //login hooks may need early access to the filesystem
  119. try {
  120. if ($this->userSession->logClientIn($username, $password, $this->request, $this->throttler)) {
  121. \OC_Util::setupFS($this->userSession->getUser()->getUID());
  122. $this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID());
  123. $this->session->close();
  124. return true;
  125. } else {
  126. $this->session->close();
  127. return false;
  128. }
  129. } catch (PasswordLoginForbiddenException $ex) {
  130. $this->session->close();
  131. throw new PasswordLoginForbidden();
  132. }
  133. }
  134. }
  135. /**
  136. * @param RequestInterface $request
  137. * @param ResponseInterface $response
  138. * @return array
  139. * @throws NotAuthenticated
  140. * @throws ServiceUnavailable
  141. */
  142. function check(RequestInterface $request, ResponseInterface $response) {
  143. try {
  144. $result = $this->auth($request, $response);
  145. return $result;
  146. } catch (NotAuthenticated $e) {
  147. throw $e;
  148. } catch (Exception $e) {
  149. $class = get_class($e);
  150. $msg = $e->getMessage();
  151. \OC::$server->getLogger()->logException($e);
  152. throw new ServiceUnavailable("$class: $msg");
  153. }
  154. }
  155. /**
  156. * Checks whether a CSRF check is required on the request
  157. *
  158. * @return bool
  159. */
  160. private function requiresCSRFCheck() {
  161. // GET requires no check at all
  162. if($this->request->getMethod() === 'GET') {
  163. return false;
  164. }
  165. // Official ownCloud clients require no checks
  166. if($this->request->isUserAgent([
  167. IRequest::USER_AGENT_CLIENT_DESKTOP,
  168. IRequest::USER_AGENT_CLIENT_ANDROID,
  169. IRequest::USER_AGENT_CLIENT_IOS,
  170. ])) {
  171. return false;
  172. }
  173. // If not logged-in no check is required
  174. if(!$this->userSession->isLoggedIn()) {
  175. return false;
  176. }
  177. // POST always requires a check
  178. if($this->request->getMethod() === 'POST') {
  179. return true;
  180. }
  181. // If logged-in AND DAV authenticated no check is required
  182. if($this->userSession->isLoggedIn() &&
  183. $this->isDavAuthenticated($this->userSession->getUser()->getUID())) {
  184. return false;
  185. }
  186. return true;
  187. }
  188. /**
  189. * @param RequestInterface $request
  190. * @param ResponseInterface $response
  191. * @return array
  192. * @throws NotAuthenticated
  193. */
  194. private function auth(RequestInterface $request, ResponseInterface $response) {
  195. $forcedLogout = false;
  196. if(!$this->request->passesCSRFCheck() &&
  197. $this->requiresCSRFCheck()) {
  198. // In case of a fail with POST we need to recheck the credentials
  199. if($this->request->getMethod() === 'POST') {
  200. $forcedLogout = true;
  201. } else {
  202. $response->setStatus(401);
  203. throw new \Sabre\DAV\Exception\NotAuthenticated('CSRF check not passed.');
  204. }
  205. }
  206. if($forcedLogout) {
  207. $this->userSession->logout();
  208. } else {
  209. if($this->twoFactorManager->needsSecondFactor($this->userSession->getUser())) {
  210. throw new \Sabre\DAV\Exception\NotAuthenticated('2FA challenge not passed.');
  211. }
  212. if (\OC_User::handleApacheAuth() ||
  213. //Fix for broken webdav clients
  214. ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) ||
  215. //Well behaved clients that only send the cookie are allowed
  216. ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && $request->getHeader('Authorization') === null)
  217. ) {
  218. $user = $this->userSession->getUser()->getUID();
  219. \OC_Util::setupFS($user);
  220. $this->currentUser = $user;
  221. $this->session->close();
  222. return [true, $this->principalPrefix . $user];
  223. }
  224. }
  225. if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With')))) {
  226. // do not re-authenticate over ajax, use dummy auth name to prevent browser popup
  227. $response->addHeader('WWW-Authenticate','DummyBasic realm="' . $this->realm . '"');
  228. $response->setStatus(401);
  229. throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
  230. }
  231. $data = parent::check($request, $response);
  232. if($data[0] === true) {
  233. $startPos = strrpos($data[1], '/') + 1;
  234. $user = $this->userSession->getUser()->getUID();
  235. $data[1] = substr_replace($data[1], $user, $startPos);
  236. }
  237. return $data;
  238. }
  239. }