Skip to content

Commit

Permalink
Initial support for Microsoft account auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Terrance committed Jul 27, 2016
1 parent 99c20cf commit c289b07
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 23 deletions.
98 changes: 85 additions & 13 deletions skpy/conn.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def externalCall(cls, method, url, codes=(200, 201, 204, 207), **kwargs):
return resp

API_LOGIN = "https://login.skype.com/login"
API_MSACC = "https://login.live.com"
API_USER = "https://api.skype.com"
API_JOIN = "https://join.skype.com"
API_BOT = "https://api.aps.skype.com/v1"
Expand Down Expand Up @@ -247,8 +248,8 @@ def syncStateCall(self, method, url, params={}, **kwargs):

def setUserPwd(self, user, pwd):
"""
Replace the stub :meth:`getSkypeToken` method with one that connects using the given credentials. Avoids
storing the account password in an accessible way.
Replace the stub :meth:`getSkypeToken` method with one that connects via Skype account using the given
credentials. Avoids storing the account password in an accessible way.
Args:
user (str): username of the connecting account
Expand All @@ -258,6 +259,19 @@ def getSkypeToken(self):
self.login(user, pwd)
self.getSkypeToken = MethodType(getSkypeToken, self)

def setMicrosoftAcc(self, email, pwd):
"""
Replace the stub :meth:`getSkypeToken` method with one that connects via Microsoft account using the given
credentials. Avoids storing the account password in an accessible way.
Args:
email (str): email address of the connecting account
pwd (str): password of the connecting account
"""
def getSkypeToken(self):
self.liveLogin(email, pwd)
self.getSkypeToken = MethodType(getSkypeToken, self)

def setTokenFile(self, path):
"""
Enable reading and writing session tokens to a file at the given location.
Expand Down Expand Up @@ -381,7 +395,66 @@ def login(self, user, pwd):
if expiryField:
self.tokenExpiry["skype"] = datetime.fromtimestamp(secs + int(expiryField.get("value")))
self.userId = user
# Invalidate the registration token.
# (Re)generate the registration token.
self.tokens.pop("reg", None)
self.tokenExpiry.pop("reg", None)
self.getRegToken()

def liveLogin(self, email, pwd):
"""
Obtain connection parameters from the Microsoft Account login page, and perform a login with the given email
address and password. This emulates a login to Skype for Web on ``login.live.com``.
.. note:: Microsoft accounts with two-factor authentication enabled are not supported.
Args:
email (str): email address of the connecting account
pwd (str): password of the connecting account
Raises:
SkypeAuthException: if a captcha is required, or the login fails
.SkypeApiException: if the login form can't be processed
"""
# First, start a Microsoft account login from Skype, which will redirect to login.live.com.
loginResp = self("GET", "{0}/oauth/microsoft".format(self.API_LOGIN),
params={"client_id": "578134", "redirect_uri": "https://web.skype.com"})
# This is inside some embedded JavaScript, so can't easily parse with BeautifulSoup.
ppft = re.search(r"<input.*?name=\"PPFT\".*?value=\"(.*?)\"", loginResp.text).group(1)
# Now pass the login credentials over.
loginResp = self("POST", "{0}/ppsecure/post.srf".format(self.API_MSACC),
params={"wa": "wsignin1.0", "wp": "MBI_SSL",
"wreply": "https://lw.skype.com/login/oauth/proxy?client_id=578134&site_name="
"lw.skype.com&redirect_uri=https%3A%2F%2Fweb.skype.com%2F"},
cookies={"MSPRequ": loginResp.cookies.get("MSPRequ"),
"MSPOK": loginResp.cookies.get("MSPOK"),
"CkTst": str(int(time.time() * 1000))},
data={"login": email, "passwd": pwd, "PPFT": ppft})
tField = BeautifulSoup(loginResp.text, "html.parser").find(id="t")
if tField is None:
if "{0}/GetSessionState.srf".format(self.API_MSACC) in loginResp.text:
# Two-factor authentication, not supported as it's rather unwieldy to implement.
raise SkypeAuthException("Two-factor authentication not supported", loginResp)
err = re.search(r"sErrTxt:'([^'\\]*(\\.[^'\\]*)*)'", loginResp.text)
if err:
raise SkypeAuthException(err.group(1), loginResp)
# Now exchange the 't' value for a Skype token.
loginResp = self("POST", "{0}/microsoft".format(self.API_LOGIN),
params={"client_id": "578134", "redirect_uri": "https://web.skype.com"},
data={"t": tField.get("value"), "client_id": "578134", "oauthPartner": "999",
"site_name": "lw.skype.com", "redirect_uri": "https://web.skype.com"})
loginPage = BeautifulSoup(loginResp.text, "html.parser")
tokenField = loginPage.find("input", {"name": "skypetoken"})
if not tokenField:
raise SkypeApiException("Couldn't retrieve Skype token from login response", loginResp)
self.tokens["skype"] = tokenField.get("value")
expiryField = loginPage.find("input", {"name": "expires_in"})
if expiryField:
secs = int(time.time())
self.tokenExpiry["skype"] = datetime.fromtimestamp(secs + int(expiryField.get("value")))
# Figure out what the username is.
self.userId = self("GET", "{0}/users/self/profile".format(self.API_USER),
auth=self.Auth.SkypeToken).json().get("username")
# (Re)generate the registration token.
self.tokens.pop("reg", None)
self.tokenExpiry.pop("reg", None)
self.getRegToken()
Expand All @@ -398,21 +471,20 @@ def guestLogin(self, url, name):
name (str): display name as shown to other participants
"""
urlId = url.split("/")[-1]
# Pretend to be Chrome on Windows (required to avoid "unsupported device" messages)..
# Pretend to be Chrome on Windows (required to avoid "unsupported device" messages).
agent = "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) " \
"Chrome/33.0.1750.117 Safari/537.36"
cookies = self("GET", "{0}/{1}".format(self.API_JOIN, urlId), headers={"User-Agent": agent}).cookies
ids = self("POST", "{0}/api/v2/conversation/".format(self.API_JOIN),
json={"shortId": urlId, "type": "wl"}).json()
headers = {"csrf_token": cookies.get("csrf_token"),
"X-Skype-Request-Id": cookies.get("launcher_session_id")}
json = {"flowId": cookies.get("launcher_session_id"),
"shortId": urlId,
"longId": ids.get("Long"),
"threadId": ids.get("Resource"),
"name": name}
self.tokens["skype"] = self("POST", "{0}/api/v1/users/guests".format(self.API_JOIN),
headers=headers, json=json).json().get("skypetoken")
headers={"csrf_token": cookies.get("csrf_token"),
"X-Skype-Request-Id": cookies.get("launcher_session_id")},
json={"flowId": cookies.get("launcher_session_id"),
"shortId": urlId,
"longId": ids.get("Long"),
"threadId": ids.get("Resource"),
"name": name}).json().get("skypetoken")
# Assume the token lasts 24 hours, as a guest account only lasts that long anyway.
self.tokenExpiry["skype"] = datetime.now() + timedelta(days=1)
self.userId = self("GET", "{0}/users/self/profile".format(self.API_USER),
Expand All @@ -421,7 +493,7 @@ def guestLogin(self, url, name):

def getSkypeToken(self):
"""
A wrapper for :meth:`login` that applies the previously given username and password.
A wrapper for :meth:`login` or :meth:`liveLogin` that applies the previously given username and password.
Raises:
SkypeAuthException: if credentials were never provided
Expand Down
26 changes: 16 additions & 10 deletions skpy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,25 @@ class Skype(SkypeObj):

attrs = ("userId",)

def __init__(self, user=None, pwd=None, tokenFile=None, connect=None):
def __init__(self, user=None, pwd=None, msEmail=None, msPwd=None, tokenFile=None, connect=True):
"""
Create a new Skype object and corresponding connection.
If ``user`` and ``pwd`` are given, they will be passed to :meth:`.SkypeConnection.setUserPwd`. If a token file
path is present, it will be used if valid. On a successful connection, the token file will also be written to.
If ``user`` and ``pwd`` are given, they will be passed to :meth:`.SkypeConnection.setUserPwd`. Similarly,
``msEmail`` and ``msPwd`` are passed to :meth:`.SkypeConnection.setMicrosoftAcc`. Only one pair of login
credentials should be passed (Skype account will take priority over Microsoft account).
By default, a connection attempt will be made if any of ``user``, ``pwd`` or ``tokenFile`` are specified. It
is also possible to handle authentication manually, by working with the underlying connection object instead.
If a token file path is present, it will be used if valid. On a successful connection, the token file will
also be written to.
By default, a connection attempt will be made if any valid form of credentials are supplied. It is also
possible to handle authentication manually, by working with the underlying connection object instead.
Args:
user (str): username of the connecting account
pwd (str): password of the connecting account
user (str): Skype username of the connecting account
pwd (str): corresponding Skype account password
msEmail (str): Microsoft account email address
msPwd (str): corresponding Microsoft account password
tokenFile (str): path to file used for token storage
connect (bool): whether to try and connect straight away
"""
Expand All @@ -55,9 +61,9 @@ def __init__(self, user=None, pwd=None, tokenFile=None, connect=None):
self.conn.setTokenFile(tokenFile)
if user and pwd:
self.conn.setUserPwd(user, pwd)
if connect is None:
connect = (user and pwd) or tokenFile
if connect:
elif msEmail and msPwd:
self.conn.setMicrosoftAcc(msEmail, msPwd)
if connect and ((user and pwd) or (msEmail and msPwd) or tokenFile):
try:
self.conn.readToken()
except:
Expand Down

0 comments on commit c289b07

Please sign in to comment.