Skip to content

Commit 0727a8c

Browse files
committed
Merge branch 'feat/xal_auth'
2 parents 38d84d3 + 6f7967c commit 0727a8c

14 files changed

Lines changed: 668 additions & 55 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: 19 additions & 0 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,6 +12,11 @@
1112
XAUResponse,
1213
XSTSResponse,
1314
)
15+
from xbox.webapi.authentication.xal import (
16+
APP_PARAMS_GAMEPASS_BETA,
17+
CLIENT_PARAMS_ANDROID,
18+
XALManager,
19+
)
1420
from xbox.webapi.common.request_signer import RequestSigner
1521
from xbox.webapi.common.signed_session import SignedSession
1622

@@ -30,6 +36,19 @@ async def auth_mgr(event_loop):
3036
await session.aclose()
3137

3238

39+
@pytest_asyncio.fixture(scope="function")
40+
async def xal_mgr(event_loop):
41+
session = SignedSession()
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+
)
48+
yield mgr
49+
await session.aclose()
50+
51+
3352
@pytest.fixture(scope="function")
3453
def xbl_client(auth_mgr):
3554
return XboxLiveClient(auth_mgr)
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 & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from datetime import datetime, timedelta, timezone
2-
import uuid
32

43
from httpx import Response
54
import pytest
@@ -90,26 +89,6 @@ async def test_refresh_tokens_user_still_valid(respx_mock, auth_mgr):
9089
assert route2.called
9190

9291

93-
@pytest.mark.asyncio
94-
async def test_get_title_endpoints(respx_mock, auth_mgr):
95-
route = respx_mock.get("https://title.mgt.xboxlive.com").mock(
96-
return_value=Response(200, json=get_response_json("auth_title_endpoints"))
97-
)
98-
await auth_mgr.get_title_endpoints()
99-
assert route.called
100-
101-
102-
@pytest.mark.asyncio
103-
async def test_get_device_token(respx_mock, auth_mgr):
104-
route = respx_mock.post(
105-
"https://device.auth.xboxlive.com/device/authenticate"
106-
).mock(return_value=Response(200, json=get_response_json("auth_device_token")))
107-
resp = await auth_mgr.request_device_token(
108-
uuid.UUID("9c493431-5462-4a4a-a247-f6420396318d")
109-
)
110-
assert route.called
111-
112-
11392
@pytest.mark.asyncio
11493
async def test_xsts_properties(auth_mgr):
11594
assert auth_mgr.xsts_token.xuid == "2669321029139235"

tests/test_signed_session.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,34 @@ async def test_sending_signed_request(synthetic_request_signer, respx_mock):
2929
)
3030

3131
async with signed_session:
32-
resp = await signed_session.send_signed(request)
32+
resp = await signed_session.send_request_signed(request)
33+
34+
assert route.called
35+
assert resp.request.headers.get("Signature") is not None
36+
37+
38+
@pytest.mark.asyncio
39+
async def test_sending_signed(synthetic_request_signer, respx_mock):
40+
route = respx_mock.post("https://xsts.auth.xboxlive.com").mock(
41+
return_value=Response(200, json=get_response_json("auth_xsts_token"))
42+
)
43+
44+
signed_session = SignedSession(synthetic_request_signer)
45+
46+
method = "POST"
47+
url = "https://xsts.auth.xboxlive.com/xsts/authorize"
48+
headers = {"x-xbl-contract-version": "1"}
49+
data = {
50+
"RelyingParty": "http://xboxlive.com",
51+
"TokenType": "JWT",
52+
"Properties": {
53+
"UserTokens": ["eyJWTblabla"],
54+
"SandboxId": "RETAIL",
55+
},
56+
}
57+
58+
async with signed_session:
59+
resp = await signed_session.send_signed(method, url, headers=headers, data=data)
3360

3461
assert route.called
3562
assert resp.request.headers.get("Signature") is not None

tests/test_xal.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import uuid
2+
3+
from httpx import AsyncClient, Response
4+
import pytest
5+
6+
from tests.common import get_response_json
7+
8+
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+
19+
@pytest.mark.asyncio
20+
async def test_get_device_token(respx_mock, xal_mgr):
21+
route = respx_mock.post(
22+
"https://device.auth.xboxlive.com/device/authenticate"
23+
).mock(return_value=Response(200, json=get_response_json("auth_device_token")))
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"
52+
)
53+
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/manager.py

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@
55
"""
66
import logging
77
from typing import List, Optional
8-
import uuid
98

109
import httpx
1110

1211
from xbox.webapi.authentication.models import (
1312
OAuth2TokenResponse,
14-
TitleEndpointsResponse,
15-
XADResponse,
1613
XAUResponse,
1714
XSTSResponse,
1815
)
@@ -83,14 +80,6 @@ async def refresh_tokens(self) -> None:
8380
if not (self.xsts_token and self.xsts_token.is_valid()):
8481
self.xsts_token = await self.request_xsts_token()
8582

86-
async def get_title_endpoints(self) -> TitleEndpointsResponse:
87-
url = "https://title.mgt.xboxlive.com/titles/default/endpoints"
88-
headers = {"x-xbl-contract-version": "1"}
89-
params = {"type": 1}
90-
resp = await self.session.get(url, headers=headers, params=params)
91-
resp.raise_for_status()
92-
return TitleEndpointsResponse(**resp.json())
93-
9483
async def request_oauth_token(self, authorization_code: str) -> OAuth2TokenResponse:
9584
"""Request OAuth2 token."""
9685
return await self._oauth2_token_request(
@@ -170,23 +159,3 @@ async def request_xsts_token(
170159
raise AuthenticationException()
171160
resp.raise_for_status()
172161
return XSTSResponse(**resp.json())
173-
174-
async def request_device_token(self, device_id: uuid.UUID) -> XADResponse:
175-
url = "https://device.auth.xboxlive.com/device/authenticate"
176-
headers = {"x-xbl-contract-version": "1"}
177-
data = {
178-
"RelyingParty": "http://auth.xboxlive.com",
179-
"TokenType": "JWT",
180-
"Properties": {
181-
"AuthMethod": "ProofOfPossession",
182-
"Id": str(device_id).upper(),
183-
"DeviceType": "Win32",
184-
"Version": "10.0.22000.194",
185-
"ProofKey": self.session.request_signer.proof_field,
186-
},
187-
}
188-
189-
request = httpx.Request("POST", url, headers=headers, json=data)
190-
resp = await self.session.send_signed(request)
191-
resp.raise_for_status()
192-
return XADResponse(**resp.json())

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)