request->getHeader('USER_AGENT'); return $userAgent !== '' ? $userAgent : 'unknown'; } private function isValidToken(string $stateToken): bool { $currentToken = $this->session->get(self::STATE_NAME); if (!is_string($currentToken)) { return false; } return hash_equals($currentToken, $stateToken); } private function stateTokenForbiddenResponse(): StandaloneTemplateResponse { $response = new StandaloneTemplateResponse( $this->appName, '403', [ 'message' => $this->l10n->t('State token does not match'), ], 'guest' ); $response->setStatus(Http::STATUS_FORBIDDEN); return $response; } #[PublicPage] #[NoCSRFRequired] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/login/flow')] public function showAuthPickerPage(string $clientIdentifier = '', string $user = '', int $direct = 0): StandaloneTemplateResponse { $clientName = $this->getClientName(); $client = null; if ($clientIdentifier !== '') { $client = $this->clientMapper->getByIdentifier($clientIdentifier); $clientName = $client->getName(); } // No valid clientIdentifier given and no valid API Request (APIRequest header not set) $clientRequest = $this->request->getHeader('OCS-APIREQUEST'); if ($clientRequest !== 'true' && $client === null) { return new StandaloneTemplateResponse( $this->appName, 'error', [ 'errors' => [ [ 'error' => 'Access Forbidden', 'hint' => 'Invalid request', ], ], ], 'guest' ); } $stateToken = $this->random->generate( 64, ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS ); $this->session->set(self::STATE_NAME, $stateToken); $csp = new Http\ContentSecurityPolicy(); if ($client) { $csp->addAllowedFormActionDomain($client->getRedirectUri()); } else { $csp->addAllowedFormActionDomain('nc://*'); } $response = new StandaloneTemplateResponse( $this->appName, 'loginflow/authpicker', [ 'client' => $clientName, 'clientIdentifier' => $clientIdentifier, 'instanceName' => $this->defaults->getName(), 'urlGenerator' => $this->urlGenerator, 'stateToken' => $stateToken, 'serverHost' => $this->getServerPath(), 'oauthState' => $this->session->get('oauth.state'), 'user' => $user, 'direct' => $direct, ], 'guest' ); $response->setContentSecurityPolicy($csp); return $response; } /** * @NoSameSiteCookieRequired */ #[NoAdminRequired] #[NoCSRFRequired] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/login/flow/grant')] public function grantPage(string $stateToken = '', string $clientIdentifier = '', int $direct = 0): StandaloneTemplateResponse { if (!$this->isValidToken($stateToken)) { return $this->stateTokenForbiddenResponse(); } $clientName = $this->getClientName(); $client = null; if ($clientIdentifier !== '') { $client = $this->clientMapper->getByIdentifier($clientIdentifier); $clientName = $client->getName(); } $csp = new Http\ContentSecurityPolicy(); if ($client) { $csp->addAllowedFormActionDomain($client->getRedirectUri()); } else { $csp->addAllowedFormActionDomain('nc://*'); } /** @var IUser $user */ $user = $this->userSession->getUser(); $response = new StandaloneTemplateResponse( $this->appName, 'loginflow/grant', [ 'userId' => $user->getUID(), 'userDisplayName' => $user->getDisplayName(), 'client' => $clientName, 'clientIdentifier' => $clientIdentifier, 'instanceName' => $this->defaults->getName(), 'urlGenerator' => $this->urlGenerator, 'stateToken' => $stateToken, 'serverHost' => $this->getServerPath(), 'oauthState' => $this->session->get('oauth.state'), 'direct' => $direct, ], 'guest' ); $response->setContentSecurityPolicy($csp); return $response; } /** * @return Http\RedirectResponse|Response */ #[NoAdminRequired] #[UseSession] #[FrontpageRoute(verb: 'POST', url: '/login/flow')] public function generateAppPassword(string $stateToken, string $clientIdentifier = '') { if (!$this->isValidToken($stateToken)) { $this->session->remove(self::STATE_NAME); return $this->stateTokenForbiddenResponse(); } $this->session->remove(self::STATE_NAME); try { $sessionId = $this->session->getId(); } catch (SessionNotAvailableException $ex) { $response = new Response(); $response->setStatus(Http::STATUS_FORBIDDEN); return $response; } try { $sessionToken = $this->tokenProvider->getToken($sessionId); $loginName = $sessionToken->getLoginName(); try { $password = $this->tokenProvider->getPassword($sessionToken, $sessionId); } catch (PasswordlessTokenException $ex) { $password = null; } } catch (InvalidTokenException $ex) { $response = new Response(); $response->setStatus(Http::STATUS_FORBIDDEN); return $response; } $clientName = $this->getClientName(); $client = false; if ($clientIdentifier !== '') { $client = $this->clientMapper->getByIdentifier($clientIdentifier); $clientName = $client->getName(); } $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); $uid = $this->userSession->getUser()->getUID(); $generatedToken = $this->tokenProvider->generateToken( $token, $uid, $loginName, $password, $clientName, IToken::PERMANENT_TOKEN, IToken::DO_NOT_REMEMBER ); if ($client) { $code = $this->random->generate(128, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); $accessToken = new AccessToken(); $accessToken->setClientId($client->getId()); $accessToken->setEncryptedToken($this->crypto->encrypt($token, $code)); $accessToken->setHashedCode(hash('sha512', $code)); $accessToken->setTokenId($generatedToken->getId()); $accessToken->setCodeCreatedAt($this->timeFactory->now()->getTimestamp()); $this->accessTokenMapper->insert($accessToken); $redirectUri = $client->getRedirectUri(); if (parse_url($redirectUri, PHP_URL_QUERY)) { $redirectUri .= '&'; } else { $redirectUri .= '?'; } $redirectUri .= sprintf( 'state=%s&code=%s', urlencode($this->session->get('oauth.state')), urlencode($code) ); $this->session->remove('oauth.state'); } else { $redirectUri = 'nc://login/server:' . $this->getServerPath() . '&user:' . urlencode($loginName) . '&password:' . urlencode($token); // Clear the token from the login here $this->tokenProvider->invalidateToken($sessionId); } $this->eventDispatcher->dispatchTyped( new AppPasswordCreatedEvent($generatedToken) ); return new Http\RedirectResponse($redirectUri); } #[PublicPage] #[FrontpageRoute(verb: 'POST', url: '/login/flow/apptoken')] public function apptokenRedirect(string $stateToken, string $user, string $password): Response { if (!$this->isValidToken($stateToken)) { return $this->stateTokenForbiddenResponse(); } try { $token = $this->tokenProvider->getToken($password); if ($token->getLoginName() !== $user) { throw new InvalidTokenException('login name does not match'); } } catch (InvalidTokenException $e) { $response = new StandaloneTemplateResponse( $this->appName, '403', [ 'message' => $this->l10n->t('Invalid app password'), ], 'guest' ); $response->setStatus(Http::STATUS_FORBIDDEN); return $response; } $redirectUri = 'nc://login/server:' . $this->getServerPath() . '&user:' . urlencode($user) . '&password:' . urlencode($password); return new Http\RedirectResponse($redirectUri); } private function getServerPath(): string { $serverPostfix = ''; if (str_contains($this->request->getRequestUri(), '/index.php')) { $serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/index.php')); } elseif (str_contains($this->request->getRequestUri(), '/login/flow')) { $serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/login/flow')); } $protocol = $this->request->getServerProtocol(); if ($protocol !== 'https') { $xForwardedProto = $this->request->getHeader('X-Forwarded-Proto'); $xForwardedSSL = $this->request->getHeader('X-Forwarded-Ssl'); if ($xForwardedProto === 'https' || $xForwardedSSL === 'on') { $protocol = 'https'; } } return $protocol . '://' . $this->request->getServerHost() . $serverPostfix; } }