mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-21 21:58:01 -06:00
Make getAuthenticationTokenUsingXYZ asynchronous
As a result, the local webserver now needs to synchronise that with a lock. Otherwise the do_GET function would no longer block, and wouldn't properly be able to return the correct redirect URL. Contributes to issue CURA-8539.
This commit is contained in:
parent
9b55ae6dda
commit
7091c5f3aa
3 changed files with 33 additions and 43 deletions
|
@ -6,15 +6,14 @@ from datetime import datetime
|
|||
from hashlib import sha512
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
import secrets
|
||||
from threading import Lock
|
||||
from typing import Callable, Optional
|
||||
import urllib.parse
|
||||
|
||||
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To download log-in tokens.
|
||||
|
||||
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
|
||||
catalog = i18nCatalog("cura")
|
||||
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
|
@ -25,8 +24,6 @@ class AuthorizationHelpers:
|
|||
def __init__(self, settings: "OAuth2Settings") -> None:
|
||||
self._settings = settings
|
||||
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
|
||||
self._request_lock = Lock()
|
||||
self._auth_response = None # type: Optional[AuthenticationResponse]
|
||||
|
||||
@property
|
||||
def settings(self) -> "OAuth2Settings":
|
||||
|
@ -34,14 +31,13 @@ class AuthorizationHelpers:
|
|||
|
||||
return self._settings
|
||||
|
||||
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse":
|
||||
"""Request the access token from the authorization server.
|
||||
|
||||
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str, callback: Callable[[AuthenticationResponse], None]) -> None:
|
||||
"""
|
||||
Request the access token from the authorization server.
|
||||
:param authorization_code: The authorization code from the 1st step.
|
||||
:param verification_code: The verification code needed for the PKCE extension.
|
||||
:return: An AuthenticationResponse object.
|
||||
:param callback: Once the token has been obtained, this function will be called with the response.
|
||||
"""
|
||||
|
||||
data = {
|
||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
||||
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
|
||||
|
@ -51,26 +47,19 @@ class AuthorizationHelpers:
|
|||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||
}
|
||||
headers = {"Content-type": "application/x-www-form-urlencoded"}
|
||||
self._request_lock.acquire()
|
||||
HttpRequestManager.getInstance().post(
|
||||
self._token_url,
|
||||
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
||||
headers_dict = headers,
|
||||
callback = self.parseTokenResponse
|
||||
callback = lambda response: self.parseTokenResponse(response, callback)
|
||||
)
|
||||
self._request_lock.acquire(timeout = 60) # Block until the request is completed. 1 minute timeout.
|
||||
response = self._auth_response
|
||||
self._auth_response = None
|
||||
self._request_lock.release()
|
||||
return response
|
||||
|
||||
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
|
||||
"""Request the access token from the authorization server using a refresh token.
|
||||
|
||||
:param refresh_token:
|
||||
:return: An AuthenticationResponse object.
|
||||
def getAccessTokenUsingRefreshToken(self, refresh_token: str, callback: Callable[[AuthenticationResponse], None]) -> None:
|
||||
"""
|
||||
Request the access token from the authorization server using a refresh token.
|
||||
:param refresh_token: A long-lived token used to refresh the authentication token.
|
||||
:param callback: Once the token has been obtained, this function will be called with the response.
|
||||
"""
|
||||
|
||||
Logger.log("d", "Refreshing the access token for [%s]", self._settings.OAUTH_SERVER_URL)
|
||||
data = {
|
||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
||||
|
@ -80,20 +69,14 @@ class AuthorizationHelpers:
|
|||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||
}
|
||||
headers = {"Content-type": "application/x-www-form-urlencoded"}
|
||||
self._request_lock.acquire()
|
||||
HttpRequestManager.getInstance().post(
|
||||
self._token_url,
|
||||
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
||||
headers_dict = headers,
|
||||
callback = self.parseTokenResponse
|
||||
callback = lambda response: self.parseTokenResponse(response, callback)
|
||||
)
|
||||
self._request_lock.acquire(timeout = 60) # Block until the request is completed. 1 minute timeout.
|
||||
response = self._auth_response
|
||||
self._auth_response = None
|
||||
self._request_lock.release()
|
||||
return response
|
||||
|
||||
def parseTokenResponse(self, token_response: QNetworkReply) -> None:
|
||||
def parseTokenResponse(self, token_response: QNetworkReply, callback: Callable[[AuthenticationResponse], None]) -> None:
|
||||
"""Parse the token response from the authorization server into an AuthenticationResponse object.
|
||||
|
||||
:param token_response: The JSON string data response from the authorization server.
|
||||
|
@ -101,23 +84,20 @@ class AuthorizationHelpers:
|
|||
"""
|
||||
token_data = HttpRequestManager.readJSON(token_response)
|
||||
if not token_data:
|
||||
self._auth_response = AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response."))
|
||||
self._request_lock.release()
|
||||
callback(AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response.")))
|
||||
return
|
||||
|
||||
if token_response.error() != QNetworkReply.NetworkError.NoError:
|
||||
self._auth_response = AuthenticationResponse(success = False, err_message = token_data["error_description"])
|
||||
self._request_lock.release()
|
||||
callback(AuthenticationResponse(success = False, err_message = token_data["error_description"]))
|
||||
return
|
||||
|
||||
self._auth_response = AuthenticationResponse(success = True,
|
||||
callback(AuthenticationResponse(success = True,
|
||||
token_type = token_data["token_type"],
|
||||
access_token = token_data["access_token"],
|
||||
refresh_token = token_data["refresh_token"],
|
||||
expires_in = token_data["expires_in"],
|
||||
scope = token_data["scope"],
|
||||
received_at = datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT))
|
||||
self._request_lock.release()
|
||||
received_at = datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT)))
|
||||
return
|
||||
|
||||
def checkToken(self, access_token: str, success_callback: Optional[Callable[[UserProfile], None]] = None, failed_callback: Optional[Callable[[], None]] = None) -> None:
|
||||
|
@ -129,7 +109,6 @@ class AuthorizationHelpers:
|
|||
there will not be a callback.
|
||||
:param failed_callback: When the request failed or the response didn't parse, this function will be called.
|
||||
"""
|
||||
self._user_profile = None
|
||||
check_token_url = "{}/check-token".format(self._settings.OAUTH_SERVER_URL)
|
||||
Logger.log("d", "Checking the access token for [%s]", check_token_url)
|
||||
headers = {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from threading import Lock # To turn an asynchronous call synchronous.
|
||||
from typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
|
@ -70,13 +71,23 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
|||
if state != self.state:
|
||||
token_response = AuthenticationResponse(
|
||||
success = False,
|
||||
err_message=catalog.i18nc("@message",
|
||||
"The provided state is not correct.")
|
||||
err_message = catalog.i18nc("@message", "The provided state is not correct.")
|
||||
)
|
||||
elif code and self.authorization_helpers is not None and self.verification_code is not None:
|
||||
token_response = AuthenticationResponse(
|
||||
success = False,
|
||||
err_message = catalog.i18nc("@message", "Timeout when authenticating with the account server.")
|
||||
)
|
||||
# If the code was returned we get the access token.
|
||||
token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode(
|
||||
code, self.verification_code)
|
||||
lock = Lock()
|
||||
lock.acquire()
|
||||
|
||||
def callback(response: AuthenticationResponse) -> None:
|
||||
nonlocal token_response
|
||||
token_response = response
|
||||
lock.release()
|
||||
self.authorization_helpers.getAccessTokenUsingAuthorizationCode(code, self.verification_code, callback)
|
||||
lock.acquire(timeout = 60) # Block thread until request is completed (which releases the lock). If not acquired, the timeout message stays.
|
||||
|
||||
elif self._queryGet(query, "error_code") == "user_denied":
|
||||
# Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog).
|
||||
|
|
|
@ -249,7 +249,7 @@ class AuthorizationService:
|
|||
self._auth_data = AuthenticationResponse(**preferences_data)
|
||||
|
||||
# Also check if we can actually get the user profile information.
|
||||
def callback(profile: Optional[UserProfile]):
|
||||
def callback(profile: Optional["UserProfile"]):
|
||||
if profile is not None:
|
||||
self.onAuthStateChanged.emit(logged_in = True)
|
||||
Logger.debug("Auth data was successfully loaded")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue