made msauth state serializable for saving and restoring
This commit is contained in:
parent
3cb6760f10
commit
4a4f034b33
2 changed files with 48 additions and 61 deletions
|
@ -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")
|
||||||
|
|
|
@ -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'])
|
|
||||||
|
|
Loading…
Reference in a new issue