diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index a122290c38..06cc0a6061 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -16,7 +16,7 @@ from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settin class AuthorizationHelpers: """Class containing several helpers to deal with the authorization flow.""" - def __init__(self, settings: "OAuth2Settings"): + def __init__(self, settings: "OAuth2Settings") -> None: self._settings = settings self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL) @@ -25,8 +25,7 @@ class AuthorizationHelpers: """Get the OAuth2 settings object.""" return self._settings - def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str)->\ - Optional["AuthenticationResponse"]: + def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str)-> "AuthenticationResponse": """ Request the access token from the authorization server. :param authorization_code: The authorization code from the 1st step. @@ -42,7 +41,7 @@ class AuthorizationHelpers: "scope": self._settings.CLIENT_SCOPES })) - def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> Optional["AuthenticationResponse"]: + def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> AuthenticationResponse: """ Request the access token from the authorization server using a refresh token. :param refresh_token: @@ -57,7 +56,7 @@ class AuthorizationHelpers: })) @staticmethod - def parseTokenResponse(token_response: "requests.request") -> Optional["AuthenticationResponse"]: + def parseTokenResponse(token_response: requests.models.Response) -> AuthenticationResponse: """ Parse the token response from the authorization server into an AuthenticationResponse object. :param token_response: The JSON string data response from the authorization server. diff --git a/cura/OAuth2/AuthorizationRequestHandler.py b/cura/OAuth2/AuthorizationRequestHandler.py index 923787d33f..d13639c45d 100644 --- a/cura/OAuth2/AuthorizationRequestHandler.py +++ b/cura/OAuth2/AuthorizationRequestHandler.py @@ -1,6 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, Callable +from typing import Optional, Callable, Tuple, Dict, Any, List from http.server import BaseHTTPRequestHandler from urllib.parse import parse_qs, urlparse @@ -49,16 +49,17 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): # This will cause the server to shut down, so we do it at the very end of the request handling. self.authorization_callback(token_response) - def _handleCallback(self, query: dict) -> ("ResponseData", Optional["AuthenticationResponse"]): + def _handleCallback(self, query: Dict[Any, List]) -> Tuple["ResponseData", Optional["AuthenticationResponse"]]: """ Handler for the callback URL redirect. :param query: Dict containing the HTTP query parameters. :return: HTTP ResponseData containing a success page to show to the user. """ - if self._queryGet(query, "code"): + code = self._queryGet(query, "code") + if code: # If the code was returned we get the access token. token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode( - self._queryGet(query, "code"), self.verification_code) + code, self.verification_code) elif self._queryGet(query, "error_code") == "user_denied": # Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog). @@ -99,6 +100,6 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): self.wfile.write(data) @staticmethod - def _queryGet(query_data: dict, key: str, default=None) -> Optional[str]: + def _queryGet(query_data: Dict[Any, List], key: str, default=None) -> Optional[str]: """Helper for getting values from a pre-parsed query string""" return query_data.get(key, [default])[0] diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index eb68d5c0a4..4c66170c32 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -27,7 +27,7 @@ class AuthorizationService: # Emit signal when authentication failed. onAuthenticationError = Signal() - def __init__(self, preferences, settings: "OAuth2Settings"): + def __init__(self, preferences, settings: "OAuth2Settings") -> None: self._settings = settings self._auth_helpers = AuthorizationHelpers(settings) self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL) @@ -55,7 +55,7 @@ class AuthorizationService: Tries to parse the JWT if all the needed data exists. :return: UserProfile if found, otherwise None. """ - if not self._auth_data: + if not self._auth_data or self._auth_data.access_token is None: # If no auth data exists, we should always log in again. return None user_data = self._auth_helpers.parseJWT(self._auth_data.access_token) @@ -63,10 +63,13 @@ class AuthorizationService: # If the profile was found, we return it immediately. return user_data # The JWT was expired or invalid and we should request a new one. + if self._auth_data.refresh_token is None: + return None self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token) - if not self._auth_data: + if not self._auth_data or self._auth_data.access_token is None: # The token could not be refreshed using the refresh token. We should login again. return None + return self._auth_helpers.parseJWT(self._auth_data.access_token) def getAccessToken(self) -> Optional[str]: @@ -78,16 +81,23 @@ class AuthorizationService: # We check if we can get the user profile. # If we can't get it, that means the access token (JWT) was invalid or expired. return None + + if self._auth_data is None: + return None + return self._auth_data.access_token def refreshAccessToken(self) -> None: """ Refresh the access token when it expired. """ + if self._auth_data is None or self._auth_data.refresh_token is None: + Logger.log("w", "Unable to refresh acces token, since there is no refresh token.") + return self._storeAuthData(self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)) self.onAuthStateChanged.emit(logged_in=True) - def deleteAuthData(self): + def deleteAuthData(self) -> None: """Delete authentication data from preferences and locally.""" self._storeAuthData() self.onAuthStateChanged.emit(logged_in=False) diff --git a/cura/OAuth2/LocalAuthorizationServer.py b/cura/OAuth2/LocalAuthorizationServer.py index 9979eaaa08..d6a4bf5216 100644 --- a/cura/OAuth2/LocalAuthorizationServer.py +++ b/cura/OAuth2/LocalAuthorizationServer.py @@ -2,7 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import threading from http.server import HTTPServer -from typing import Optional, Callable +from typing import Optional, Callable, Any # As this module is specific for Cura plugins, we can rely on these imports. from UM.Logger import Logger @@ -16,22 +16,22 @@ from cura.OAuth2.Models import AuthenticationResponse class LocalAuthorizationServer: def __init__(self, auth_helpers: "AuthorizationHelpers", - auth_state_changed_callback: "Callable[[AuthenticationResponse], any]", - daemon: bool): + auth_state_changed_callback: "Callable[[AuthenticationResponse], Any]", + daemon: bool) -> None: """ :param auth_helpers: An instance of the authorization helpers class. :param auth_state_changed_callback: A callback function to be called when the authorization state changes. :param daemon: Whether the server thread should be run in daemon mode. Note: Daemon threads are abruptly stopped at shutdown. Their resources (e.g. open files) may never be released. """ - self._web_server = None # type: Optional[HTTPServer] + self._web_server = None # type: Optional[AuthorizationRequestServer] self._web_server_thread = None # type: Optional[threading.Thread] self._web_server_port = auth_helpers.settings.CALLBACK_PORT self._auth_helpers = auth_helpers self._auth_state_changed_callback = auth_state_changed_callback self._daemon = daemon - def start(self, verification_code: "str") -> None: + def start(self, verification_code: str) -> None: """ Starts the local web server to handle the authorization callback. :param verification_code: The verification code part of the OAuth2 client identification. @@ -42,6 +42,9 @@ class LocalAuthorizationServer: self._web_server.setVerificationCode(verification_code) return + if self._web_server_port is None: + raise Exception("Unable to start server without specifying the port.") + Logger.log("d", "Starting local web server to handle authorization callback on port %s", self._web_server_port) diff --git a/cura/OAuth2/Models.py b/cura/OAuth2/Models.py index 08bed7e6d9..a6b91cae26 100644 --- a/cura/OAuth2/Models.py +++ b/cura/OAuth2/Models.py @@ -9,7 +9,7 @@ class BaseModel: # OAuth OAuth2Settings data template. class OAuth2Settings(BaseModel): - CALLBACK_PORT = None # type: Optional[str] + CALLBACK_PORT = None # type: Optional[int] OAUTH_SERVER_URL = None # type: Optional[str] CLIENT_ID = None # type: Optional[str] CLIENT_SCOPES = None # type: Optional[str]