import asyncio import logging import httpx logger = logging.getLogger(__name__) MS_DEVICE_CODE_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode" MS_TOKEN_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token" XBOX_AUTH_URL = "https://user.auth.xboxlive.com/user/authenticate" XSTS_AUTH_URL = "https://xsts.auth.xboxlive.com/xsts/authorize" MC_AUTH_URL = "https://api.minecraftservices.com/authentication/login_with_xbox" MC_PROFILE_URL = "https://api.minecraftservices.com/minecraft/profile" CLIENT_ID = "00000000402b5328" class MinecraftAuth: def __init__(self, email: str | None = None, password: str | None = None): self.email = email self.password = password self._http = httpx.AsyncClient(timeout=30) async def close(self): await self._http.aclose() async def authenticate(self) -> dict: ms_token = await self._get_ms_token() xbl_token, uhs = await self._auth_xbox_live(ms_token) xsts_token, _ = await self._auth_xsts(xbl_token) mc_token = await self._auth_minecraft(uhs, xsts_token) profile = await self._get_minecraft_profile(mc_token) return {"access_token": mc_token, "uuid": profile["id"], "username": profile["name"]} async def _get_ms_token(self) -> str: device_resp = await self._http.post( MS_DEVICE_CODE_URL, data={"client_id": CLIENT_ID, "scope": "XboxLive.signin offline_access"} ) device_data = device_resp.json() user_code = device_data["user_code"] device_code = device_data["device_code"] interval = device_data.get("interval", 5) logger.info("Microsoft login: visit https://microsoft.com/devicelogin and enter code: %s", user_code) while True: await asyncio.sleep(interval) token_resp = await self._http.post( MS_TOKEN_URL, data={ "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "client_id": CLIENT_ID, "device_code": device_code, }, ) token_data = token_resp.json() if "access_token" in token_data: return token_data["access_token"] if token_data.get("error") == "authorization_pending": continue raise RuntimeError(f"Microsoft OAuth failed: {token_data.get('error')}") async def _auth_xbox_live(self, ms_token: str) -> tuple[str, str]: resp = await self._http.post( XBOX_AUTH_URL, json={ "Properties": {"AuthMethod": "RPS", "SiteName": "user.auth.xboxlive.com", "RpsTicket": f"d={ms_token}"}, "RelyingParty": "http://auth.xboxlive.com", "TokenType": "JWT", }, ) data = resp.json() return data["Token"], data["DisplayClaims"]["xui"][0]["uhs"] async def _auth_xsts(self, xbl_token: str) -> tuple[str, str]: resp = await self._http.post( XSTS_AUTH_URL, json={ "Properties": {"SandboxId": "RETAIL", "UserTokens": [xbl_token]}, "RelyingParty": "rp://api.minecraftservices.com/", "TokenType": "JWT", }, ) data = resp.json() return data["Token"], data["DisplayClaims"]["xui"][0]["uhs"] async def _auth_minecraft(self, uhs: str, xsts_token: str) -> str: resp = await self._http.post(MC_AUTH_URL, json={"identityToken": f"XBL3.0 x={uhs};{xsts_token}"}) return resp.json()["access_token"] async def _get_minecraft_profile(self, mc_token: str) -> dict: resp = await self._http.get(MC_PROFILE_URL, headers={"Authorization": f"Bearer {mc_token}"}) return resp.json()