ForcePilot/backend/package/yuxi/channel/extensions/minecraft/auth.py

93 lines
3.7 KiB
Python
Raw Normal View History

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()