2020-04-20 16:25:55 +02:00
|
|
|
"""
|
|
|
|
Authenticators that can obtain proper ILIAS session cookies.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import abc
|
|
|
|
import logging
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
import bs4
|
|
|
|
import requests
|
|
|
|
|
2020-05-08 21:51:33 +02:00
|
|
|
from ..authenticators import TfaAuthenticator, UserPassAuthenticator
|
2020-04-20 18:38:18 +02:00
|
|
|
from ..utils import soupify
|
2020-04-20 16:25:55 +02:00
|
|
|
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class IliasAuthenticator(abc.ABC):
|
2020-04-20 19:27:26 +02:00
|
|
|
# pylint: disable=too-few-public-methods
|
|
|
|
|
2020-04-20 16:25:55 +02:00
|
|
|
"""
|
|
|
|
An authenticator that logs an existing requests session into an ILIAS
|
|
|
|
account.
|
|
|
|
"""
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
def authenticate(self, sess: requests.Session) -> None:
|
|
|
|
"""
|
|
|
|
Log a requests session into this authenticator's ILIAS account.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
class KitShibbolethAuthenticator(IliasAuthenticator):
|
2020-04-20 19:27:26 +02:00
|
|
|
# pylint: disable=too-few-public-methods
|
|
|
|
|
2020-04-20 16:25:55 +02:00
|
|
|
"""
|
|
|
|
Authenticate via KIT's shibboleth system.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, username: Optional[str] = None, password: Optional[str] = None) -> None:
|
|
|
|
self._auth = UserPassAuthenticator("KIT ILIAS Shibboleth", username, password)
|
2020-05-08 21:51:33 +02:00
|
|
|
self._tfa_auth = TfaAuthenticator("KIT ILIAS Shibboleth")
|
2020-04-20 16:25:55 +02:00
|
|
|
|
|
|
|
def authenticate(self, sess: requests.Session) -> None:
|
|
|
|
"""
|
|
|
|
Performs the ILIAS Shibboleth authentication dance and saves the login
|
|
|
|
cookies it receieves.
|
|
|
|
|
|
|
|
This function should only be called whenever it is detected that you're
|
|
|
|
not logged in. The cookies obtained should be good for a few minutes,
|
|
|
|
maybe even an hour or two.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Equivalent: Click on "Mit KIT-Account anmelden" button in
|
|
|
|
# https://ilias.studium.kit.edu/login.php
|
|
|
|
LOGGER.debug("Begin authentication process with ILIAS")
|
|
|
|
url = "https://ilias.studium.kit.edu/Shibboleth.sso/Login"
|
|
|
|
data = {
|
|
|
|
"sendLogin": "1",
|
|
|
|
"idp_selection": "https://idp.scc.kit.edu/idp/shibboleth",
|
|
|
|
"target": "/shib_login.php",
|
|
|
|
"home_organization_selection": "Mit KIT-Account anmelden",
|
|
|
|
}
|
2020-04-20 18:38:18 +02:00
|
|
|
soup = soupify(sess.post(url, data=data))
|
2020-04-20 16:25:55 +02:00
|
|
|
|
|
|
|
# Attempt to login using credentials, if necessary
|
|
|
|
while not self._login_successful(soup):
|
|
|
|
# Searching the form here so that this fails before asking for
|
|
|
|
# credentials rather than after asking.
|
2020-07-28 21:13:51 +02:00
|
|
|
form = soup.find("form", {"class": "full content", "method": "post"})
|
2020-04-20 16:25:55 +02:00
|
|
|
action = form["action"]
|
|
|
|
|
|
|
|
# Equivalent: Enter credentials in
|
|
|
|
# https://idp.scc.kit.edu/idp/profile/SAML2/Redirect/SSO
|
|
|
|
LOGGER.debug("Attempt to log in to Shibboleth using credentials")
|
|
|
|
url = "https://idp.scc.kit.edu" + action
|
|
|
|
data = {
|
|
|
|
"_eventId_proceed": "",
|
|
|
|
"j_username": self._auth.username,
|
|
|
|
"j_password": self._auth.password,
|
|
|
|
}
|
2020-04-20 18:38:18 +02:00
|
|
|
soup = soupify(sess.post(url, data=data))
|
2020-04-20 16:25:55 +02:00
|
|
|
|
2020-05-08 21:51:33 +02:00
|
|
|
if self._tfa_required(soup):
|
|
|
|
soup = self._authenticate_tfa(sess, soup)
|
|
|
|
|
2020-04-20 16:25:55 +02:00
|
|
|
if not self._login_successful(soup):
|
|
|
|
print("Incorrect credentials.")
|
|
|
|
self._auth.invalidate_credentials()
|
|
|
|
|
|
|
|
# Equivalent: Being redirected via JS automatically
|
|
|
|
# (or clicking "Continue" if you have JS disabled)
|
|
|
|
LOGGER.debug("Redirect back to ILIAS with login information")
|
|
|
|
relay_state = soup.find("input", {"name": "RelayState"})
|
|
|
|
saml_response = soup.find("input", {"name": "SAMLResponse"})
|
|
|
|
url = "https://ilias.studium.kit.edu/Shibboleth.sso/SAML2/POST"
|
2020-04-21 13:30:42 +02:00
|
|
|
data = { # using the info obtained in the while loop above
|
2020-04-20 16:25:55 +02:00
|
|
|
"RelayState": relay_state["value"],
|
|
|
|
"SAMLResponse": saml_response["value"],
|
|
|
|
}
|
|
|
|
sess.post(url, data=data)
|
|
|
|
|
2020-05-08 21:51:33 +02:00
|
|
|
def _authenticate_tfa(
|
|
|
|
self,
|
|
|
|
session: requests.Session,
|
|
|
|
soup: bs4.BeautifulSoup
|
|
|
|
) -> bs4.BeautifulSoup:
|
|
|
|
# Searching the form here so that this fails before asking for
|
|
|
|
# credentials rather than after asking.
|
|
|
|
form = soup.find("form", {"method": "post"})
|
|
|
|
action = form["action"]
|
|
|
|
|
|
|
|
# Equivalent: Enter token in
|
|
|
|
# https://idp.scc.kit.edu/idp/profile/SAML2/Redirect/SSO
|
|
|
|
LOGGER.debug("Attempt to log in to Shibboleth with TFA token")
|
|
|
|
url = "https://idp.scc.kit.edu" + action
|
|
|
|
data = {
|
|
|
|
"_eventId_proceed": "",
|
|
|
|
"j_tokenNumber": self._tfa_auth.get_token()
|
|
|
|
}
|
|
|
|
return soupify(session.post(url, data=data))
|
|
|
|
|
2020-04-20 16:25:55 +02:00
|
|
|
@staticmethod
|
|
|
|
def _login_successful(soup: bs4.BeautifulSoup) -> bool:
|
|
|
|
relay_state = soup.find("input", {"name": "RelayState"})
|
|
|
|
saml_response = soup.find("input", {"name": "SAMLResponse"})
|
|
|
|
return relay_state is not None and saml_response is not None
|
2020-05-08 21:51:33 +02:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _tfa_required(soup: bs4.BeautifulSoup) -> bool:
|
|
|
|
return soup.find(id="j_tokenNumber") is not None
|