2015-04-01 23:38:10 +02:00
|
|
|
"""
|
|
|
|
Handles authentication with the Mojang authentication server.
|
|
|
|
"""
|
|
|
|
import requests
|
2015-03-21 23:08:44 +01:00
|
|
|
import json
|
2015-04-01 23:38:10 +02:00
|
|
|
from .exceptions import YggdrasilError
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-02 23:25:34 +02:00
|
|
|
AUTH_SERVER = "https://authserver.mojang.com"
|
|
|
|
SESSION_SERVER = "https://sessionserver.mojang.com/session/minecraft"
|
2015-04-01 23:38:10 +02:00
|
|
|
# Need this content type, or authserver will complain
|
|
|
|
CONTENT_TYPE = "application/json"
|
|
|
|
HEADERS = {"content-type": CONTENT_TYPE}
|
2015-03-21 23:08:44 +01:00
|
|
|
|
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
class Profile(object):
|
|
|
|
"""
|
|
|
|
Container class for a MineCraft Selected profile.
|
|
|
|
See: `<http://wiki.vg/Authentication>`_
|
2015-03-21 23:08:44 +01:00
|
|
|
"""
|
2015-04-01 23:38:10 +02:00
|
|
|
def __init__(self, id_=None, name=None):
|
2015-04-06 15:27:42 +02:00
|
|
|
self.id_ = id_
|
2015-04-01 23:38:10 +02:00
|
|
|
self.name = name
|
|
|
|
|
|
|
|
def to_dict(self):
|
|
|
|
"""
|
|
|
|
Returns ``self`` in dictionary-form, which can be serialized by json.
|
|
|
|
"""
|
|
|
|
if self:
|
2015-04-06 15:27:42 +02:00
|
|
|
return {"id": self.id_,
|
2015-04-01 23:38:10 +02:00
|
|
|
"name": self.name}
|
|
|
|
else:
|
|
|
|
raise AttributeError("Profile is not yet populated.")
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
def __bool__(self):
|
2015-04-06 15:27:42 +02:00
|
|
|
bool_state = self.id_ is not None and self.name is not None
|
2015-04-01 23:38:10 +02:00
|
|
|
return bool_state
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
# Python 2 support
|
|
|
|
def __nonzero__(self):
|
|
|
|
return self.__bool__()
|
2015-03-21 23:08:44 +01:00
|
|
|
|
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
class AuthenticationToken(object):
|
2015-03-21 23:08:44 +01:00
|
|
|
"""
|
2015-04-01 23:38:10 +02:00
|
|
|
Represents an authentication token.
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
See http://wiki.vg/Authentication.
|
|
|
|
"""
|
|
|
|
AGENT_NAME = "Minecraft"
|
|
|
|
AGENT_VERSION = 1
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
def __init__(self, username=None, access_token=None, client_token=None):
|
|
|
|
"""
|
|
|
|
Constructs an `AuthenticationToken` based on `access_token` and
|
|
|
|
`client_token`.
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
Parameters:
|
|
|
|
access_token - An `str` object containing the `access_token`.
|
|
|
|
client_token - An `str` object containing the `client_token`.
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
Returns:
|
|
|
|
A `AuthenticationToken` with `access_token` and `client_token` set.
|
|
|
|
"""
|
|
|
|
self.username = username
|
|
|
|
self.access_token = access_token
|
|
|
|
self.client_token = client_token
|
|
|
|
self.profile = Profile()
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
@property
|
|
|
|
def authenticated(self):
|
|
|
|
"""
|
|
|
|
Attribute which is ``True`` when the token is authenticated and
|
|
|
|
``False`` when it isn't.
|
|
|
|
"""
|
2015-04-02 00:09:52 +02:00
|
|
|
if not self.username:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if not self.access_token:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if not self.client_token:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if not self.profile:
|
|
|
|
return False
|
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
return True
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
def authenticate(self, username, password):
|
|
|
|
"""
|
|
|
|
Authenticates the user against https://authserver.mojang.com using
|
|
|
|
`username` and `password` parameters.
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
Parameters:
|
|
|
|
username - An `str` object with the username (unmigrated accounts)
|
|
|
|
or email address for a Mojang account.
|
|
|
|
password - An `str` object with the password.
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
Returns:
|
|
|
|
Returns `True` if successful.
|
|
|
|
Otherwise it will raise an exception.
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
Raises:
|
|
|
|
minecraft.exceptions.YggdrasilError
|
|
|
|
"""
|
|
|
|
payload = {
|
|
|
|
"agent": {
|
|
|
|
"name": self.AGENT_NAME,
|
|
|
|
"version": self.AGENT_VERSION
|
|
|
|
},
|
|
|
|
"username": username,
|
|
|
|
"password": password
|
|
|
|
}
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-02 23:25:34 +02:00
|
|
|
req = _make_request(AUTH_SERVER, "authenticate", payload)
|
2015-04-01 23:38:10 +02:00
|
|
|
|
|
|
|
_raise_from_request(req)
|
|
|
|
|
|
|
|
json_resp = req.json()
|
|
|
|
|
|
|
|
self.username = username
|
|
|
|
self.access_token = json_resp["accessToken"]
|
|
|
|
self.client_token = json_resp["clientToken"]
|
2015-04-06 15:27:42 +02:00
|
|
|
self.profile.id_ = json_resp["selectedProfile"]["id"]
|
2015-04-01 23:38:10 +02:00
|
|
|
self.profile.name = json_resp["selectedProfile"]["name"]
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def refresh(self):
|
|
|
|
"""
|
|
|
|
Refreshes the `AuthenticationToken`. Used to keep a user logged in
|
|
|
|
between sessions and is preferred over storing a user's password in a
|
|
|
|
file.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Returns `True` if `AuthenticationToken` was successfully refreshed.
|
|
|
|
Otherwise it raises an exception.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
minecraft.exceptions.YggdrasilError
|
|
|
|
ValueError - if `AuthenticationToken.access_token` or
|
|
|
|
`AuthenticationToken.client_token` isn't set.
|
|
|
|
"""
|
|
|
|
if self.access_token is None:
|
|
|
|
raise ValueError("'access_token' not set!'")
|
|
|
|
|
|
|
|
if self.client_token is None:
|
|
|
|
raise ValueError("'client_token' is not set!")
|
|
|
|
|
2015-04-02 23:25:34 +02:00
|
|
|
req = _make_request(AUTH_SERVER,
|
2015-04-01 23:38:10 +02:00
|
|
|
"refresh", {"accessToken": self.access_token,
|
|
|
|
"clientToken": self.client_token})
|
|
|
|
|
|
|
|
_raise_from_request(req)
|
|
|
|
|
|
|
|
json_resp = req.json()
|
|
|
|
|
|
|
|
self.access_token = json_resp["accessToken"]
|
|
|
|
self.client_token = json_resp["clientToken"]
|
2015-04-06 15:44:51 +02:00
|
|
|
self.profile.id_ = json_resp["selectedProfile"]["id"]
|
2015-04-01 23:38:10 +02:00
|
|
|
self.profile.name = json_resp["selectedProfile"]["name"]
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def validate(self):
|
|
|
|
"""
|
|
|
|
Validates the AuthenticationToken.
|
|
|
|
|
|
|
|
`AuthenticationToken.access_token` must be set!
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
Returns:
|
|
|
|
Returns `True` if `AuthenticationToken` is valid.
|
|
|
|
Otherwise it will raise an exception.
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
Raises:
|
|
|
|
minecraft.exceptions.YggdrasilError
|
|
|
|
ValueError - if `AuthenticationToken.access_token` is not set.
|
|
|
|
"""
|
|
|
|
if self.access_token is None:
|
|
|
|
raise ValueError("'access_token' not set!")
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-02 23:25:34 +02:00
|
|
|
req = _make_request(AUTH_SERVER, "validate",
|
2015-04-01 23:38:10 +02:00
|
|
|
{"accessToken": self.access_token})
|
|
|
|
|
|
|
|
if _raise_from_request(req) is None:
|
|
|
|
return True
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def sign_out(username, password):
|
|
|
|
"""
|
|
|
|
Invalidates `access_token`s using an account's
|
|
|
|
`username` and `password`.
|
|
|
|
|
|
|
|
Parameters:
|
2015-04-02 00:09:52 +02:00
|
|
|
username - ``str`` containing the username
|
|
|
|
password - ``str`` containing the password
|
2015-04-01 23:38:10 +02:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
Returns `True` if sign out was successful.
|
|
|
|
Otherwise it will raise an exception.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
minecraft.exceptions.YggdrasilError
|
|
|
|
"""
|
2015-04-02 23:25:34 +02:00
|
|
|
req = _make_request(AUTH_SERVER, "signout",
|
|
|
|
{"username": username, "password": password})
|
2015-04-01 23:38:10 +02:00
|
|
|
|
|
|
|
if _raise_from_request(req) is None:
|
|
|
|
return True
|
|
|
|
|
|
|
|
def invalidate(self):
|
|
|
|
"""
|
|
|
|
Invalidates `access_token`s using the token pair stored in
|
|
|
|
the `AuthenticationToken`.
|
|
|
|
|
|
|
|
Returns:
|
2015-04-02 00:09:52 +02:00
|
|
|
``True`` if tokens were successfully invalidated.
|
2015-04-01 23:38:10 +02:00
|
|
|
|
|
|
|
Raises:
|
2015-04-02 00:09:52 +02:00
|
|
|
:class:`minecraft.exceptions.YggdrasilError`
|
2015-04-01 23:38:10 +02:00
|
|
|
"""
|
2015-04-02 23:25:34 +02:00
|
|
|
req = _make_request(AUTH_SERVER, "invalidate",
|
2015-04-02 00:09:52 +02:00
|
|
|
{"accessToken": self.access_token,
|
|
|
|
"clientToken": self.client_token})
|
|
|
|
|
2015-04-02 23:25:34 +02:00
|
|
|
if not req.raise_for_status() and not req.text:
|
2015-04-02 00:09:52 +02:00
|
|
|
return True
|
|
|
|
else:
|
|
|
|
raise YggdrasilError("Failed to invalidate tokens.")
|
2015-04-01 23:38:10 +02:00
|
|
|
|
|
|
|
def join(self, server_id):
|
|
|
|
"""
|
|
|
|
Informs the Mojang session-server that we're joining the
|
|
|
|
MineCraft server with id ``server_id``.
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
server_id - ``str`` with the server id
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
``True`` if no errors occured
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
:class:`minecraft.exceptions.YggdrasilError`
|
|
|
|
|
|
|
|
"""
|
2015-04-02 00:09:52 +02:00
|
|
|
if not self.authenticated:
|
|
|
|
err = "AuthenticationToken hasn't been authenticated yet!"
|
|
|
|
raise YggdrasilError(err)
|
|
|
|
|
2015-04-02 23:25:34 +02:00
|
|
|
req = _make_request(SESSION_SERVER, "join",
|
2015-04-01 23:38:10 +02:00
|
|
|
{"accessToken": self.access_token,
|
|
|
|
"selectedProfile": self.profile.to_dict(),
|
|
|
|
"serverId": server_id})
|
|
|
|
|
2015-04-02 19:02:08 +02:00
|
|
|
if not req.raise_for_status():
|
2015-04-01 23:38:10 +02:00
|
|
|
return True
|
|
|
|
else:
|
2015-04-02 19:02:08 +02:00
|
|
|
_raise_from_request(req)
|
2015-04-01 23:38:10 +02:00
|
|
|
|
|
|
|
|
|
|
|
def _make_request(server, endpoint, data):
|
2015-03-21 23:08:44 +01:00
|
|
|
"""
|
2015-04-01 23:38:10 +02:00
|
|
|
Fires a POST with json-packed data to the given endpoint and returns
|
|
|
|
response.
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
Parameters:
|
|
|
|
endpoint - An `str` object with the endpoint, e.g. "authenticate"
|
|
|
|
data - A `dict` containing the payload data.
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
Returns:
|
|
|
|
A `requests.Request` object.
|
2015-03-21 23:08:44 +01:00
|
|
|
"""
|
2015-04-01 23:38:10 +02:00
|
|
|
req = requests.post(server + "/" + endpoint, data=json.dumps(data),
|
|
|
|
headers=HEADERS)
|
|
|
|
return req
|
|
|
|
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
def _raise_from_request(req):
|
2015-03-21 23:08:44 +01:00
|
|
|
"""
|
2015-04-01 23:38:10 +02:00
|
|
|
Raises an appropriate `YggdrasilError` based on the `status_code` and
|
|
|
|
`json` of a `requests.Request` object.
|
|
|
|
"""
|
2015-04-03 00:13:22 +02:00
|
|
|
if req.status_code == requests.codes['ok']:
|
2015-04-01 23:38:10 +02:00
|
|
|
return None
|
|
|
|
|
2015-04-02 19:02:08 +02:00
|
|
|
try:
|
|
|
|
json_resp = req.json()
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-02 19:02:08 +02:00
|
|
|
if "error" not in json_resp and "errorMessage" not in json_resp:
|
|
|
|
raise YggdrasilError("Malformed error message.")
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-02 19:02:08 +02:00
|
|
|
message = "[{status_code}] {error}: '{error_message}'"
|
|
|
|
message = message.format(status_code=str(req.status_code),
|
|
|
|
error=json_resp["error"],
|
|
|
|
error_message=json_resp["errorMessage"])
|
|
|
|
except ValueError:
|
2015-04-05 22:14:38 +02:00
|
|
|
message = "Unknown requests error. Status code: {}"
|
|
|
|
message.format(str(req.status_code))
|
2015-03-21 23:08:44 +01:00
|
|
|
|
2015-04-01 23:38:10 +02:00
|
|
|
raise YggdrasilError(message)
|