From 4a4f034b33175cf178300c58f09f11330067c821 Mon Sep 17 00:00:00 2001 From: alemidev Date: Mon, 14 Feb 2022 01:57:07 +0100 Subject: [PATCH] made msauth state serializable for saving and restoring --- aiocraft/client.py | 1 - aiocraft/mc/auth/microsoft.py | 108 +++++++++++++++------------------- 2 files changed, 48 insertions(+), 61 deletions(-) diff --git a/aiocraft/client.py b/aiocraft/client.py index 046a513..dd8d94e 100644 --- a/aiocraft/client.py +++ b/aiocraft/client.py @@ -149,7 +149,6 @@ class MinecraftClient(CallbacksHolder, Runnable): await super().start() if self.started: return - self._processing = True self._worker = asyncio.get_event_loop().create_task(self._client_worker()) self._logger.info("Minecraft client started") diff --git a/aiocraft/mc/auth/microsoft.py b/aiocraft/mc/auth/microsoft.py index 4417455..fe29394 100644 --- a/aiocraft/mc/auth/microsoft.py +++ b/aiocraft/mc/auth/microsoft.py @@ -3,7 +3,7 @@ import uuid import logging from urllib.parse import urlencode -from typing import Dict, Optional, Any +from typing import Dict, List, Optional, Any from yarl import URL import aiohttp @@ -19,15 +19,9 @@ class MicrosoftAuthenticator(AuthInterface): client_secret : str redirect_uri : str - ms_token : Optional[str] - ms_refresh_token : Optional[str] - xbl_token : Optional[str] - xsts_token : Optional[str] - userhash : Optional[str] - - mcstore : dict accessToken : str selectedProfile : GameProfile + refreshToken : Optional[str] MINECRAFT_CLIENT_ID = "00000000402b5328" OAUTH_LOGIN = "https://login.live.com/oauth20" @@ -43,18 +37,25 @@ class MicrosoftAuthenticator(AuthInterface): self.client_id = client_id self.client_secret = client_secret self.redirect_uri = redirect_uri - self.ms_token = None - self.ms_refresh_token = None - self.xbl_token = None - self.xsts_token = None - self.userhash = None - self.mcstore = {} + self.refreshToken = None self.accessToken = '' self.selectedProfile = GameProfile(id='', name='') + def serialize(self) -> Dict[str, Any]: + return { + 'accessToken': self.accessToken, + 'refreshToken': self.refreshToken, + 'selectedProfile': self.selectedProfile.as_dict(), + } + + def deserialize(self, data:Dict[str, Any]): + self.accessToken = data['accessToken'] + self.refreshToken = data['refreshToken'] + self.selectedProfile = GameProfile(**data['selectedProfile']) + @property def refreshable(self) -> bool: - return self.ms_refresh_token is not None + return self.refreshToken is not None def url(self, state:str=""): """Builds MS OAuth url for the user to login""" @@ -68,27 +69,24 @@ class MicrosoftAuthenticator(AuthInterface): ) async def login(self, code:str): # TODO nicer way to get code? - self._full_auth(code) - await self.fetch_profile() + self.accessToken = await self.authenticate(code) + prof = await self.fetch_profile() + self.selectedProfile = GameProfile(id=prof['id'], name=prof['name']) logging.info("Successfully logged into Microsoft account") async def refresh(self): - if not self.ms_refresh_token: + if not self.refreshToken: raise AuthException("Missing refresh token") - await self._full_auth() - await self.fetch_profile() + self.accessToken = await self.authenticate() + prof = await self.fetch_profile() + self.selectedProfile = GameProfile(id=prof['id'], name=prof['name']) logging.info("Successfully refreshed Microsoft token") async def validate(self): - await self.fetch_profile() + prof = await self.fetch_profile() + self.selectedProfile = GameProfile(id=prof['id'], name=prof['name']) - async def _full_auth(self, code:str=''): - await self._ms_auth(code) - await self._xbl_auth() - await self._xsts_auth() - await self._mc_auth() - - async def _ms_auth(self, code:str=""): + async def authenticate(self, code:str="") -> str: """Authorize Microsoft account""" logging.debug("Authenticating Microsoft account") payload = { @@ -99,8 +97,8 @@ class MicrosoftAuthenticator(AuthInterface): } if code: payload['code'] = code - elif self.ms_refresh_token: - payload['refresh_token'] = self.ms_refresh_token + elif self.refreshToken: + payload['refreshToken'] = self.refreshToken else: raise InvalidStateError("Missing auth code and refresh token") auth_response = await self._post( @@ -108,14 +106,14 @@ class MicrosoftAuthenticator(AuthInterface): headers={ "Content-Type": "application/x-www-form-urlencoded" }, data=urlencode(payload) ) - self.ms_token = auth_response['access_token'] - self.ms_refresh_token = auth_response['refresh_token'] + self.refreshToken = auth_response['refreshToken'] # maybe store expire_in and other stuff too? TODO - async def _xbl_auth(self): + ms_token = auth_response['access_token'] + return await self._xbl_auth(ms_token) + + async def _xbl_auth(self, ms_token:str) -> str: """Authorize with XBox Live""" - if not self.ms_token: - raise InvalidStateError("Missing MS access token") logging.debug("Authenticating against XBox Live") auth_response = await self._post( self.XBL_LOGIN, @@ -127,19 +125,17 @@ class MicrosoftAuthenticator(AuthInterface): "Properties": { "AuthMethod": "RPS", "SiteName": "user.auth.xboxlive.com", - "RpsTicket": f"d={self.ms_token}" + "RpsTicket": f"d={ms_token}" }, "RelyingParty": "http://auth.xboxlive.com", "TokenType": "JWT" } ) - self.userhash = auth_response["DisplayClaims"]["xui"][0]["uhs"] - self.xbl_token = auth_response["Token"] + xbl_token = auth_response["Token"] + return await self._xsts_auth(xbl_token) - async def _xsts_auth(self): + async def _xsts_auth(self, xbl_token:str) -> str: """Authenticate with XBox Security Tokens""" - if not self.xbl_token: - raise InvalidStateError("Missing XBL Token") logging.debug("Authenticating against XSTS") auth_response = await self._post( self.XSTS_LOGIN, @@ -150,24 +146,18 @@ class MicrosoftAuthenticator(AuthInterface): json={ "Properties": { "SandboxId": "RETAIL", - "UserTokens": [ - self.xbl_token - ] + "UserTokens": [ xbl_token ] }, "RelyingParty": "rp://api.minecraftservices.com/", "TokenType": "JWT" } ) - self.xsts_token = auth_response["Token"] - if self.userhash != auth_response["DisplayClaims"]["xui"][0]["uhs"]: - raise InvalidStateError("userhash differs from XBL and XSTS") + xsts_token = auth_response["Token"] + userhash = auth_response["DisplayClaims"]["xui"][0]["uhs"] + return await self._mc_auth(userhash, xsts_token) - async def _mc_auth(self): + async def _mc_auth(self, userhash:str, xsts_token:str) -> str: """Authenticate with Minecraft""" - if not self.userhash: - raise InvalidStateError("Missing userhash") - if not self.xsts_token: - raise InvalidStateError("Missing XSTS Token") logging.debug("Authenticating against Minecraft") auth_response = await self._post( self.MINECRAFT_API + "/authentication/login_with_xbox", @@ -176,25 +166,23 @@ class MicrosoftAuthenticator(AuthInterface): "Accept": "application/json" }, json={ - "identityToken": f"XBL3.0 x={self.userhash};{self.xsts_token}" + "identityToken": f"XBL3.0 x={userhash};{xsts_token}" }, ) - self.accessToken = auth_response['access_token'] - logging.info("Received access token : %s", self.accessToken) + return auth_response['access_token'] - async def fetch_mcstore(self): + async def fetch_mcstore(self) -> Dict[str, Any]: """Get the store information""" logging.debug("Fetching MC Store") - self.mcstore = await self._get( + return await self._get( self.MINECRAFT_API + "/entitlements/mcstore", headers={ "Authorization": f"Bearer {self.accessToken}" }, ) - async def fetch_profile(self): + async def fetch_profile(self) -> Dict[str, Any]: """Get player profile""" logging.debug("Fetching profile") - p = await self._get( + return await self._get( self.MINECRAFT_API + "/minecraft/profile", headers={ "Authorization": f"Bearer {self.accessToken}" }, ) - self.selectedProfile = GameProfile(id=p['id'], name=p['name'])