made msauth state serializable for saving and restoring

This commit is contained in:
əlemi 2022-02-14 01:57:07 +01:00
parent 3cb6760f10
commit 4a4f034b33
No known key found for this signature in database
GPG key ID: BBCBFE5D7244634E
2 changed files with 48 additions and 61 deletions

View file

@ -149,7 +149,6 @@ class MinecraftClient(CallbacksHolder, Runnable):
await super().start() await super().start()
if self.started: if self.started:
return return
self._processing = True self._processing = True
self._worker = asyncio.get_event_loop().create_task(self._client_worker()) self._worker = asyncio.get_event_loop().create_task(self._client_worker())
self._logger.info("Minecraft client started") self._logger.info("Minecraft client started")

View file

@ -3,7 +3,7 @@ import uuid
import logging import logging
from urllib.parse import urlencode from urllib.parse import urlencode
from typing import Dict, Optional, Any from typing import Dict, List, Optional, Any
from yarl import URL from yarl import URL
import aiohttp import aiohttp
@ -19,15 +19,9 @@ class MicrosoftAuthenticator(AuthInterface):
client_secret : str client_secret : str
redirect_uri : 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 accessToken : str
selectedProfile : GameProfile selectedProfile : GameProfile
refreshToken : Optional[str]
MINECRAFT_CLIENT_ID = "00000000402b5328" MINECRAFT_CLIENT_ID = "00000000402b5328"
OAUTH_LOGIN = "https://login.live.com/oauth20" OAUTH_LOGIN = "https://login.live.com/oauth20"
@ -43,18 +37,25 @@ class MicrosoftAuthenticator(AuthInterface):
self.client_id = client_id self.client_id = client_id
self.client_secret = client_secret self.client_secret = client_secret
self.redirect_uri = redirect_uri self.redirect_uri = redirect_uri
self.ms_token = None self.refreshToken = None
self.ms_refresh_token = None
self.xbl_token = None
self.xsts_token = None
self.userhash = None
self.mcstore = {}
self.accessToken = '' self.accessToken = ''
self.selectedProfile = GameProfile(id='', name='') 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 @property
def refreshable(self) -> bool: def refreshable(self) -> bool:
return self.ms_refresh_token is not None return self.refreshToken is not None
def url(self, state:str=""): def url(self, state:str=""):
"""Builds MS OAuth url for the user to login""" """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? async def login(self, code:str): # TODO nicer way to get code?
self._full_auth(code) self.accessToken = await self.authenticate(code)
await self.fetch_profile() prof = await self.fetch_profile()
self.selectedProfile = GameProfile(id=prof['id'], name=prof['name'])
logging.info("Successfully logged into Microsoft account") logging.info("Successfully logged into Microsoft account")
async def refresh(self): async def refresh(self):
if not self.ms_refresh_token: if not self.refreshToken:
raise AuthException("Missing refresh token") raise AuthException("Missing refresh token")
await self._full_auth() self.accessToken = await self.authenticate()
await self.fetch_profile() prof = await self.fetch_profile()
self.selectedProfile = GameProfile(id=prof['id'], name=prof['name'])
logging.info("Successfully refreshed Microsoft token") logging.info("Successfully refreshed Microsoft token")
async def validate(self): 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=''): async def authenticate(self, code:str="") -> 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=""):
"""Authorize Microsoft account""" """Authorize Microsoft account"""
logging.debug("Authenticating Microsoft account") logging.debug("Authenticating Microsoft account")
payload = { payload = {
@ -99,8 +97,8 @@ class MicrosoftAuthenticator(AuthInterface):
} }
if code: if code:
payload['code'] = code payload['code'] = code
elif self.ms_refresh_token: elif self.refreshToken:
payload['refresh_token'] = self.ms_refresh_token payload['refreshToken'] = self.refreshToken
else: else:
raise InvalidStateError("Missing auth code and refresh token") raise InvalidStateError("Missing auth code and refresh token")
auth_response = await self._post( auth_response = await self._post(
@ -108,14 +106,14 @@ class MicrosoftAuthenticator(AuthInterface):
headers={ "Content-Type": "application/x-www-form-urlencoded" }, headers={ "Content-Type": "application/x-www-form-urlencoded" },
data=urlencode(payload) data=urlencode(payload)
) )
self.ms_token = auth_response['access_token'] self.refreshToken = auth_response['refreshToken']
self.ms_refresh_token = auth_response['refresh_token']
# maybe store expire_in and other stuff too? TODO # 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""" """Authorize with XBox Live"""
if not self.ms_token:
raise InvalidStateError("Missing MS access token")
logging.debug("Authenticating against XBox Live") logging.debug("Authenticating against XBox Live")
auth_response = await self._post( auth_response = await self._post(
self.XBL_LOGIN, self.XBL_LOGIN,
@ -127,19 +125,17 @@ class MicrosoftAuthenticator(AuthInterface):
"Properties": { "Properties": {
"AuthMethod": "RPS", "AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com", "SiteName": "user.auth.xboxlive.com",
"RpsTicket": f"d={self.ms_token}" "RpsTicket": f"d={ms_token}"
}, },
"RelyingParty": "http://auth.xboxlive.com", "RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT" "TokenType": "JWT"
} }
) )
self.userhash = auth_response["DisplayClaims"]["xui"][0]["uhs"] xbl_token = auth_response["Token"]
self.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""" """Authenticate with XBox Security Tokens"""
if not self.xbl_token:
raise InvalidStateError("Missing XBL Token")
logging.debug("Authenticating against XSTS") logging.debug("Authenticating against XSTS")
auth_response = await self._post( auth_response = await self._post(
self.XSTS_LOGIN, self.XSTS_LOGIN,
@ -150,24 +146,18 @@ class MicrosoftAuthenticator(AuthInterface):
json={ json={
"Properties": { "Properties": {
"SandboxId": "RETAIL", "SandboxId": "RETAIL",
"UserTokens": [ "UserTokens": [ xbl_token ]
self.xbl_token
]
}, },
"RelyingParty": "rp://api.minecraftservices.com/", "RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT" "TokenType": "JWT"
} }
) )
self.xsts_token = auth_response["Token"] xsts_token = auth_response["Token"]
if self.userhash != auth_response["DisplayClaims"]["xui"][0]["uhs"]: userhash = auth_response["DisplayClaims"]["xui"][0]["uhs"]
raise InvalidStateError("userhash differs from XBL and XSTS") 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""" """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") logging.debug("Authenticating against Minecraft")
auth_response = await self._post( auth_response = await self._post(
self.MINECRAFT_API + "/authentication/login_with_xbox", self.MINECRAFT_API + "/authentication/login_with_xbox",
@ -176,25 +166,23 @@ class MicrosoftAuthenticator(AuthInterface):
"Accept": "application/json" "Accept": "application/json"
}, },
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'] return auth_response['access_token']
logging.info("Received access token : %s", self.accessToken)
async def fetch_mcstore(self): async def fetch_mcstore(self) -> Dict[str, Any]:
"""Get the store information""" """Get the store information"""
logging.debug("Fetching MC Store") logging.debug("Fetching MC Store")
self.mcstore = await self._get( return await self._get(
self.MINECRAFT_API + "/entitlements/mcstore", self.MINECRAFT_API + "/entitlements/mcstore",
headers={ "Authorization": f"Bearer {self.accessToken}" }, headers={ "Authorization": f"Bearer {self.accessToken}" },
) )
async def fetch_profile(self): async def fetch_profile(self) -> Dict[str, Any]:
"""Get player profile""" """Get player profile"""
logging.debug("Fetching profile") logging.debug("Fetching profile")
p = await self._get( return await self._get(
self.MINECRAFT_API + "/minecraft/profile", self.MINECRAFT_API + "/minecraft/profile",
headers={ "Authorization": f"Bearer {self.accessToken}" }, headers={ "Authorization": f"Bearer {self.accessToken}" },
) )
self.selectedProfile = GameProfile(id=p['id'], name=p['name'])