Skip to content

Commit 381bb4d

Browse files
committed
Microsoft API for Authentication
1 parent 5da7602 commit 381bb4d

File tree

5 files changed

+247
-4
lines changed

5 files changed

+247
-4
lines changed

mojang/account/auth/microsoft.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import requests
2+
from mojang.exceptions import *
3+
4+
from ..utils.auth import URLs
5+
6+
7+
HEADERS_ACCEPT_URL_ENCODED = {
8+
'content-type': 'application/x-www-form-urlencoded'
9+
}
10+
11+
HEADERS_ACCEPT_JSON = {
12+
'content-type': 'application/json',
13+
'accept': 'application/json'
14+
}
15+
16+
17+
def get_login_url(client_id: str, redirect_uri: str = 'http://example.com') -> str:
18+
"""Returns the login url for the browser
19+
20+
Args:
21+
client_id (str): Azure Active Directory App's client id
22+
redirect_uri (str, optional): The redirect uri of your application
23+
"""
24+
return URLs.microsoft_authorize(client_id, redirect_uri)
25+
26+
27+
def authorize(client_id: str, client_secret: str, auth_code: str, redirect_uri: str = 'http://example.com') -> tuple:
28+
"""Retrieve the access token and refresh token from the given auth code
29+
30+
Args:
31+
client_id (str): Azure Active Directory App's client id
32+
client_secret (str): Azure Active Directory App's client secret
33+
auth_code (str): The auth code received from the login url
34+
redirect_uri (str, optional): The redirect uri of your application
35+
36+
Returns:
37+
A tuple containing the access token and refresh token
38+
39+
Raises:
40+
MicrosoftInvalidGrant: If the auth code is invalid
41+
"""
42+
43+
data = {
44+
'client_id': client_id,
45+
'client_secret': client_secret,
46+
'code': auth_code,
47+
'grant_type': 'authorization_code',
48+
'redirect_uri': redirect_uri
49+
}
50+
51+
response = requests.post(URLs.microsoft_token(), headers=HEADERS_ACCEPT_URL_ENCODED, data=data)
52+
data = handle_response(response, MicrosoftInvalidGrant)
53+
54+
return data['access_token'], data['refresh_token']
55+
56+
def refresh(client_id: str, client_secret: str, refresh_token: str, redirect_uri: str = 'http://example.com') -> tuple:
57+
"""Refresh an access token
58+
59+
Args:
60+
client_id (str): Azure Active Directory App's client id
61+
client_secret (str): Azure Active Directory App's client secret
62+
refresh_token (str): The refresh token
63+
redirect_uri (str, optional): The redirect uri of your application
64+
65+
Returns:
66+
A tuple containing the access token and refresh token
67+
68+
Raises:
69+
MicrosoftInvalidGrant: If the auth code is invalid
70+
"""
71+
72+
data = {
73+
'client_id': client_id,
74+
'client_secret': client_secret,
75+
'refresh_token': refresh_token,
76+
'grant_type': 'refresh_token',
77+
'redirect_uri': redirect_uri
78+
}
79+
80+
response = requests.post(URLs.microsoft_token(), headers=HEADERS_ACCEPT_URL_ENCODED, data=data)
81+
data = handle_response(response, MicrosoftInvalidGrant)
82+
83+
return data['access_token'], data['refresh_token']
84+
85+
86+
def authenticate_xbl(auth_token: str) -> tuple:
87+
"""Authenticate with Xbox Live
88+
89+
Args:
90+
auth_token (str): The access token received from the authentication with Microsoft
91+
92+
Returns:
93+
A tuple containing the Xbox Live token and user hash
94+
95+
Raises:
96+
XboxLiveAuthenticationError: If the auth token is invalid
97+
"""
98+
99+
data = {
100+
"Properties": {
101+
"AuthMethod": "RPS",
102+
"SiteName": "user.auth.xboxlive.com",
103+
"RpsTicket": "d={}".format(auth_token)
104+
},
105+
"RelyingParty": "http://auth.xboxlive.com",
106+
"TokenType": "JWT"
107+
}
108+
109+
response = requests.post(URLs.microsoft_xbl_authenticate(), headers=HEADERS_ACCEPT_JSON, json=data)
110+
data = handle_response(response, XboxLiveAuthenticationError)
111+
return data['Token'], data['DisplayClaims']['xui'][0]['uhs']
112+
113+
def authenticate_xsts(xbl_token: str) -> tuple:
114+
"""Retrieve the XSTS Token
115+
116+
Args:
117+
xbl_token (str): The Xbox Live token
118+
119+
Returns:
120+
A tuple containing the XSTS token and user hash
121+
122+
Raises:
123+
XboxLiveAuthenticationError: If the xbl token is invalid
124+
"""
125+
126+
data = {
127+
"Properties": {
128+
"SandboxId": "RETAIL",
129+
"UserTokens": [xbl_token]
130+
},
131+
"RelyingParty": "rp://api.minecraftservices.com/",
132+
"TokenType": "JWT"
133+
}
134+
135+
response = requests.post(URLs.microsoft_xbl_authorize(), headers=HEADERS_ACCEPT_JSON, json=data)
136+
data = handle_response(response, XboxLiveAuthenticationError)
137+
return data['Token'], data['DisplayClaims']['xui'][0]['uhs']
138+
139+
def authenticate_minecraft(userhash: str, xsts_token: str):
140+
"""Login to minecraft using Xbox Live
141+
Args:
142+
user_hash (str): The user hash from Xbox Live
143+
xbl_token (str): The XSTS Token from Xbox Live
144+
145+
Returns:
146+
The minecraft access token
147+
148+
Raises:
149+
XboxLiveInvalidUserHash: If the user hash is invalid
150+
Unauthorized: If the XSTS token is invalid
151+
"""
152+
153+
data = {"identityToken": f"XBL3.0 x={userhash};{xsts_token}"}
154+
155+
response = requests.post(URLs.login_with_microsoft(), headers=HEADERS_ACCEPT_JSON, json=data)
156+
data = handle_response(response, XboxLiveInvalidUserHash, Unauthorized)
157+
return data['access_token']

mojang/account/session.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime as dt
22

3+
import jwt
34
import requests
45

56
from ..exceptions import *
@@ -110,3 +111,37 @@ def reset_user_skin(access_token: str, uuid: str):
110111
"""
111112
response = requests.delete(URLs.reset_skin(uuid), auth=BearerAuth(access_token))
112113
handle_response(response, PayloadError, Unauthorized)
114+
115+
def owns_minecraft(access_token: str, verify_sig: bool = False, public_key: str = None) -> bool:
116+
"""Returns True if the authenticated user owns minecraft
117+
118+
Args:
119+
access_token (str): The session's access token
120+
verify_sig (bool, optional): If True, will check the jwt sig with the public key
121+
public_key (str, optional): The key to use to verify jwt sig
122+
123+
Returns:
124+
True if user owns the game, else False
125+
126+
Raises:
127+
Unauthorized: If the access token is invalid
128+
129+
Example:
130+
131+
```python
132+
from mojang.account import session
133+
134+
if session.owns_minecraft('ACCESS_TOKEN'):
135+
print('This user owns minecraft')
136+
```
137+
"""
138+
response = requests.get(URLs.check_minecraft_onwership(), auth=BearerAuth(access_token))
139+
data = handle_response(response, Unauthorized)
140+
141+
if verify_sig:
142+
for i in data.get('items', []):
143+
jwt.decode(i['signature'], public_key, algorithms=['RS256'])
144+
145+
jwt.decode(data['signature'], public_key, algorithms=['RS256'])
146+
147+
return not len(data['items']) == 0

mojang/account/utils/auth.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,28 @@ def get_challenges(cls):
5050
"""Returns the url to get the challenges for user IP verification"""
5151
return 'https://api.mojang.com/user/security/challenges'
5252

53+
# Microsoft
54+
@classmethod
55+
def microsoft_authorize(cls, client_id: str, redirect_uri: str):
56+
"""Returns the authorization url for Microsoft OAuth"""
57+
return f'https://login.live.com/oauth20_authorize.srf?client_id={client_id}&response_type=code&redirect_uri={redirect_uri}&scope=XboxLive.signin%20offline_access'
58+
59+
@classmethod
60+
def microsoft_token(cls):
61+
"""Returns the token url for Microsoft OAuth"""
62+
return 'https://login.live.com/oauth20_token.srf'
63+
64+
@classmethod
65+
def microsoft_xbl_authenticate(cls):
66+
"""Returns the authentication url for Xbox Live"""
67+
return 'https://user.auth.xboxlive.com/user/authenticate'
68+
69+
@classmethod
70+
def microsoft_xbl_authorize(cls):
71+
"""Returns the authorization url for Xbox Live"""
72+
return 'https://xsts.auth.xboxlive.com/xsts/authorize'
73+
74+
@classmethod
75+
def login_with_microsoft(cls):
76+
"""Returns the url to login to minecraft with Xbox Live"""
77+
return 'https://api.minecraftservices.com/authentication/login_with_xbox'

mojang/account/utils/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,7 @@ def change_skin(cls):
4747
def reset_skin(cls, uuid: str):
4848
"""Returns the url to reset the user's skin"""
4949
return f'https://api.mojang.com/user/profile/{uuid}/skin'
50+
51+
@classmethod
52+
def check_minecraft_onwership(cls):
53+
return 'https://api.minecraftservices.com/entitlements/mcstore'

mojang/exceptions.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
13

24
def handle_response(response, *exceptions, use_defaults=True):
35
"""Handle response message from http request. Every given `exception`
@@ -10,21 +12,25 @@ def handle_response(response, *exceptions, use_defaults=True):
1012
data = {}
1113
try:
1214
data = response.json()
13-
except ValueError:
15+
except json.decoder.JSONDecodeError:
1416
pass
1517
finally:
1618
return data
1719
else:
1820
if use_defaults:
1921
exceptions += (NotFound, MethodNotAllowed, ServerError)
20-
data = response.json()
22+
try:
23+
data = response.json()
24+
except json.decoder.JSONDecodeError:
25+
data = {'errorMessage': response.text}
26+
2127
for exception in exceptions:
2228
if isinstance(exception.code, int):
2329
if response.status_code == exception.code:
24-
raise exception(data['errorMessage'])
30+
raise exception(*data.values())
2531
elif isinstance(exception.code, list):
2632
if response.status_code in exception.code:
27-
raise exception(data['errorMessage'])
33+
raise exception(*data.values())
2834
else:
2935
raise Exception(*data.values())
3036

@@ -66,6 +72,22 @@ class Unauthorized(Exception):
6672
code = 401
6773

6874

75+
# Microsoft Authentication Errors
76+
class MicrosoftInvalidGrant(Exception):
77+
"""The auth code or refresh token sent to the server is invalid"""
78+
code = 400
79+
80+
81+
class XboxLiveAuthenticationError(Exception):
82+
"""Authentication with Xbox Live failed"""
83+
code = 400
84+
85+
86+
class XboxLiveInvalidUserHash(Exception):
87+
"""The user hash sent to the server is invalid"""
88+
code = 400
89+
90+
6991
# Name Change Errors
7092
class InvalidName(Exception):
7193
"""The name is invalid, longer than 16 characters or contains

0 commit comments

Comments
 (0)