Skip to content

Commit 71f79ba

Browse files
committed
Update support
Add: Microsoft Account Support Add: Login status persistence (Microsoft account)
1 parent bcd156e commit 71f79ba

File tree

2 files changed

+274
-13
lines changed

2 files changed

+274
-13
lines changed

minecraft/authentication.py

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import requests
22
import json
33
import uuid
4+
import os
45
from .exceptions import YggdrasilError
56

67
#: The base url for Ygdrassil requests
@@ -264,6 +265,247 @@ def join(self, server_id):
264265
_raise_from_response(res)
265266
return True
266267

268+
class Microsoft_AuthenticationToken(object):
269+
"""
270+
Represents an authentication token.
271+
272+
See https://wiki.vg/Microsoft_Authentication_Scheme.
273+
"""
274+
275+
276+
UserLoginURL = "https://login.live.com/oauth20_authorize.srf?client_id=00000000402b5328&response_type=code\
277+
&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf"
278+
oauth20_URL = 'https://login.live.com/oauth20_token.srf'
279+
XBL_URL = 'https://user.auth.xboxlive.com/user/authenticate'
280+
XSTS_URL = 'https://xsts.auth.xboxlive.com/xsts/authorize'
281+
LOGIN_WITH_XBOX_URL = 'https://api.minecraftservices.com/authentication/login_with_xbox'
282+
CheckAccount_URL = 'https://api.minecraftservices.com/entitlements/mcstore'
283+
Profile_URL = 'https://api.minecraftservices.com/minecraft/profile'
284+
285+
jwt_Token=''
286+
287+
def __init__(self, access_token=None):
288+
self.access_token = access_token
289+
self.profile = Profile()
290+
291+
def GetoAuth20(self, code: str='') -> object:
292+
if code == '':
293+
print("Please copy this link to your browser to open: \n%s" % self.UserLoginURL)
294+
code = input("After logging in, paste the 'code' field in your browser's address bar here:")
295+
oauth20 = requests.post(self.oauth20_URL, data={
296+
"client_id":"00000000402b5328",
297+
"code":f"{code}",
298+
"grant_type":"authorization_code",
299+
"redirect_uri":"https://login.live.com/oauth20_desktop.srf",
300+
"scope":"service::user.auth.xboxlive.com::MBI_SSL"
301+
},
302+
headers={"content-type": "application/x-www-form-urlencoded"},
303+
timeout=15
304+
)
305+
oauth20 = json.loads(oauth20.text)
306+
if 'error' in oauth20:
307+
print("Error: %s" % oauth20["error"])
308+
return 1
309+
else:
310+
self.oauth20_access_token=oauth20['access_token']
311+
self.oauth20_refresh_token=oauth20['refresh_token']
312+
oauth20_access_token=oauth20['access_token']
313+
oauth20_refresh_token=oauth20['refresh_token']
314+
return {"access_token":oauth20_access_token,"refresh_token":oauth20_refresh_token}
315+
316+
def GetXBL(self, access_token: str) -> object:
317+
XBL = requests.post(self.XBL_URL,
318+
json={"Properties": {"AuthMethod": "RPS","SiteName": "user.auth.xboxlive.com","RpsTicket": f"{access_token}"},
319+
"RelyingParty": "http://auth.xboxlive.com","TokenType": "JWT"},
320+
headers=HEADERS, timeout=15
321+
)
322+
return {
323+
"Token": json.loads(XBL.text)['Token'],
324+
"uhs": json.loads(XBL.text)['DisplayClaims']['xui'][0]['uhs']
325+
}
326+
327+
def GetXSTS(self, access_token: str) -> object:
328+
XBL = requests.post(self.XSTS_URL,
329+
json={
330+
"Properties": {"SandboxId": "RETAIL","UserTokens": [f"{access_token}"]},
331+
"RelyingParty": "rp://api.minecraftservices.com/","TokenType": "JWT"
332+
},
333+
headers=HEADERS, timeout=15
334+
)
335+
return {
336+
"Token": json.loads(XBL.text)['Token'],
337+
"uhs": json.loads(XBL.text)['DisplayClaims']['xui'][0]['uhs']
338+
}
339+
340+
def GetXBOX(self, access_token: str,uhs: str) -> str:
341+
mat_jwt = requests.post(self.LOGIN_WITH_XBOX_URL, json={"identityToken": f"XBL3.0 x={uhs};{access_token}"},
342+
headers=HEADERS, timeout=15)
343+
self.access_token = json.loads(mat_jwt.text)['access_token']
344+
return self.access_token
345+
346+
def CheckAccount(self, jwt_Token: str) -> bool:
347+
CheckAccount = requests.get(self.CheckAccount_URL, headers={"Authorization": f"Bearer {jwt_Token}"},
348+
timeout=15)
349+
CheckAccount = len(json.loads(CheckAccount.text)['items'])
350+
if CheckAccount != 0:
351+
return True
352+
else:
353+
return False
354+
355+
def GetProfile(self, access_token: str) -> object:
356+
if self.CheckAccount(access_token):
357+
Profile = requests.get(self.Profile_URL, headers={"Authorization": f"Bearer {access_token}"},
358+
timeout=15)
359+
Profile = json.loads(Profile.text)
360+
if 'error' in Profile:
361+
return False
362+
self.profile.id_ = Profile["id"]
363+
self.profile.name = Profile["name"]
364+
self.username = Profile["name"]
365+
return True
366+
else:
367+
return False
368+
369+
@property
370+
def authenticated(self):
371+
"""
372+
Attribute which is ``True`` when the token is authenticated and
373+
``False`` when it isn't.
374+
"""
375+
if not self.username:
376+
return False
377+
378+
if not self.access_token:
379+
return False
380+
381+
if not self.oauth20_refresh_token:
382+
return False
383+
384+
if not self.profile:
385+
return False
386+
387+
return True
388+
389+
def authenticate(self):
390+
"Get verification information for a Microsoft account"
391+
oauth20 = self.GetoAuth20()
392+
XBL = self.GetXBL(oauth20['access_token'])
393+
XSTS = self.GetXSTS(XBL['Token'])
394+
XBOX = self.GetXBOX(XSTS['Token'],XSTS['uhs'])
395+
if self.GetProfile(XBOX):
396+
print(f'GameID: {self.profile.id_}')
397+
self.PersistenceLogoin_w()
398+
return True
399+
else:
400+
print('Account does not exist')
401+
return False
402+
403+
def refresh(self):
404+
"""
405+
Refreshes the `AuthenticationToken`. Used to keep a user logged in
406+
between sessions and is preferred over storing a user's password in a
407+
file.
408+
409+
Returns:
410+
Returns `True` if `AuthenticationToken` was successfully refreshed.
411+
Otherwise it raises an exception.
412+
413+
Raises:
414+
minecraft.exceptions.YggdrasilError
415+
ValueError - if `AuthenticationToken.access_token` or
416+
`AuthenticationToken.client_token` isn't set.
417+
"""
418+
if self.access_token is None:
419+
raise ValueError("'access_token' not set!'")
420+
421+
if self.oauth20_refresh_token is None:
422+
raise ValueError("'oauth20_refresh_token' is not set!")
423+
424+
oauth20 = requests.post(self.oauth20_URL,data={
425+
"client_id":"00000000402b5328",
426+
"refresh_token":f"{self.oauth20_refresh_token}",
427+
"grant_type":"refresh_token",
428+
"redirect_uri":"https://login.live.com/oauth20_desktop.srf",
429+
"scope":"service::user.auth.xboxlive.com::MBI_SSL"
430+
},
431+
headers={"content-type": "application/x-www-form-urlencoded"},
432+
timeout=15
433+
)
434+
oauth20 = json.loads(oauth20.text)
435+
if 'error' in oauth20:
436+
print("Error: %s" % oauth20["error"])
437+
return False
438+
else:
439+
self.oauth20_access_token=oauth20['access_token']
440+
self.oauth20_refresh_token=oauth20['refresh_token']
441+
XBL = self.GetXBL(self.oauth20_access_token)
442+
XSTS = self.GetXSTS(XBL['Token'])
443+
XBOX = self.GetXBOX(XSTS['Token'],XSTS['uhs'])
444+
if self.GetProfile(XBOX):
445+
print(f'账户: {self.profile.id_}')
446+
return True
447+
else:
448+
print('账户不存在')
449+
return False
450+
451+
def join(self, server_id):
452+
"""
453+
Informs the Mojang session-server that we're joining the
454+
MineCraft server with id ``server_id``.
455+
456+
Parameters:
457+
server_id - ``str`` with the server id
458+
459+
Returns:
460+
``True`` if no errors occured
461+
462+
Raises:
463+
:class:`minecraft.exceptions.YggdrasilError`
464+
465+
"""
466+
if not self.authenticated:
467+
err = "AuthenticationToken hasn't been authenticated yet!"
468+
raise YggdrasilError(err)
469+
470+
res = _make_request(SESSION_SERVER, "join",
471+
{"accessToken": self.access_token,
472+
"selectedProfile": self.profile.to_dict(),
473+
"serverId": server_id})
474+
475+
if res.status_code != 204:
476+
_raise_from_response(res)
477+
return True
478+
479+
def PersistenceLogoin_w(self):
480+
"Save access token persistent login"
481+
if not self.authenticated:
482+
err = "AuthenticationToken hasn't been authenticated yet!"
483+
raise YggdrasilError(err)
484+
if not os.path.exists("Persistence"):
485+
os.mkdir("Persistence")
486+
"Save access_token and oauth20_refresh_token"
487+
with open(f"Persistence/{self.username}", mode='w', encoding='utf-8') as file_obj:
488+
file_obj.write(f'{{"access_token": "{self.access_token}","oauth20_refresh_token": "{self.oauth20_refresh_token}"}}')
489+
file_obj.close()
490+
return True
491+
492+
def PersistenceLogoin_r(self, GameID: str):
493+
"Load access token persistent login"
494+
if not os.path.exists("Persistence"):
495+
return False
496+
"Load access_token and oauth20_refresh_token"
497+
if os.path.isfile(f"Persistence/{GameID}"):
498+
with open(f"Persistence/{GameID}", mode='r', encoding='utf-8') as file_obj:
499+
Persistence = file_obj.read()
500+
file_obj.close()
501+
Persistence = json.loads(Persistence)
502+
self.access_token = Persistence["access_token"]
503+
self.oauth20_refresh_token = Persistence["oauth20_refresh_token"]
504+
self.GetProfile(self.access_token)
505+
return self.authenticated
506+
else:
507+
return False
508+
267509

268510
def _make_request(server, endpoint, data):
269511
"""

start.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
def get_options():
1515
parser = OptionParser()
1616

17+
parser.add_option("-a", "--authentication-method", dest="auth",
18+
default="microsoft",
19+
help="what to use for authentication, allowed values are: microsoft, mojang")
20+
1721
parser.add_option("-u", "--username", dest="username", default=None,
18-
help="username to log in with")
22+
help="User name used for login, if AUTH is microsoft and a persistent archive is detected locally, the persistent login information will be read first")
1923

2024
parser.add_option("-p", "--password", dest="password", default=None,
2125
help="password to log in with")
@@ -38,13 +42,15 @@ def get_options():
3842

3943
(options, args) = parser.parse_args()
4044

41-
if not options.username:
42-
options.username = input("Enter your username: ")
45+
if options.auth == 'mojang':
46+
47+
if not options.username:
48+
options.username = input("Enter your username: ")
4349

44-
if not options.password and not options.offline:
45-
options.password = getpass.getpass("Enter your password (leave "
46-
"blank for offline mode): ")
47-
options.offline = options.offline or (options.password == "")
50+
if not options.password and not options.offline:
51+
options.password = getpass.getpass("Enter your password (leave "
52+
"blank for offline mode): ")
53+
options.offline = options.offline or (options.password == "")
4854

4955
if not options.server:
5056
options.server = input("Enter server host or host:port "
@@ -68,12 +74,25 @@ def main():
6874
connection = Connection(
6975
options.address, options.port, username=options.username)
7076
else:
71-
auth_token = authentication.AuthenticationToken()
72-
try:
73-
auth_token.authenticate(options.username, options.password)
74-
except YggdrasilError as e:
75-
print(e)
76-
sys.exit()
77+
if options.auth == "mojang":
78+
auth_token = authentication.AuthenticationToken()
79+
try:
80+
auth_token.authenticate(options.username, options.password)
81+
except YggdrasilError as e:
82+
print(e)
83+
sys.exit()
84+
elif options.auth == "microsoft":
85+
auth_token = authentication.Microsoft_AuthenticationToken()
86+
try:
87+
if options.username:
88+
if not auth_token.PersistenceLogoin_r(options.username):
89+
print(f"登陆 {options.username} 失败")
90+
else:
91+
auth_token.authenticate()
92+
except YggdrasilError as e:
93+
print(e)
94+
sys.exit()
95+
7796
print("Logged in as %s..." % auth_token.username)
7897
connection = Connection(
7998
options.address, options.port, auth_token=auth_token)

0 commit comments

Comments
 (0)