Skip to content

Commit

Permalink
Microsoft API for Authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucino772 committed Sep 13, 2021
1 parent 5da7602 commit 381bb4d
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 4 deletions.
157 changes: 157 additions & 0 deletions mojang/account/auth/microsoft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import requests
from mojang.exceptions import *

from ..utils.auth import URLs


HEADERS_ACCEPT_URL_ENCODED = {
'content-type': 'application/x-www-form-urlencoded'
}

HEADERS_ACCEPT_JSON = {
'content-type': 'application/json',
'accept': 'application/json'
}


def get_login_url(client_id: str, redirect_uri: str = 'http://example.com') -> str:
"""Returns the login url for the browser
Args:
client_id (str): Azure Active Directory App's client id
redirect_uri (str, optional): The redirect uri of your application
"""
return URLs.microsoft_authorize(client_id, redirect_uri)


def authorize(client_id: str, client_secret: str, auth_code: str, redirect_uri: str = 'http://example.com') -> tuple:
"""Retrieve the access token and refresh token from the given auth code
Args:
client_id (str): Azure Active Directory App's client id
client_secret (str): Azure Active Directory App's client secret
auth_code (str): The auth code received from the login url
redirect_uri (str, optional): The redirect uri of your application
Returns:
A tuple containing the access token and refresh token
Raises:
MicrosoftInvalidGrant: If the auth code is invalid
"""

data = {
'client_id': client_id,
'client_secret': client_secret,
'code': auth_code,
'grant_type': 'authorization_code',
'redirect_uri': redirect_uri
}

response = requests.post(URLs.microsoft_token(), headers=HEADERS_ACCEPT_URL_ENCODED, data=data)
data = handle_response(response, MicrosoftInvalidGrant)

return data['access_token'], data['refresh_token']

def refresh(client_id: str, client_secret: str, refresh_token: str, redirect_uri: str = 'http://example.com') -> tuple:
"""Refresh an access token
Args:
client_id (str): Azure Active Directory App's client id
client_secret (str): Azure Active Directory App's client secret
refresh_token (str): The refresh token
redirect_uri (str, optional): The redirect uri of your application
Returns:
A tuple containing the access token and refresh token
Raises:
MicrosoftInvalidGrant: If the auth code is invalid
"""

data = {
'client_id': client_id,
'client_secret': client_secret,
'refresh_token': refresh_token,
'grant_type': 'refresh_token',
'redirect_uri': redirect_uri
}

response = requests.post(URLs.microsoft_token(), headers=HEADERS_ACCEPT_URL_ENCODED, data=data)
data = handle_response(response, MicrosoftInvalidGrant)

return data['access_token'], data['refresh_token']


def authenticate_xbl(auth_token: str) -> tuple:
"""Authenticate with Xbox Live
Args:
auth_token (str): The access token received from the authentication with Microsoft
Returns:
A tuple containing the Xbox Live token and user hash
Raises:
XboxLiveAuthenticationError: If the auth token is invalid
"""

data = {
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": "d={}".format(auth_token)
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}

response = requests.post(URLs.microsoft_xbl_authenticate(), headers=HEADERS_ACCEPT_JSON, json=data)
data = handle_response(response, XboxLiveAuthenticationError)
return data['Token'], data['DisplayClaims']['xui'][0]['uhs']

def authenticate_xsts(xbl_token: str) -> tuple:
"""Retrieve the XSTS Token
Args:
xbl_token (str): The Xbox Live token
Returns:
A tuple containing the XSTS token and user hash
Raises:
XboxLiveAuthenticationError: If the xbl token is invalid
"""

data = {
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [xbl_token]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}

response = requests.post(URLs.microsoft_xbl_authorize(), headers=HEADERS_ACCEPT_JSON, json=data)
data = handle_response(response, XboxLiveAuthenticationError)
return data['Token'], data['DisplayClaims']['xui'][0]['uhs']

def authenticate_minecraft(userhash: str, xsts_token: str):
"""Login to minecraft using Xbox Live
Args:
user_hash (str): The user hash from Xbox Live
xbl_token (str): The XSTS Token from Xbox Live
Returns:
The minecraft access token
Raises:
XboxLiveInvalidUserHash: If the user hash is invalid
Unauthorized: If the XSTS token is invalid
"""

data = {"identityToken": f"XBL3.0 x={userhash};{xsts_token}"}

response = requests.post(URLs.login_with_microsoft(), headers=HEADERS_ACCEPT_JSON, json=data)
data = handle_response(response, XboxLiveInvalidUserHash, Unauthorized)
return data['access_token']
35 changes: 35 additions & 0 deletions mojang/account/session.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime as dt

import jwt
import requests

from ..exceptions import *
Expand Down Expand Up @@ -110,3 +111,37 @@ def reset_user_skin(access_token: str, uuid: str):
"""
response = requests.delete(URLs.reset_skin(uuid), auth=BearerAuth(access_token))
handle_response(response, PayloadError, Unauthorized)

def owns_minecraft(access_token: str, verify_sig: bool = False, public_key: str = None) -> bool:
"""Returns True if the authenticated user owns minecraft
Args:
access_token (str): The session's access token
verify_sig (bool, optional): If True, will check the jwt sig with the public key
public_key (str, optional): The key to use to verify jwt sig
Returns:
True if user owns the game, else False
Raises:
Unauthorized: If the access token is invalid
Example:
```python
from mojang.account import session
if session.owns_minecraft('ACCESS_TOKEN'):
print('This user owns minecraft')
```
"""
response = requests.get(URLs.check_minecraft_onwership(), auth=BearerAuth(access_token))
data = handle_response(response, Unauthorized)

if verify_sig:
for i in data.get('items', []):
jwt.decode(i['signature'], public_key, algorithms=['RS256'])

jwt.decode(data['signature'], public_key, algorithms=['RS256'])

return not len(data['items']) == 0
25 changes: 25 additions & 0 deletions mojang/account/utils/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,28 @@ def get_challenges(cls):
"""Returns the url to get the challenges for user IP verification"""
return 'https://api.mojang.com/user/security/challenges'

# Microsoft
@classmethod
def microsoft_authorize(cls, client_id: str, redirect_uri: str):
"""Returns the authorization url for Microsoft OAuth"""
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'

@classmethod
def microsoft_token(cls):
"""Returns the token url for Microsoft OAuth"""
return 'https://login.live.com/oauth20_token.srf'

@classmethod
def microsoft_xbl_authenticate(cls):
"""Returns the authentication url for Xbox Live"""
return 'https://user.auth.xboxlive.com/user/authenticate'

@classmethod
def microsoft_xbl_authorize(cls):
"""Returns the authorization url for Xbox Live"""
return 'https://xsts.auth.xboxlive.com/xsts/authorize'

@classmethod
def login_with_microsoft(cls):
"""Returns the url to login to minecraft with Xbox Live"""
return 'https://api.minecraftservices.com/authentication/login_with_xbox'
4 changes: 4 additions & 0 deletions mojang/account/utils/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ def change_skin(cls):
def reset_skin(cls, uuid: str):
"""Returns the url to reset the user's skin"""
return f'https://api.mojang.com/user/profile/{uuid}/skin'

@classmethod
def check_minecraft_onwership(cls):
return 'https://api.minecraftservices.com/entitlements/mcstore'
30 changes: 26 additions & 4 deletions mojang/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json


def handle_response(response, *exceptions, use_defaults=True):
"""Handle response message from http request. Every given `exception`
Expand All @@ -10,21 +12,25 @@ def handle_response(response, *exceptions, use_defaults=True):
data = {}
try:
data = response.json()
except ValueError:
except json.decoder.JSONDecodeError:
pass
finally:
return data
else:
if use_defaults:
exceptions += (NotFound, MethodNotAllowed, ServerError)
data = response.json()
try:
data = response.json()
except json.decoder.JSONDecodeError:
data = {'errorMessage': response.text}

for exception in exceptions:
if isinstance(exception.code, int):
if response.status_code == exception.code:
raise exception(data['errorMessage'])
raise exception(*data.values())
elif isinstance(exception.code, list):
if response.status_code in exception.code:
raise exception(data['errorMessage'])
raise exception(*data.values())
else:
raise Exception(*data.values())

Expand Down Expand Up @@ -66,6 +72,22 @@ class Unauthorized(Exception):
code = 401


# Microsoft Authentication Errors
class MicrosoftInvalidGrant(Exception):
"""The auth code or refresh token sent to the server is invalid"""
code = 400


class XboxLiveAuthenticationError(Exception):
"""Authentication with Xbox Live failed"""
code = 400


class XboxLiveInvalidUserHash(Exception):
"""The user hash sent to the server is invalid"""
code = 400


# Name Change Errors
class InvalidName(Exception):
"""The name is invalid, longer than 16 characters or contains
Expand Down

0 comments on commit 381bb4d

Please sign in to comment.