gateway = $gateway; $this->service = $service; $this->httpClient = $httpClient; $this->baseUrl = $requestBaseUrl; $this->credentials = $credentials; } /** * @param Session $session * @param string[] $scopes * @param string $uriAfterRedirect * * @throws InvalidArgumentException * @throws GoogleCredentialsException * * @return RedirectResponse */ public function requestScopeAuthorization( Session $session, array $scopes = [], string $uriAfterRedirect = 'index.php?module=welcome&action=settings' ): RedirectResponse { if (count($scopes) === 0) { throw new InvalidArgumentException('No scopes for Google authorization defined.'); } $this->credentials->validate(); $clientId = $this->credentials->getClientId(); $session->setValue(self::SESSION_SEGMENT, self::SESSION_KEY_URI, $uriAfterRedirect); $csrfToken = $session->createCsrfToken(self::CSRF_KEY); $redirectUri = $this->credentials->getRedirectUri(); if ($redirectUri === null || $redirectUri === '') { $redirectUri = $this->getDefaultRedirectUri(); } $scopeParam = implode(' ', $scopes); $queryParams = [ 'client_id' => $clientId, 'redirect_uri' => $redirectUri, 'response_type' => 'code', 'scope' => $scopeParam, 'access_type' => 'offline', 'include_granted_scopes' => 'true', 'state' => $csrfToken, ]; $url = sprintf('%s?%s', self::URL_AUTHORIZATION_CODE, http_build_query($queryParams)); return RedirectResponse::createFromUrl($url); } /** * @param Session $session * @param Request $request * @param int $userId * * @throws Exception * * @return RedirectResponse */ public function authorizationCallback(Session $session, Request $request, int $userId): RedirectResponse { $code = $request->get->get('code'); $scopes = explode(' ', $request->get->get('scope', '')); $error = $request->get->get('error'); $csrfToken = $request->get->get('state'); if ( $csrfToken === null || !$session->isCsrfTokenValid(self::CSRF_KEY, $csrfToken, true) ) { throw new CsrfViolationException('Invalid CSRF token in authorization.'); } // error in callback means the user declined access if ($error !== null) { $this->logger->error( 'User consent rejected by "user_id={user}" original error: "{error}"', ['user_id' => $userId, 'error' => $error] ); throw new UserConsentException($error); } // find/create account try { $account = $this->gateway->getAccountByUser($userId); } catch (GoogleAccountNotFoundException $e) { $account = $this->service->createAccount($userId, null); } // store granted scopes $this->service->deleteAccountScopes($account->getId()); foreach ($scopes as $scope) { $this->service->saveAccountScope($account->getId(), $scope); } // fetch and save refresh token $array = $this->fetchTokenByAuthCode($code); $tokenResponse = GoogleTokenResponseData::createfromResponseArray($array); if ($tokenResponse->hasRefreshToken()) { $account = new GoogleAccountData( $account->getId(), $account->getUserId(), $account->getIdentifier(), $tokenResponse->getRefreshToken() ); $this->service->saveAccount($account); } // cache access token $accessToken = new GoogleAccessTokenData( $account->getId(), $tokenResponse->getAccessToken(), $tokenResponse->getExpirationDate() ); $this->service->saveAccessToken($accessToken); // read redirect uri from session $redirectUri = $session->getValue( self::SESSION_SEGMENT, self::SESSION_KEY_URI, 'index.php?module=googleapi&action=edit', true ); return RedirectResponse::createFromUrl($redirectUri); } /** * @param GoogleAccountData $account * * @throws NoRefreshTokenException * @throws GoogleCredentialsException * @throws AuthorizationExpiredException * * @return GoogleAccessTokenData */ public function refreshAccessToken(GoogleAccountData $account): GoogleAccessTokenData { $this->credentials->validate(); $refresh_token = $account->getRefreshToken(); if ($refresh_token === null) { $this->logger->warning( 'User "id={user_id} has no Google refresh token.', ['user_id' => $account->getUserId()] ); try { $refresh_token = $this->gateway->getAccessToken($account->getId())->getToken(); } catch (NoAccessTokenException $e) { throw new NoRefreshTokenException('Account not authorized.'); } } $postData = [ 'refresh_token' => $refresh_token, 'client_id' => $this->credentials->getClientId(), 'client_secret' => $this->credentials->getClientSecret(), 'grant_type' => 'refresh_token', ]; try { $array = $this->apiRequest('POST', self::URL_TOKEN_REFRESH, $postData); } catch (ClientErrorException $e) { $this->logger->error( 'Fetching new Google access token failed. Repeat the Authorization process!', ['exception' => $e] ); $this->revokeAuthorization($account); throw new AuthorizationExpiredException( 'Failed to fetch access token. Try to repeat the Google authorization process.', $e->getCode(), $e ); } $tokenResponse = GoogleTokenResponseData::createfromResponseArray($array); $accessToken = new GoogleAccessTokenData( $account->getId(), $tokenResponse->getAccessToken(), $tokenResponse->getExpirationDate() ); $this->service->saveAccessToken($accessToken); return $accessToken; } /** * @param GoogleAccountData $account * * @return GoogleAccountData */ public function revokeAuthorization(GoogleAccountData $account): GoogleAccountData { try { $accessToken = $this->gateway->getAccessToken($account->getId()); $this->revokeToken($accessToken->getToken()); $this->service->deleteAccessToken($accessToken); } catch (NoAccessTokenException $e) { } if ($account->getRefreshToken() !== null) { $this->revokeToken($account->getRefreshToken()); $account = new GoogleAccountData( $account->getId(), $account->getUserId(), $account->getIdentifier(), null ); $this->service->saveAccount($account); } $this->service->deleteAccountScopes($account->getId()); return $account; } /** * @return string */ public function getDefaultRedirectUri(): string { return sprintf('%s/index.php?module=googleapi&action=redirect', $this->baseUrl); } /** * @param string $token refresh_token or access_token * * @return bool success */ public function revokeToken(string $token): bool { $url = sprintf('%s?token=%s', self::URL_TOKEN_REVOKE, $token); try { $this->apiRequest('GET', $url, null, []); } catch (ClientErrorException $e) { return true; } catch (ServerErrorException $e) { return false; } return true; } /** * @param string $authorizationCode * * @throws GoogleCredentialsException * * @return array */ private function fetchTokenByAuthCode(string $authorizationCode): array { $redirectUri = $this->credentials->getRedirectUri(); if (empty($redirectUri)) { $redirectUri = $this->getDefaultRedirectUri(); } $this->credentials->validate(); $postData = [ 'code' => $authorizationCode, 'client_id' => $this->credentials->getClientId(), 'client_secret' => $this->credentials->getClientSecret(), 'redirect_uri' => $redirectUri, 'grant_type' => 'authorization_code', ]; return $this->apiRequest('POST', self::URL_TOKEN_FETCH, $postData, []); } /** * @param string $method * @param string $url * @param array|null $data * @param array $headers * * @throws ClientErrorException * @throws ServerErrorException * * @return array */ private function apiRequest($method, $url, $data = null, $headers = []): array { $requestBody = null; if ($data !== null) { $headers['Content-Type'] = 'application/json'; $requestBody = json_encode($data); } $request = new ClientRequest($method, $url, $headers, $requestBody); try { $response = $this->httpClient->sendRequest($request); $this->logger->debug( 'Google authorization request succeeded: {uri}', ['uri' => $request->getUri(), 'request' => $request, 'response' => $response] ); } catch (TransferErrorExceptionInterface $e) { $code = $e->getCode(); $this->logger->warning( 'Google authorization request failed: {uri} ERROR {code}', [ 'uri' => $request->getUri(), 'code' => $code, 'request' => $request, 'response' => $e->getResponse(), ] ); if ($code > 399 && $code < 500) { throw new ClientErrorException($e->getMessage(), $e->getCode(), $e); } if ($code > 499 && $code < 600) { throw new ServerErrorException($e->getMessage(), $e->getCode(), $e); } } $contentType = $response->getHeaderLine('content-type'); $responseBody = $response->getBody()->getContents(); $result = []; if ($responseBody !== '' && StringUtil::startsWith($contentType, 'application/json')) { $result = json_decode($responseBody, true); } return $result; } }