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 hashlib import sha512
|
||||||
from PyQt5.QtNetwork import QNetworkReply
|
from PyQt5.QtNetwork import QNetworkReply
|
||||||
import secrets
|
import secrets
|
||||||
from threading import Lock
|
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
|
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To download log-in tokens.
|
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To download log-in tokens.
|
||||||
|
|
||||||
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
@ -25,8 +24,6 @@ class AuthorizationHelpers:
|
||||||
def __init__(self, settings: "OAuth2Settings") -> None:
|
def __init__(self, settings: "OAuth2Settings") -> None:
|
||||||
self._settings = settings
|
self._settings = settings
|
||||||
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
|
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
|
||||||
self._request_lock = Lock()
|
|
||||||
self._auth_response = None # type: Optional[AuthenticationResponse]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def settings(self) -> "OAuth2Settings":
|
def settings(self) -> "OAuth2Settings":
|
||||||
|
@ -34,14 +31,13 @@ class AuthorizationHelpers:
|
||||||
|
|
||||||
return self._settings
|
return self._settings
|
||||||
|
|
||||||
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse":
|
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str, callback: Callable[[AuthenticationResponse], None]) -> None:
|
||||||
"""Request the access token from the authorization server.
|
"""
|
||||||
|
Request the access token from the authorization server.
|
||||||
:param authorization_code: The authorization code from the 1st step.
|
:param authorization_code: The authorization code from the 1st step.
|
||||||
:param verification_code: The verification code needed for the PKCE extension.
|
: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 = {
|
data = {
|
||||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
"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 "",
|
"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 "",
|
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||||
}
|
}
|
||||||
headers = {"Content-type": "application/x-www-form-urlencoded"}
|
headers = {"Content-type": "application/x-www-form-urlencoded"}
|
||||||
self._request_lock.acquire()
|
|
||||||
HttpRequestManager.getInstance().post(
|
HttpRequestManager.getInstance().post(
|
||||||
self._token_url,
|
self._token_url,
|
||||||
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
||||||
headers_dict = headers,
|
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":
|
def getAccessTokenUsingRefreshToken(self, refresh_token: str, callback: Callable[[AuthenticationResponse], None]) -> None:
|
||||||
"""Request the access token from the authorization server using a refresh token.
|
"""
|
||||||
|
Request the access token from the authorization server using a refresh token.
|
||||||
:param refresh_token:
|
:param refresh_token: A long-lived token used to refresh the authentication token.
|
||||||
:return: An AuthenticationResponse object.
|
: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)
|
Logger.log("d", "Refreshing the access token for [%s]", self._settings.OAUTH_SERVER_URL)
|
||||||
data = {
|
data = {
|
||||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
"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 "",
|
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||||
}
|
}
|
||||||
headers = {"Content-type": "application/x-www-form-urlencoded"}
|
headers = {"Content-type": "application/x-www-form-urlencoded"}
|
||||||
self._request_lock.acquire()
|
|
||||||
HttpRequestManager.getInstance().post(
|
HttpRequestManager.getInstance().post(
|
||||||
self._token_url,
|
self._token_url,
|
||||||
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
||||||
headers_dict = headers,
|
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.
|
"""Parse the token response from the authorization server into an AuthenticationResponse object.
|
||||||
|
|
||||||
:param token_response: The JSON string data response from the authorization server.
|
:param token_response: The JSON string data response from the authorization server.
|
||||||
|
@ -101,23 +84,20 @@ class AuthorizationHelpers:
|
||||||
"""
|
"""
|
||||||
token_data = HttpRequestManager.readJSON(token_response)
|
token_data = HttpRequestManager.readJSON(token_response)
|
||||||
if not token_data:
|
if not token_data:
|
||||||
self._auth_response = AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response."))
|
callback(AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response.")))
|
||||||
self._request_lock.release()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if token_response.error() != QNetworkReply.NetworkError.NoError:
|
if token_response.error() != QNetworkReply.NetworkError.NoError:
|
||||||
self._auth_response = AuthenticationResponse(success = False, err_message = token_data["error_description"])
|
callback(AuthenticationResponse(success = False, err_message = token_data["error_description"]))
|
||||||
self._request_lock.release()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self._auth_response = AuthenticationResponse(success = True,
|
callback(AuthenticationResponse(success = True,
|
||||||
token_type = token_data["token_type"],
|
token_type = token_data["token_type"],
|
||||||
access_token = token_data["access_token"],
|
access_token = token_data["access_token"],
|
||||||
refresh_token = token_data["refresh_token"],
|
refresh_token = token_data["refresh_token"],
|
||||||
expires_in = token_data["expires_in"],
|
expires_in = token_data["expires_in"],
|
||||||
scope = token_data["scope"],
|
scope = token_data["scope"],
|
||||||
received_at = datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT))
|
received_at = datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT)))
|
||||||
self._request_lock.release()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def checkToken(self, access_token: str, success_callback: Optional[Callable[[UserProfile], None]] = None, failed_callback: Optional[Callable[[], None]] = None) -> None:
|
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.
|
there will not be a callback.
|
||||||
:param failed_callback: When the request failed or the response didn't parse, this function will be called.
|
: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)
|
check_token_url = "{}/check-token".format(self._settings.OAUTH_SERVER_URL)
|
||||||
Logger.log("d", "Checking the access token for [%s]", check_token_url)
|
Logger.log("d", "Checking the access token for [%s]", check_token_url)
|
||||||
headers = {
|
headers = {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from http.server import BaseHTTPRequestHandler
|
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 typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
|
@ -70,13 +71,23 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
||||||
if state != self.state:
|
if state != self.state:
|
||||||
token_response = AuthenticationResponse(
|
token_response = AuthenticationResponse(
|
||||||
success = False,
|
success = False,
|
||||||
err_message=catalog.i18nc("@message",
|
err_message = catalog.i18nc("@message", "The provided state is not correct.")
|
||||||
"The provided state is not correct.")
|
|
||||||
)
|
)
|
||||||
elif code and self.authorization_helpers is not None and self.verification_code is not None:
|
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.
|
# If the code was returned we get the access token.
|
||||||
token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode(
|
lock = Lock()
|
||||||
code, self.verification_code)
|
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":
|
elif self._queryGet(query, "error_code") == "user_denied":
|
||||||
# Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog).
|
# 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)
|
self._auth_data = AuthenticationResponse(**preferences_data)
|
||||||
|
|
||||||
# Also check if we can actually get the user profile information.
|
# 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:
|
if profile is not None:
|
||||||
self.onAuthStateChanged.emit(logged_in = True)
|
self.onAuthStateChanged.emit(logged_in = True)
|
||||||
Logger.debug("Auth data was successfully loaded")
|
Logger.debug("Auth data was successfully loaded")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue