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 */ 32namespace OC\Core\Controller; 33 34use OC\Authentication\Events\AppPasswordCreatedEvent; 35use OC\Authentication\Exceptions\InvalidTokenException; 36use OC\Authentication\Exceptions\PasswordlessTokenException; 37use OC\Authentication\Token\IProvider; 38use OC\Authentication\Token\IToken; 39use OCA\OAuth2\Db\AccessToken; 40use OCA\OAuth2\Db\AccessTokenMapper; 41use OCA\OAuth2\Db\ClientMapper; 42use OCP\AppFramework\Controller; 43use OCP\AppFramework\Http; 44use OCP\AppFramework\Http\Response; 45use OCP\AppFramework\Http\StandaloneTemplateResponse; 46use OCP\Defaults; 47use OCP\EventDispatcher\IEventDispatcher; 48use OCP\IL10N; 49use OCP\IRequest; 50use OCP\ISession; 51use OCP\IURLGenerator; 52use OCP\IUserSession; 53use OCP\Security\ICrypto; 54use OCP\Security\ISecureRandom; 55use OCP\Session\Exceptions\SessionNotAvailableException; 56 57class ClientFlowLoginController extends Controller { 58 /** @var IUserSession */ 59 private $userSession; 60 /** @var IL10N */ 61 private $l10n; 62 /** @var Defaults */ 63 private $defaults; 64 /** @var ISession */ 65 private $session; 66 /** @var IProvider */ 67 private $tokenProvider; 68 /** @var ISecureRandom */ 69 private $random; 70 /** @var IURLGenerator */ 71 private $urlGenerator; 72 /** @var ClientMapper */ 73 private $clientMapper; 74 /** @var AccessTokenMapper */ 75 private $accessTokenMapper; 76 /** @var ICrypto */ 77 private $crypto; 78 /** @var IEventDispatcher */ 79 private $eventDispatcher; 80 81 public const STATE_NAME = 'client.flow.state.token'; 82 83 /** 84 * @param string $appName 85 * @param IRequest $request 86 * @param IUserSession $userSession 87 * @param IL10N $l10n 88 * @param Defaults $defaults 89 * @param ISession $session 90 * @param IProvider $tokenProvider 91 * @param ISecureRandom $random 92 * @param IURLGenerator $urlGenerator 93 * @param ClientMapper $clientMapper 94 * @param AccessTokenMapper $accessTokenMapper 95 * @param ICrypto $crypto 96 * @param IEventDispatcher $eventDispatcher 97 */ 98 public function __construct($appName, 99 IRequest $request, 100 IUserSession $userSession, 101 IL10N $l10n, 102 Defaults $defaults, 103 ISession $session, 104 IProvider $tokenProvider, 105 ISecureRandom $random, 106 IURLGenerator $urlGenerator, 107 ClientMapper $clientMapper, 108 AccessTokenMapper $accessTokenMapper, 109 ICrypto $crypto, 110 IEventDispatcher $eventDispatcher) { 111 parent::__construct($appName, $request); 112 $this->userSession = $userSession; 113 $this->l10n = $l10n; 114 $this->defaults = $defaults; 115 $this->session = $session; 116 $this->tokenProvider = $tokenProvider; 117 $this->random = $random; 118 $this->urlGenerator = $urlGenerator; 119 $this->clientMapper = $clientMapper; 120 $this->accessTokenMapper = $accessTokenMapper; 121 $this->crypto = $crypto; 122 $this->eventDispatcher = $eventDispatcher; 123 } 124 125 /** 126 * @return string 127 */ 128 private function getClientName() { 129 $userAgent = $this->request->getHeader('USER_AGENT'); 130 return $userAgent !== '' ? $userAgent : 'unknown'; 131 } 132 133 /** 134 * @param string $stateToken 135 * @return bool 136 */ 137 private function isValidToken($stateToken) { 138 $currentToken = $this->session->get(self::STATE_NAME); 139 if (!is_string($stateToken) || !is_string($currentToken)) { 140 return false; 141 } 142 return hash_equals($currentToken, $stateToken); 143 } 144 145 /** 146 * @return StandaloneTemplateResponse 147 */ 148 private function stateTokenForbiddenResponse() { 149 $response = new StandaloneTemplateResponse( 150 $this->appName, 151 '403', 152 [ 153 'message' => $this->l10n->t('State token does not match'), 154 ], 155 'guest' 156 ); 157 $response->setStatus(Http::STATUS_FORBIDDEN); 158 return $response; 159 } 160 161 /** 162 * @PublicPage 163 * @NoCSRFRequired 164 * @UseSession 165 * 166 * @param string $clientIdentifier 167 * 168 * @return StandaloneTemplateResponse 169 */ 170 public function showAuthPickerPage($clientIdentifier = '') { 171 $clientName = $this->getClientName(); 172 $client = null; 173 if ($clientIdentifier !== '') { 174 $client = $this->clientMapper->getByIdentifier($clientIdentifier); 175 $clientName = $client->getName(); 176 } 177 178 // No valid clientIdentifier given and no valid API Request (APIRequest header not set) 179 $clientRequest = $this->request->getHeader('OCS-APIREQUEST'); 180 if ($clientRequest !== 'true' && $client === null) { 181 return new StandaloneTemplateResponse( 182 $this->appName, 183 'error', 184 [ 185 'errors' => 186 [ 187 [ 188 'error' => 'Access Forbidden', 189 'hint' => 'Invalid request', 190 ], 191 ], 192 ], 193 'guest' 194 ); 195 } 196 197 $stateToken = $this->random->generate( 198 64, 199 ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS 200 ); 201 $this->session->set(self::STATE_NAME, $stateToken); 202 203 $csp = new Http\ContentSecurityPolicy(); 204 if ($client) { 205 $csp->addAllowedFormActionDomain($client->getRedirectUri()); 206 } else { 207 $csp->addAllowedFormActionDomain('nc://*'); 208 } 209 210 $response = new StandaloneTemplateResponse( 211 $this->appName, 212 'loginflow/authpicker', 213 [ 214 'client' => $clientName, 215 'clientIdentifier' => $clientIdentifier, 216 'instanceName' => $this->defaults->getName(), 217 'urlGenerator' => $this->urlGenerator, 218 'stateToken' => $stateToken, 219 'serverHost' => $this->getServerPath(), 220 'oauthState' => $this->session->get('oauth.state'), 221 ], 222 'guest' 223 ); 224 225 $response->setContentSecurityPolicy($csp); 226 return $response; 227 } 228 229 /** 230 * @NoAdminRequired 231 * @NoCSRFRequired 232 * @NoSameSiteCookieRequired 233 * @UseSession 234 * 235 * @param string $stateToken 236 * @param string $clientIdentifier 237 * @return StandaloneTemplateResponse 238 */ 239 public function grantPage($stateToken = '', 240 $clientIdentifier = '') { 241 if (!$this->isValidToken($stateToken)) { 242 return $this->stateTokenForbiddenResponse(); 243 } 244 245 $clientName = $this->getClientName(); 246 $client = null; 247 if ($clientIdentifier !== '') { 248 $client = $this->clientMapper->getByIdentifier($clientIdentifier); 249 $clientName = $client->getName(); 250 } 251 252 $csp = new Http\ContentSecurityPolicy(); 253 if ($client) { 254 $csp->addAllowedFormActionDomain($client->getRedirectUri()); 255 } else { 256 $csp->addAllowedFormActionDomain('nc://*'); 257 } 258 259 $response = new StandaloneTemplateResponse( 260 $this->appName, 261 'loginflow/grant', 262 [ 263 'client' => $clientName, 264 'clientIdentifier' => $clientIdentifier, 265 'instanceName' => $this->defaults->getName(), 266 'urlGenerator' => $this->urlGenerator, 267 'stateToken' => $stateToken, 268 'serverHost' => $this->getServerPath(), 269 'oauthState' => $this->session->get('oauth.state'), 270 ], 271 'guest' 272 ); 273 274 $response->setContentSecurityPolicy($csp); 275 return $response; 276 } 277 278 /** 279 * @NoAdminRequired 280 * @UseSession 281 * 282 * @param string $stateToken 283 * @param string $clientIdentifier 284 * @return Http\RedirectResponse|Response 285 */ 286 public function generateAppPassword($stateToken, 287 $clientIdentifier = '') { 288 if (!$this->isValidToken($stateToken)) { 289 $this->session->remove(self::STATE_NAME); 290 return $this->stateTokenForbiddenResponse(); 291 } 292 293 $this->session->remove(self::STATE_NAME); 294 295 try { 296 $sessionId = $this->session->getId(); 297 } catch (SessionNotAvailableException $ex) { 298 $response = new Response(); 299 $response->setStatus(Http::STATUS_FORBIDDEN); 300 return $response; 301 } 302 303 try { 304 $sessionToken = $this->tokenProvider->getToken($sessionId); 305 $loginName = $sessionToken->getLoginName(); 306 try { 307 $password = $this->tokenProvider->getPassword($sessionToken, $sessionId); 308 } catch (PasswordlessTokenException $ex) { 309 $password = null; 310 } 311 } catch (InvalidTokenException $ex) { 312 $response = new Response(); 313 $response->setStatus(Http::STATUS_FORBIDDEN); 314 return $response; 315 } 316 317 $clientName = $this->getClientName(); 318 $client = false; 319 if ($clientIdentifier !== '') { 320 $client = $this->clientMapper->getByIdentifier($clientIdentifier); 321 $clientName = $client->getName(); 322 } 323 324 $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); 325 $uid = $this->userSession->getUser()->getUID(); 326 $generatedToken = $this->tokenProvider->generateToken( 327 $token, 328 $uid, 329 $loginName, 330 $password, 331 $clientName, 332 IToken::PERMANENT_TOKEN, 333 IToken::DO_NOT_REMEMBER 334 ); 335 336 if ($client) { 337 $code = $this->random->generate(128, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); 338 $accessToken = new AccessToken(); 339 $accessToken->setClientId($client->getId()); 340 $accessToken->setEncryptedToken($this->crypto->encrypt($token, $code)); 341 $accessToken->setHashedCode(hash('sha512', $code)); 342 $accessToken->setTokenId($generatedToken->getId()); 343 $this->accessTokenMapper->insert($accessToken); 344 345 $redirectUri = $client->getRedirectUri(); 346 347 if (parse_url($redirectUri, PHP_URL_QUERY)) { 348 $redirectUri .= '&'; 349 } else { 350 $redirectUri .= '?'; 351 } 352 353 $redirectUri .= sprintf( 354 'state=%s&code=%s', 355 urlencode($this->session->get('oauth.state')), 356 urlencode($code) 357 ); 358 $this->session->remove('oauth.state'); 359 } else { 360 $redirectUri = 'nc://login/server:' . $this->getServerPath() . '&user:' . urlencode($loginName) . '&password:' . urlencode($token); 361 362 // Clear the token from the login here 363 $this->tokenProvider->invalidateToken($sessionId); 364 } 365 366 $this->eventDispatcher->dispatchTyped( 367 new AppPasswordCreatedEvent($generatedToken) 368 ); 369 370 return new Http\RedirectResponse($redirectUri); 371 } 372 373 /** 374 * @PublicPage 375 */ 376 public function apptokenRedirect(string $stateToken, string $user, string $password) { 377 if (!$this->isValidToken($stateToken)) { 378 return $this->stateTokenForbiddenResponse(); 379 } 380 381 try { 382 $token = $this->tokenProvider->getToken($password); 383 if ($token->getLoginName() !== $user) { 384 throw new InvalidTokenException('login name does not match'); 385 } 386 } catch (InvalidTokenException $e) { 387 $response = new StandaloneTemplateResponse( 388 $this->appName, 389 '403', 390 [ 391 'message' => $this->l10n->t('Invalid app password'), 392 ], 393 'guest' 394 ); 395 $response->setStatus(Http::STATUS_FORBIDDEN); 396 return $response; 397 } 398 399 $redirectUri = 'nc://login/server:' . $this->getServerPath() . '&user:' . urlencode($user) . '&password:' . urlencode($password); 400 return new Http\RedirectResponse($redirectUri); 401 } 402 403 private function getServerPath(): string { 404 $serverPostfix = ''; 405 406 if (strpos($this->request->getRequestUri(), '/index.php') !== false) { 407 $serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/index.php')); 408 } elseif (strpos($this->request->getRequestUri(), '/login/flow') !== false) { 409 $serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/login/flow')); 410 } 411 412 $protocol = $this->request->getServerProtocol(); 413 414 if ($protocol !== "https") { 415 $xForwardedProto = $this->request->getHeader('X-Forwarded-Proto'); 416 $xForwardedSSL = $this->request->getHeader('X-Forwarded-Ssl'); 417 if ($xForwardedProto === 'https' || $xForwardedSSL === 'on') { 418 $protocol = 'https'; 419 } 420 } 421 422 return $protocol . "://" . $this->request->getServerHost() . $serverPostfix; 423 } 424} 425