Skip to content

Commit 6f7967c

Browse files
committed
feat: Implement XAL/Sisu auth flow
1 parent adf9558 commit 6f7967c

11 files changed

Lines changed: 573 additions & 23 deletions

File tree

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
entry_points={
6666
"console_scripts": [
6767
"xbox-authenticate=xbox.webapi.scripts.authenticate:main",
68+
"xbox-xal=xbox.webapi.scripts.xal:main",
6869
"xbox-searchlive=xbox.webapi.scripts.search:main",
6970
"xbox-change-gt=xbox.webapi.scripts.change_gamertag:main",
7071
"xbox-friends=xbox.webapi.scripts.friends:main",

tests/conftest.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime
2+
import uuid
23

34
from ecdsa.keys import SigningKey, VerifyingKey
45
import pytest
@@ -11,7 +12,11 @@
1112
XAUResponse,
1213
XSTSResponse,
1314
)
14-
from xbox.webapi.authentication.xal import XALManager
15+
from xbox.webapi.authentication.xal import (
16+
APP_PARAMS_GAMEPASS_BETA,
17+
CLIENT_PARAMS_ANDROID,
18+
XALManager,
19+
)
1520
from xbox.webapi.common.request_signer import RequestSigner
1621
from xbox.webapi.common.signed_session import SignedSession
1722

@@ -34,7 +39,12 @@ async def auth_mgr(event_loop):
3439
@pytest_asyncio.fixture(scope="function")
3540
async def xal_mgr(event_loop):
3641
session = SignedSession()
37-
mgr = XALManager(session)
42+
mgr = XALManager(
43+
session,
44+
device_id=uuid.UUID("9c493431-5462-4a4a-a247-f6420396318d"),
45+
app_params=APP_PARAMS_GAMEPASS_BETA,
46+
client_params=CLIENT_PARAMS_ANDROID,
47+
)
3848
yield mgr
3949
await session.aclose()
4050

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"MsaOauthRedirect":"https://login.live.com/oauth20_authorize.srf?lw=1&fl=dob,easi2&xsup=1&display=android_phone&code_challenge=code_challenge_string&code_challenge_method=S256&state=state_string&client_id=000000004C20A908&response_type=code&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL&redirect_uri=ms-xal-public-beta-000000004c20a908%3A%2F%2Fauth&nopa=2",
3+
"MsaRequestParameters":{}
4+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"DeviceToken": "eyDeviceToken",
3+
"TitleToken": {
4+
"DisplayClaims": {
5+
"xti": {
6+
"tid": "1016898439"
7+
}
8+
},
9+
"IssueInstant": "2022-11-11T21:11:43.9456623Z",
10+
"NotAfter": "2099-11-25T21:11:43.9456623Z",
11+
"Token": "eyTitletoken"
12+
},
13+
"UserToken": {
14+
"DisplayClaims": {
15+
"xui": [
16+
{
17+
"uhs": "2034583485034500345"
18+
}
19+
]
20+
},
21+
"IssueInstant": "2022-11-11T21:11:43.9422756Z",
22+
"NotAfter": "2099-11-25T21:11:43.9422756Z",
23+
"Token": "eyUserToken"
24+
},
25+
"AuthorizationToken": {
26+
"DisplayClaims": {
27+
"xui": [
28+
{
29+
"gtg": "Pony",
30+
"xid": "24812480912094",
31+
"uhs": "2034583485034500345",
32+
"agg": "Adult",
33+
"usr": "195 229 243",
34+
"prv": "184 185 186 187 188 190 191 192 193 194 196 198 199 200 201 203 204 205 206 207 208 211 214 215 216 217 220 224 227 228 235 238 245 247 249 252 254 255"
35+
}
36+
]
37+
},
38+
"IssueInstant": "2022-11-11T21:11:44.1945885Z",
39+
"NotAfter": "2099-11-12T13:11:44.1945885Z",
40+
"Token": "eyXSTSToken"
41+
},
42+
"WebPage": "https://sisu.xboxlive.com/client/v27/000000004c20a908/view/index.html",
43+
"Sandbox": "RETAIL"
44+
}

tests/test_auth.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,6 @@ async def test_refresh_tokens_user_still_valid(respx_mock, auth_mgr):
8989
assert route2.called
9090

9191

92-
@pytest.mark.asyncio
93-
async def test_get_title_endpoints(respx_mock, auth_mgr):
94-
route = respx_mock.get("https://title.mgt.xboxlive.com").mock(
95-
return_value=Response(200, json=get_response_json("auth_title_endpoints"))
96-
)
97-
await auth_mgr.get_title_endpoints()
98-
assert route.called
99-
100-
10192
@pytest.mark.asyncio
10293
async def test_xsts_properties(auth_mgr):
10394
assert auth_mgr.xsts_token.xuid == "2669321029139235"

tests/test_xal.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,63 @@
1-
from datetime import datetime, timedelta, timezone
21
import uuid
32

4-
from httpx import Response
3+
from httpx import AsyncClient, Response
54
import pytest
65

76
from tests.common import get_response_json
87

98

9+
@pytest.mark.asyncio
10+
async def test_get_title_endpoints(respx_mock, xal_mgr):
11+
route = respx_mock.get("https://title.mgt.xboxlive.com").mock(
12+
return_value=Response(200, json=get_response_json("auth_title_endpoints"))
13+
)
14+
async with AsyncClient() as client:
15+
await xal_mgr.get_title_endpoints(client)
16+
assert route.called
17+
18+
1019
@pytest.mark.asyncio
1120
async def test_get_device_token(respx_mock, xal_mgr):
1221
route = respx_mock.post(
1322
"https://device.auth.xboxlive.com/device/authenticate"
1423
).mock(return_value=Response(200, json=get_response_json("auth_device_token")))
15-
resp = await xal_mgr.request_device_token(
16-
uuid.UUID("9c493431-5462-4a4a-a247-f6420396318d")
24+
await xal_mgr.request_device_token()
25+
assert route.called
26+
27+
28+
@pytest.mark.asyncio
29+
async def test_sisu_authentication(respx_mock, xal_mgr):
30+
route = respx_mock.post("https://sisu.xboxlive.com/authenticate").mock(
31+
return_value=Response(
32+
200,
33+
json=get_response_json("xal_authentication_resp"),
34+
headers={"X-SessionId": "abcsession-id"},
35+
)
36+
)
37+
resp, session_id = await xal_mgr.request_sisu_authentication(
38+
"eyDeviceToken", "code_challenge_string", "state_string"
39+
)
40+
assert route.called
41+
assert session_id == "abcsession-id"
42+
assert resp.msa_oauth_redirect is not None
43+
44+
45+
@pytest.mark.asyncio
46+
async def test_sisu_authorization(respx_mock, xal_mgr):
47+
route = respx_mock.post("https://sisu.xboxlive.com/authorize").mock(
48+
return_value=Response(200, json=get_response_json("xal_authorization_resp"))
49+
)
50+
await xal_mgr.do_sisu_authorization(
51+
"SISU-Session-ID", "eyAccessToken", "eyDeviceToken"
1752
)
1853
assert route.called
54+
55+
56+
@pytest.mark.asyncio
57+
async def test_exchange_code_for_token(respx_mock, xal_mgr):
58+
route = respx_mock.post("https://login.live.com").mock(
59+
return_value=Response(200, json=get_response_json("auth_oauth2_token"))
60+
)
61+
await xal_mgr.exchange_code_for_token("abc", "xyz")
62+
63+
assert route.called

xbox/webapi/authentication/models.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Dict, List, Optional
44

55
from pydantic import BaseModel, Field
6+
from pydantic.dataclasses import dataclass
67

78
from xbox.webapi.common.models import PascalCaseModel
89

@@ -94,6 +95,39 @@ def is_valid(self) -> bool:
9495
return (self.issued + timedelta(seconds=self.expires_in)) > utc_now()
9596

9697

98+
"""XAL related models"""
99+
100+
101+
@dataclass
102+
class XalAppParameters:
103+
app_id: str
104+
title_id: str
105+
redirect_uri: str
106+
107+
108+
@dataclass
109+
class XalClientParameters:
110+
user_agent: str
111+
device_type: str
112+
client_version: str
113+
query_display: str
114+
115+
116+
class SisuAuthenticationResponse(PascalCaseModel):
117+
msa_oauth_redirect: str
118+
msa_request_parameters: Dict[str, str]
119+
120+
121+
class SisuAuthorizationResponse(PascalCaseModel):
122+
device_token: str
123+
title_token: XATResponse
124+
user_token: XAUResponse
125+
authorization_token: XSTSResponse
126+
web_page: str
127+
sandbox: str
128+
use_modern_gamertag: Optional[bool]
129+
130+
97131
"""Signature related models"""
98132

99133

0 commit comments

Comments
 (0)