|JSONResponse, array{}> * * 200: Login flow credentials returned * 404: Login flow not found or completed */ #[FrontpageRoute(verb: 'POST', url: '/login/v2/poll')] public function poll(string $token): JSONResponse { try { $creds = $this->loginFlowV2Service->poll($token); } catch (LoginFlowV2NotFoundException $e) { return new JSONResponse([], Http::STATUS_NOT_FOUND); } return new JSONResponse($creds->jsonSerialize()); } /** * @NoCSRFRequired * @PublicPage */ #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/login/v2/flow/{token}')] public function landing(string $token, $user = ''): Response { if (!$this->loginFlowV2Service->startLoginFlow($token)) { return $this->loginTokenForbiddenResponse(); } $this->session->set(self::TOKEN_NAME, $token); return new RedirectResponse( $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.showAuthPickerPage', ['user' => $user]) ); } /** * @NoCSRFRequired * @PublicPage */ #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/login/v2/flow')] public function showAuthPickerPage($user = ''): StandaloneTemplateResponse { try { $flow = $this->getFlowByLoginToken(); } catch (LoginFlowV2NotFoundException $e) { return $this->loginTokenForbiddenResponse(); } $stateToken = $this->random->generate( 64, ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS ); $this->session->set(self::STATE_NAME, $stateToken); return new StandaloneTemplateResponse( $this->appName, 'loginflowv2/authpicker', [ 'client' => $flow->getClientName(), 'instanceName' => $this->defaults->getName(), 'urlGenerator' => $this->urlGenerator, 'stateToken' => $stateToken, 'user' => $user, ], 'guest' ); } /** * @NoAdminRequired * @NoCSRFRequired * @NoSameSiteCookieRequired */ #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/login/v2/grant')] public function grantPage(?string $stateToken): StandaloneTemplateResponse { if ($stateToken === null) { return $this->stateTokenMissingResponse(); } if (!$this->isValidStateToken($stateToken)) { return $this->stateTokenForbiddenResponse(); } try { $flow = $this->getFlowByLoginToken(); } catch (LoginFlowV2NotFoundException $e) { return $this->loginTokenForbiddenResponse(); } /** @var IUser $user */ $user = $this->userSession->getUser(); return new StandaloneTemplateResponse( $this->appName, 'loginflowv2/grant', [ 'userId' => $user->getUID(), 'userDisplayName' => $user->getDisplayName(), 'client' => $flow->getClientName(), 'instanceName' => $this->defaults->getName(), 'urlGenerator' => $this->urlGenerator, 'stateToken' => $stateToken, ], 'guest' ); } /** * @PublicPage */ #[FrontpageRoute(verb: 'POST', url: '/login/v2/apptoken')] public function apptokenRedirect(?string $stateToken, string $user, string $password) { if ($stateToken === null) { return $this->stateTokenMissingResponse(); } if (!$this->isValidStateToken($stateToken)) { return $this->stateTokenForbiddenResponse(); } try { $this->getFlowByLoginToken(); } catch (LoginFlowV2NotFoundException $e) { return $this->loginTokenForbiddenResponse(); } $loginToken = $this->session->get(self::TOKEN_NAME); // Clear session variables $this->session->remove(self::TOKEN_NAME); $this->session->remove(self::STATE_NAME); try { $token = \OC::$server->get(\OC\Authentication\Token\IProvider::class)->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; } $result = $this->loginFlowV2Service->flowDoneWithAppPassword($loginToken, $this->getServerPath(), $token->getLoginName(), $password); return $this->handleFlowDone($result); } /** * @NoAdminRequired */ #[UseSession] #[FrontpageRoute(verb: 'POST', url: '/login/v2/grant')] public function generateAppPassword(?string $stateToken): Response { if ($stateToken === null) { return $this->stateTokenMissingResponse(); } if (!$this->isValidStateToken($stateToken)) { return $this->stateTokenForbiddenResponse(); } try { $this->getFlowByLoginToken(); } catch (LoginFlowV2NotFoundException $e) { return $this->loginTokenForbiddenResponse(); } $loginToken = $this->session->get(self::TOKEN_NAME); // Clear session variables $this->session->remove(self::TOKEN_NAME); $this->session->remove(self::STATE_NAME); $sessionId = $this->session->getId(); $result = $this->loginFlowV2Service->flowDone($loginToken, $sessionId, $this->getServerPath(), $this->userId); return $this->handleFlowDone($result); } private function handleFlowDone(bool $result): StandaloneTemplateResponse { if ($result) { return new StandaloneTemplateResponse( $this->appName, 'loginflowv2/done', [], 'guest' ); } $response = new StandaloneTemplateResponse( $this->appName, '403', [ 'message' => $this->l10n->t('Could not complete login'), ], 'guest' ); $response->setStatus(Http::STATUS_FORBIDDEN); return $response; } /** * @NoCSRFRequired * @PublicPage * * Init a login flow * * @return JSONResponse * * 200: Login flow init returned */ #[FrontpageRoute(verb: 'POST', url: '/login/v2')] public function init(): JSONResponse { // Get client user agent $userAgent = $this->request->getHeader('USER_AGENT'); $tokens = $this->loginFlowV2Service->createTokens($userAgent); $data = [ 'poll' => [ 'token' => $tokens->getPollToken(), 'endpoint' => $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.poll') ], 'login' => $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.landing', ['token' => $tokens->getLoginToken()]), ]; return new JSONResponse($data); } private function isValidStateToken(string $stateToken): bool { $currentToken = $this->session->get(self::STATE_NAME); if (!is_string($stateToken) || !is_string($currentToken)) { return false; } return hash_equals($currentToken, $stateToken); } private function stateTokenMissingResponse(): StandaloneTemplateResponse { $response = new StandaloneTemplateResponse( $this->appName, '403', [ 'message' => $this->l10n->t('State token missing'), ], 'guest' ); $response->setStatus(Http::STATUS_FORBIDDEN); return $response; } 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; } /** * @return LoginFlowV2 * @throws LoginFlowV2NotFoundException */ private function getFlowByLoginToken(): LoginFlowV2 { $currentToken = $this->session->get(self::TOKEN_NAME); if (!is_string($currentToken)) { throw new LoginFlowV2NotFoundException('Login token not set in session'); } return $this->loginFlowV2Service->getByLoginToken($currentToken); } private function loginTokenForbiddenResponse(): StandaloneTemplateResponse { $response = new StandaloneTemplateResponse( $this->appName, '403', [ 'message' => $this->l10n->t('Your login token is invalid or has expired'), ], 'guest' ); $response->setStatus(Http::STATUS_FORBIDDEN); return $response; } 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/v2')) { $serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/login/v2')); } $protocol = $this->request->getServerProtocol(); return $protocol . '://' . $this->request->getServerHost() . $serverPostfix; } }