ClientFlowLoginController.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
  4. *
  5. * @author Bjoern Schiessle <bjoern@schiessle.org>
  6. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  7. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Lukas Reschke <lukas@statuscode.ch>
  10. * @author Mario Danic <mario@lovelyhq.com>
  11. * @author Morris Jobke <hey@morrisjobke.de>
  12. * @author Roeland Jago Douma <roeland@famdouma.nl>
  13. * @author RussellAult <RussellAult@users.noreply.github.com>
  14. * @author Sergej Nikolaev <kinolaev@gmail.com>
  15. *
  16. * @license GNU AGPL version 3 or any later version
  17. *
  18. * This program is free software: you can redistribute it and/or modify
  19. * it under the terms of the GNU Affero General Public License as
  20. * published by the Free Software Foundation, either version 3 of the
  21. * License, or (at your option) any later version.
  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
  29. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  30. *
  31. */
  32. namespace OC\Core\Controller;
  33. use OC\Authentication\Events\AppPasswordCreatedEvent;
  34. use OC\Authentication\Exceptions\InvalidTokenException;
  35. use OC\Authentication\Exceptions\PasswordlessTokenException;
  36. use OC\Authentication\Token\IProvider;
  37. use OC\Authentication\Token\IToken;
  38. use OCA\OAuth2\Db\AccessToken;
  39. use OCA\OAuth2\Db\AccessTokenMapper;
  40. use OCA\OAuth2\Db\ClientMapper;
  41. use OCP\AppFramework\Controller;
  42. use OCP\AppFramework\Http;
  43. use OCP\AppFramework\Http\Response;
  44. use OCP\AppFramework\Http\StandaloneTemplateResponse;
  45. use OCP\Defaults;
  46. use OCP\EventDispatcher\IEventDispatcher;
  47. use OCP\IL10N;
  48. use OCP\IRequest;
  49. use OCP\ISession;
  50. use OCP\IURLGenerator;
  51. use OCP\IUser;
  52. use OCP\IUserSession;
  53. use OCP\Security\ICrypto;
  54. use OCP\Security\ISecureRandom;
  55. use OCP\Session\Exceptions\SessionNotAvailableException;
  56. class ClientFlowLoginController extends Controller {
  57. private IUserSession $userSession;
  58. private IL10N $l10n;
  59. private Defaults $defaults;
  60. private ISession $session;
  61. private IProvider $tokenProvider;
  62. private ISecureRandom $random;
  63. private IURLGenerator $urlGenerator;
  64. private ClientMapper $clientMapper;
  65. private AccessTokenMapper $accessTokenMapper;
  66. private ICrypto $crypto;
  67. private IEventDispatcher $eventDispatcher;
  68. public const STATE_NAME = 'client.flow.state.token';
  69. public function __construct(string $appName,
  70. IRequest $request,
  71. IUserSession $userSession,
  72. IL10N $l10n,
  73. Defaults $defaults,
  74. ISession $session,
  75. IProvider $tokenProvider,
  76. ISecureRandom $random,
  77. IURLGenerator $urlGenerator,
  78. ClientMapper $clientMapper,
  79. AccessTokenMapper $accessTokenMapper,
  80. ICrypto $crypto,
  81. IEventDispatcher $eventDispatcher) {
  82. parent::__construct($appName, $request);
  83. $this->userSession = $userSession;
  84. $this->l10n = $l10n;
  85. $this->defaults = $defaults;
  86. $this->session = $session;
  87. $this->tokenProvider = $tokenProvider;
  88. $this->random = $random;
  89. $this->urlGenerator = $urlGenerator;
  90. $this->clientMapper = $clientMapper;
  91. $this->accessTokenMapper = $accessTokenMapper;
  92. $this->crypto = $crypto;
  93. $this->eventDispatcher = $eventDispatcher;
  94. }
  95. private function getClientName(): string {
  96. $userAgent = $this->request->getHeader('USER_AGENT');
  97. return $userAgent !== '' ? $userAgent : 'unknown';
  98. }
  99. private function isValidToken(string $stateToken): bool {
  100. $currentToken = $this->session->get(self::STATE_NAME);
  101. if (!is_string($currentToken)) {
  102. return false;
  103. }
  104. return hash_equals($currentToken, $stateToken);
  105. }
  106. private function stateTokenForbiddenResponse(): StandaloneTemplateResponse {
  107. $response = new StandaloneTemplateResponse(
  108. $this->appName,
  109. '403',
  110. [
  111. 'message' => $this->l10n->t('State token does not match'),
  112. ],
  113. 'guest'
  114. );
  115. $response->setStatus(Http::STATUS_FORBIDDEN);
  116. return $response;
  117. }
  118. /**
  119. * @PublicPage
  120. * @NoCSRFRequired
  121. * @UseSession
  122. */
  123. public function showAuthPickerPage(string $clientIdentifier = '', string $user = '', int $direct = 0): StandaloneTemplateResponse {
  124. $clientName = $this->getClientName();
  125. $client = null;
  126. if ($clientIdentifier !== '') {
  127. $client = $this->clientMapper->getByIdentifier($clientIdentifier);
  128. $clientName = $client->getName();
  129. }
  130. // No valid clientIdentifier given and no valid API Request (APIRequest header not set)
  131. $clientRequest = $this->request->getHeader('OCS-APIREQUEST');
  132. if ($clientRequest !== 'true' && $client === null) {
  133. return new StandaloneTemplateResponse(
  134. $this->appName,
  135. 'error',
  136. [
  137. 'errors' =>
  138. [
  139. [
  140. 'error' => 'Access Forbidden',
  141. 'hint' => 'Invalid request',
  142. ],
  143. ],
  144. ],
  145. 'guest'
  146. );
  147. }
  148. $stateToken = $this->random->generate(
  149. 64,
  150. ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS
  151. );
  152. $this->session->set(self::STATE_NAME, $stateToken);
  153. $csp = new Http\ContentSecurityPolicy();
  154. if ($client) {
  155. $csp->addAllowedFormActionDomain($client->getRedirectUri());
  156. } else {
  157. $csp->addAllowedFormActionDomain('nc://*');
  158. }
  159. $response = new StandaloneTemplateResponse(
  160. $this->appName,
  161. 'loginflow/authpicker',
  162. [
  163. 'client' => $clientName,
  164. 'clientIdentifier' => $clientIdentifier,
  165. 'instanceName' => $this->defaults->getName(),
  166. 'urlGenerator' => $this->urlGenerator,
  167. 'stateToken' => $stateToken,
  168. 'serverHost' => $this->getServerPath(),
  169. 'oauthState' => $this->session->get('oauth.state'),
  170. 'user' => $user,
  171. 'direct' => $direct,
  172. ],
  173. 'guest'
  174. );
  175. $response->setContentSecurityPolicy($csp);
  176. return $response;
  177. }
  178. /**
  179. * @NoAdminRequired
  180. * @NoCSRFRequired
  181. * @NoSameSiteCookieRequired
  182. * @UseSession
  183. */
  184. public function grantPage(string $stateToken = '',
  185. string $clientIdentifier = '',
  186. int $direct = 0): StandaloneTemplateResponse {
  187. if (!$this->isValidToken($stateToken)) {
  188. return $this->stateTokenForbiddenResponse();
  189. }
  190. $clientName = $this->getClientName();
  191. $client = null;
  192. if ($clientIdentifier !== '') {
  193. $client = $this->clientMapper->getByIdentifier($clientIdentifier);
  194. $clientName = $client->getName();
  195. }
  196. $csp = new Http\ContentSecurityPolicy();
  197. if ($client) {
  198. $csp->addAllowedFormActionDomain($client->getRedirectUri());
  199. } else {
  200. $csp->addAllowedFormActionDomain('nc://*');
  201. }
  202. /** @var IUser $user */
  203. $user = $this->userSession->getUser();
  204. $response = new StandaloneTemplateResponse(
  205. $this->appName,
  206. 'loginflow/grant',
  207. [
  208. 'userId' => $user->getUID(),
  209. 'userDisplayName' => $user->getDisplayName(),
  210. 'client' => $clientName,
  211. 'clientIdentifier' => $clientIdentifier,
  212. 'instanceName' => $this->defaults->getName(),
  213. 'urlGenerator' => $this->urlGenerator,
  214. 'stateToken' => $stateToken,
  215. 'serverHost' => $this->getServerPath(),
  216. 'oauthState' => $this->session->get('oauth.state'),
  217. 'direct' => $direct,
  218. ],
  219. 'guest'
  220. );
  221. $response->setContentSecurityPolicy($csp);
  222. return $response;
  223. }
  224. /**
  225. * @NoAdminRequired
  226. * @UseSession
  227. *
  228. * @return Http\RedirectResponse|Response
  229. */
  230. public function generateAppPassword(string $stateToken,
  231. string $clientIdentifier = '') {
  232. if (!$this->isValidToken($stateToken)) {
  233. $this->session->remove(self::STATE_NAME);
  234. return $this->stateTokenForbiddenResponse();
  235. }
  236. $this->session->remove(self::STATE_NAME);
  237. try {
  238. $sessionId = $this->session->getId();
  239. } catch (SessionNotAvailableException $ex) {
  240. $response = new Response();
  241. $response->setStatus(Http::STATUS_FORBIDDEN);
  242. return $response;
  243. }
  244. try {
  245. $sessionToken = $this->tokenProvider->getToken($sessionId);
  246. $loginName = $sessionToken->getLoginName();
  247. try {
  248. $password = $this->tokenProvider->getPassword($sessionToken, $sessionId);
  249. } catch (PasswordlessTokenException $ex) {
  250. $password = null;
  251. }
  252. } catch (InvalidTokenException $ex) {
  253. $response = new Response();
  254. $response->setStatus(Http::STATUS_FORBIDDEN);
  255. return $response;
  256. }
  257. $clientName = $this->getClientName();
  258. $client = false;
  259. if ($clientIdentifier !== '') {
  260. $client = $this->clientMapper->getByIdentifier($clientIdentifier);
  261. $clientName = $client->getName();
  262. }
  263. $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS);
  264. $uid = $this->userSession->getUser()->getUID();
  265. $generatedToken = $this->tokenProvider->generateToken(
  266. $token,
  267. $uid,
  268. $loginName,
  269. $password,
  270. $clientName,
  271. IToken::PERMANENT_TOKEN,
  272. IToken::DO_NOT_REMEMBER
  273. );
  274. if ($client) {
  275. $code = $this->random->generate(128, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS);
  276. $accessToken = new AccessToken();
  277. $accessToken->setClientId($client->getId());
  278. $accessToken->setEncryptedToken($this->crypto->encrypt($token, $code));
  279. $accessToken->setHashedCode(hash('sha512', $code));
  280. $accessToken->setTokenId($generatedToken->getId());
  281. $this->accessTokenMapper->insert($accessToken);
  282. $redirectUri = $client->getRedirectUri();
  283. if (parse_url($redirectUri, PHP_URL_QUERY)) {
  284. $redirectUri .= '&';
  285. } else {
  286. $redirectUri .= '?';
  287. }
  288. $redirectUri .= sprintf(
  289. 'state=%s&code=%s',
  290. urlencode($this->session->get('oauth.state')),
  291. urlencode($code)
  292. );
  293. $this->session->remove('oauth.state');
  294. } else {
  295. $redirectUri = 'nc://login/server:' . $this->getServerPath() . '&user:' . urlencode($loginName) . '&password:' . urlencode($token);
  296. // Clear the token from the login here
  297. $this->tokenProvider->invalidateToken($sessionId);
  298. }
  299. $this->eventDispatcher->dispatchTyped(
  300. new AppPasswordCreatedEvent($generatedToken)
  301. );
  302. return new Http\RedirectResponse($redirectUri);
  303. }
  304. /**
  305. * @PublicPage
  306. */
  307. public function apptokenRedirect(string $stateToken, string $user, string $password): Response {
  308. if (!$this->isValidToken($stateToken)) {
  309. return $this->stateTokenForbiddenResponse();
  310. }
  311. try {
  312. $token = $this->tokenProvider->getToken($password);
  313. if ($token->getLoginName() !== $user) {
  314. throw new InvalidTokenException('login name does not match');
  315. }
  316. } catch (InvalidTokenException $e) {
  317. $response = new StandaloneTemplateResponse(
  318. $this->appName,
  319. '403',
  320. [
  321. 'message' => $this->l10n->t('Invalid app password'),
  322. ],
  323. 'guest'
  324. );
  325. $response->setStatus(Http::STATUS_FORBIDDEN);
  326. return $response;
  327. }
  328. $redirectUri = 'nc://login/server:' . $this->getServerPath() . '&user:' . urlencode($user) . '&password:' . urlencode($password);
  329. return new Http\RedirectResponse($redirectUri);
  330. }
  331. private function getServerPath(): string {
  332. $serverPostfix = '';
  333. if (strpos($this->request->getRequestUri(), '/index.php') !== false) {
  334. $serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/index.php'));
  335. } elseif (strpos($this->request->getRequestUri(), '/login/flow') !== false) {
  336. $serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/login/flow'));
  337. }
  338. $protocol = $this->request->getServerProtocol();
  339. if ($protocol !== "https") {
  340. $xForwardedProto = $this->request->getHeader('X-Forwarded-Proto');
  341. $xForwardedSSL = $this->request->getHeader('X-Forwarded-Ssl');
  342. if ($xForwardedProto === 'https' || $xForwardedSSL === 'on') {
  343. $protocol = 'https';
  344. }
  345. }
  346. return $protocol . "://" . $this->request->getServerHost() . $serverPostfix;
  347. }
  348. }