Module edupage_api.login

Classes

class Login (edupage: EdupageModule)
Expand source code
class Login(Module):
    def __parse_login_data(self, data):
        try:
            json_string = (
                data.split("userhome(", 1)[1]
                .rsplit(");", 2)[0]
                .replace("\t", "")
                .replace("\n", "")
                .replace("\r", "")
            )
        except IndexError:
            raise BadCredentialsException("EduPage did not return login data")

        self.edupage.data = json.loads(json_string)
        self.edupage.is_logged_in = True

        try:
            self.edupage.gsec_hash = data.split('ASC.gsechash="', 1)[1].split('"', 1)[0]
        except IndexError:
            self.edupage.gsec_hash = None

    @staticmethod
    def _parse_rpc_response(text: str) -> Optional[dict]:
        if not text:
            return None

        try:
            decoded = RequestData.decode_response(text)
            return json.loads(decoded)
        except (Base64DecodeError, TypeError, JSONDecodeError):
            return None

    @staticmethod
    def _extract_two_factor_fields(data: str) -> Optional[dict]:
        try:
            return {
                "csrfauth": data.split('csrfauth" value="', 1)[1].split('"', 1)[0],
                "au": data.split('au" value="', 1)[1].split('"', 1)[0],
                "gu": data.split('gu" value="', 1)[1].split('"', 1)[0],
            }
        except IndexError:
            return None

    def __finish_login(self, response, subdomain: str, username: str):
        data = response.content.decode()

        if subdomain == "login1":
            subdomain = urlparse(response.url).hostname.split(".")[0]

        self.edupage.subdomain = subdomain
        self.edupage.username = username

        if "twofactor" not in response.url:
            self.__parse_login_data(data)
            return None

        request_url = (
            f"https://{self.edupage.subdomain}.edupage.org/login/twofactor?sn=1"
        )

        two_factor_response = self.edupage.session.get(request_url)

        data = two_factor_response.content.decode()

        fields = self._extract_two_factor_fields(data)
        if fields:
            return TwoFactorLogin(
                fields["gu"], fields["au"], fields["csrfauth"], self.edupage
            )

        if "/login/pics/jsw/twofactorlogin.js" in data:
            return TwoFactorLogin(None, None, None, self.edupage, True)

        raise BadCredentialsException("EduPage did not provide two-factor fields")

    def __login_with_rpc(self, username: str, password: str, subdomain: str):
        # Mirrors the login process (mainlogin.js):
        # 1. akcia=getToken
        # 2. akcia=login
        base_url = f"https://{subdomain}.edupage.org"
        headers = {"Content-Type": "application/x-www-form-urlencoded"}

        response = self.edupage.session.get(f"{base_url}/login/?cmd=MainLogin")
        if response.status_code != 200:
            return None

        token_response_raw = self.edupage.session.post(
            f"{base_url}/login/?cmd=MainLogin&akcia=getToken",
            data=RequestData.encode_request_body(
                {"rpcparams": json.dumps({"username": username, "edupage": ""})}
            ),
            headers=headers,
        )
        if token_response_raw.status_code != 200:
            return None
        token_response = self._parse_rpc_response(token_response_raw.text)

        token = token_response.get("token") if token_response else None
        if not token:
            return None

        login_response_raw = self.edupage.session.post(
            f"{base_url}/login/?cmd=MainLogin&akcia=login",
            data=RequestData.encode_request_body(
                {
                    "rpcparams": json.dumps(
                        {
                            "username": username,
                            "password": password,
                            "userToken": token,
                            "edupage": "",
                            "ctxt": "",
                            "tu": None,
                            "gu": None,
                            "au": None,
                        }
                    )
                }
            ),
            headers=headers,
        )

        if login_response_raw.status_code != 200:
            return None

        login_response = self._parse_rpc_response(login_response_raw.text)
        if not login_response:
            return None

        error_id = (login_response.get("err") or {}).get("error_id")
        redirect_url = login_response.get("redirectUrl")

        # `invalid_token` means our csrf token was rejected
        if error_id == "invalid_token" or not redirect_url:
            return None

        return self.edupage.session.get(urljoin(base_url, redirect_url))

    def login(
        self, username: str, password: str, subdomain: str = "login1"
    ) -> Optional[TwoFactorLogin]:
        """Login to your school's Edupage account (optionally with 2 factor authentication).

        If you do not have 2 factor authentication set up, this function will return `None`.
        The login will still work and succeed.

        See the `Edupage.TwoFactorLogin` documentation or the examples for more details
        of the 2 factor authentication process.

        Args:
            username (str): Your username.
            password (str): Your password.
            subdomain (str): Subdomain of your school (https://{subdomain}.edupage.org).

        Returns:
            Optional[TwoFactorLogin]: The object that can be used to complete the second factor
                (or `None` — if the second factor is not set up)

        Raises:
            BadCredentialsException: Your credentials are invalid.
            CaptchaException: The login process failed because of a captcha.
            SecondFactorFailed: The second factor login timed out
                or there was another problem with the second factor.
        """

        response = self.__login_with_rpc(username, password, subdomain)

        if response is None:
            request_url = f"https://{subdomain}.edupage.org/login/?cmd=MainLogin"

            get_response = self.edupage.session.get(request_url)
            data = get_response.content.decode()

            try:
                csrf_token = data.split('"csrftoken":"', 1)[1].split('"', 1)[0]
            except IndexError:
                raise BadCredentialsException("EduPage did not provide a login token")

            parameters = {
                "csrfauth": csrf_token,
                "username": username,
                "password": password,
            }

            request_url = f"https://{subdomain}.edupage.org/login/edubarLogin.php"

            response = self.edupage.session.post(request_url, parameters)

            if "cap=1" in response.url or "lerr=b43b43" in response.url:
                raise CaptchaException()

            if "bad=1" in response.url:
                raise BadCredentialsException()

        return self.__finish_login(response, subdomain, username)

    def reload_data(self, subdomain: str, session_id: str, username: str):
        request_url = f"https://{subdomain}.edupage.org/user"

        self.edupage.session.cookies.set("PHPSESSID", session_id)

        response = self.edupage.session.get(request_url)

        try:
            self.__parse_login_data(response.content.decode())
            self.edupage.subdomain = subdomain
            self.edupage.username = username
        except (TypeError, JSONDecodeError) as e:
            raise BadCredentialsException(f"Invalid session id: {e}")

Ancestors

Methods

def login(self, username: str, password: str, subdomain: str = 'login1') ‑> TwoFactorLogin | None
Expand source code
def login(
    self, username: str, password: str, subdomain: str = "login1"
) -> Optional[TwoFactorLogin]:
    """Login to your school's Edupage account (optionally with 2 factor authentication).

    If you do not have 2 factor authentication set up, this function will return `None`.
    The login will still work and succeed.

    See the `Edupage.TwoFactorLogin` documentation or the examples for more details
    of the 2 factor authentication process.

    Args:
        username (str): Your username.
        password (str): Your password.
        subdomain (str): Subdomain of your school (https://{subdomain}.edupage.org).

    Returns:
        Optional[TwoFactorLogin]: The object that can be used to complete the second factor
            (or `None` — if the second factor is not set up)

    Raises:
        BadCredentialsException: Your credentials are invalid.
        CaptchaException: The login process failed because of a captcha.
        SecondFactorFailed: The second factor login timed out
            or there was another problem with the second factor.
    """

    response = self.__login_with_rpc(username, password, subdomain)

    if response is None:
        request_url = f"https://{subdomain}.edupage.org/login/?cmd=MainLogin"

        get_response = self.edupage.session.get(request_url)
        data = get_response.content.decode()

        try:
            csrf_token = data.split('"csrftoken":"', 1)[1].split('"', 1)[0]
        except IndexError:
            raise BadCredentialsException("EduPage did not provide a login token")

        parameters = {
            "csrfauth": csrf_token,
            "username": username,
            "password": password,
        }

        request_url = f"https://{subdomain}.edupage.org/login/edubarLogin.php"

        response = self.edupage.session.post(request_url, parameters)

        if "cap=1" in response.url or "lerr=b43b43" in response.url:
            raise CaptchaException()

        if "bad=1" in response.url:
            raise BadCredentialsException()

    return self.__finish_login(response, subdomain, username)

Login to your school's Edupage account (optionally with 2 factor authentication).

If you do not have 2 factor authentication set up, this function will return None. The login will still work and succeed.

See the Edupage.TwoFactorLogin documentation or the examples for more details of the 2 factor authentication process.

Args

username : str
Your username.
password : str
Your password.
subdomain : str
Subdomain of your school (https://{subdomain}.edupage.org).

Returns

Optional[TwoFactorLogin]
The object that can be used to complete the second factor (or None — if the second factor is not set up)

Raises

BadCredentialsException
Your credentials are invalid.
CaptchaException
The login process failed because of a captcha.
SecondFactorFailed
The second factor login timed out or there was another problem with the second factor.
def reload_data(self, subdomain: str, session_id: str, username: str)
Expand source code
def reload_data(self, subdomain: str, session_id: str, username: str):
    request_url = f"https://{subdomain}.edupage.org/user"

    self.edupage.session.cookies.set("PHPSESSID", session_id)

    response = self.edupage.session.get(request_url)

    try:
        self.__parse_login_data(response.content.decode())
        self.edupage.subdomain = subdomain
        self.edupage.username = username
    except (TypeError, JSONDecodeError) as e:
        raise BadCredentialsException(f"Invalid session id: {e}")
class TwoFactorLogin (_TwoFactorLogin__authentication_endpoint: str | None,
_TwoFactorLogin__authentication_token: str | None,
_TwoFactorLogin__csrf_token: str | None,
_TwoFactorLogin__edupage: EdupageModule)
Expand source code
@dataclass
class TwoFactorLogin:
    __authentication_endpoint: Optional[str]
    __authentication_token: Optional[str]
    __csrf_token: Optional[str]
    __edupage: EdupageModule
    __use_modern_rpc: bool = False

    __code: Optional[str] = None

    def is_confirmed(self):
        """Check if the second factor process was finished by confirmation with a device.

        If this function returns true, you can safely use `TwoFactorLogin.finish` to finish the second factor authentication process.

        Returns:
            bool: True if the second factor was confirmed with a device.
        """

        request_url = f"https://{self.__edupage.subdomain}.edupage.org/login/twofactor?akcia=checkIfConfirmed"
        response = self.__edupage.session.post(request_url)

        data = response.json()
        if data.get("status") == "fail":
            return False
        elif data.get("status") != "ok":
            raise MissingDataException(
                f"Invalid response from edupage's server!: {str(data)}"
            )

        self.__code = data["data"]

        return True

    def resend_notifications(self):
        """Resends the confirmation notification to all devices."""

        request_url = f"https://{self.__edupage.subdomain}.edupage.org/login/twofactor?akcia=resendNotifs"
        response = self.__edupage.session.post(request_url)

        data = response.json()
        if data.get("status") != "ok":
            raise RequestError(f"Failed to resend notifications: {str(data)}")

    def __finish(self, code: str):
        if self.__use_modern_rpc:
            self.__finish_with_modern_rpc(code)
            return

        request_url = (
            f"https://{self.__edupage.subdomain}.edupage.org/login/edubarLogin.php"
        )
        parameters = {
            "csrfauth": self.__csrf_token,
            "t2fasec": code,
            "2fNoSave": "y",
            "2fform": "1",
            "gu": self.__authentication_endpoint,
            "au": self.__authentication_token,
        }

        response = self.__edupage.session.post(request_url, parameters)

        if "window.location = gu;" in response.text:
            cookies = self.__edupage.session.cookies.get_dict(
                f"{self.__edupage.subdomain}.edupage.org"
            )

            Login(self.__edupage).reload_data(
                self.__edupage.subdomain, cookies["PHPSESSID"], self.__edupage.username
            )

            return

        raise SecondFactorFailedException(
            f"Second factor failed! (wrong/expired code? expired session?)"
        )

    def __finish_with_modern_rpc(self, code: str):
        """Complete the JavaScript-based 2FA flow used by current EduPage pages."""

        base_url = f"https://{self.__edupage.subdomain}.edupage.org"
        response = self.__edupage.session.post(
            f"{base_url}/login/?cmd=MainLogin&akcia=login",
            data=RequestData.encode_request_body(
                {
                    "rpcparams": json.dumps(
                        {
                            "t2fasec": code,
                            "2fNoSave": "y",
                            "2fform": "1",
                            "tu": None,
                            "gu": None,
                            "au": None,
                        }
                    )
                }
            ),
            headers={"Content-Type": "application/x-www-form-urlencoded"},
        )
        result = Login._parse_rpc_response(response.text)

        if not result or result.get("status") != "OK":
            raise SecondFactorFailedException(
                "Second factor failed! (wrong/expired code? expired session?)"
            )

        user_page = self.__edupage.session.get(
            urljoin(base_url, result.get("redirectUrl", "/user/"))
        )
        Login(self.__edupage)._Login__parse_login_data(user_page.content.decode())

    def finish(self):
        """Finish the second factor authentication process.
        This function should be used when using a device to confirm the login. If you are using email 2fa codes, please use `TwoFactorLogin.finish_with_code`.

        Notes:
            - This function can only be used after `TwoFactorLogin.is_confirmed` returned `True`.
            - This function can raise `SecondFactorFailedException` if there is a big delay from calling `TwoFactorLogin.is_confirmed` (and getting `True` as a result) to calling `TwoFactorLogin.finish`.

        Raises:
            BadCredentialsException: You didn't call and get the `True` result from `TwoFactorLogin.is_confirmed` before calling this function.
            SecondFactorFailedException: The delay between calling `TwoFactorLogin.is_confirmed` and `TwoFactorLogin.finish` was too long, or there was another error with the second factor authentication confirmation process.
        """

        if self.__code is None:
            raise BadCredentialsException(
                "Not confirmed! (you can only call finish after `TwoFactorLogin.is_confirmed` has returned True)"
            )

        self.__finish(self.__code)

    def finish_with_code(self, code: str):
        """Finish the second factor authentication process.
        This function should be used when email 2fa codes are used to confirm the login. If you are using a device to confirm the login, please use `TwoFactorLogin.finish`.

        Args:
            code (str): The 2fa code from your email or from the mobile app.

        Raises:
            SecondFactorFailedException: An invalid 2fa code was provided.
        """
        self.__finish(code)

TwoFactorLogin(_TwoFactorLogin__authentication_endpoint: Optional[str], _TwoFactorLogin__authentication_token: Optional[str], _TwoFactorLogin__csrf_token: Optional[str], _TwoFactorLogin__edupage: edupage_api.module.EdupageModule, _TwoFactorLogin__use_modern_rpc: bool = False, _TwoFactorLogin__code: Optional[str] = None)

Methods

def finish(self)
Expand source code
def finish(self):
    """Finish the second factor authentication process.
    This function should be used when using a device to confirm the login. If you are using email 2fa codes, please use `TwoFactorLogin.finish_with_code`.

    Notes:
        - This function can only be used after `TwoFactorLogin.is_confirmed` returned `True`.
        - This function can raise `SecondFactorFailedException` if there is a big delay from calling `TwoFactorLogin.is_confirmed` (and getting `True` as a result) to calling `TwoFactorLogin.finish`.

    Raises:
        BadCredentialsException: You didn't call and get the `True` result from `TwoFactorLogin.is_confirmed` before calling this function.
        SecondFactorFailedException: The delay between calling `TwoFactorLogin.is_confirmed` and `TwoFactorLogin.finish` was too long, or there was another error with the second factor authentication confirmation process.
    """

    if self.__code is None:
        raise BadCredentialsException(
            "Not confirmed! (you can only call finish after `TwoFactorLogin.is_confirmed` has returned True)"
        )

    self.__finish(self.__code)

Finish the second factor authentication process. This function should be used when using a device to confirm the login. If you are using email 2fa codes, please use TwoFactorLogin.finish_with_code().

Notes

Raises

BadCredentialsException
You didn't call and get the True result from TwoFactorLogin.is_confirmed() before calling this function.
SecondFactorFailedException
The delay between calling TwoFactorLogin.is_confirmed() and TwoFactorLogin.finish() was too long, or there was another error with the second factor authentication confirmation process.
def finish_with_code(self, code: str)
Expand source code
def finish_with_code(self, code: str):
    """Finish the second factor authentication process.
    This function should be used when email 2fa codes are used to confirm the login. If you are using a device to confirm the login, please use `TwoFactorLogin.finish`.

    Args:
        code (str): The 2fa code from your email or from the mobile app.

    Raises:
        SecondFactorFailedException: An invalid 2fa code was provided.
    """
    self.__finish(code)

Finish the second factor authentication process. This function should be used when email 2fa codes are used to confirm the login. If you are using a device to confirm the login, please use TwoFactorLogin.finish().

Args

code : str
The 2fa code from your email or from the mobile app.

Raises

SecondFactorFailedException
An invalid 2fa code was provided.
def is_confirmed(self)
Expand source code
def is_confirmed(self):
    """Check if the second factor process was finished by confirmation with a device.

    If this function returns true, you can safely use `TwoFactorLogin.finish` to finish the second factor authentication process.

    Returns:
        bool: True if the second factor was confirmed with a device.
    """

    request_url = f"https://{self.__edupage.subdomain}.edupage.org/login/twofactor?akcia=checkIfConfirmed"
    response = self.__edupage.session.post(request_url)

    data = response.json()
    if data.get("status") == "fail":
        return False
    elif data.get("status") != "ok":
        raise MissingDataException(
            f"Invalid response from edupage's server!: {str(data)}"
        )

    self.__code = data["data"]

    return True

Check if the second factor process was finished by confirmation with a device.

If this function returns true, you can safely use TwoFactorLogin.finish() to finish the second factor authentication process.

Returns

bool
True if the second factor was confirmed with a device.
def resend_notifications(self)
Expand source code
def resend_notifications(self):
    """Resends the confirmation notification to all devices."""

    request_url = f"https://{self.__edupage.subdomain}.edupage.org/login/twofactor?akcia=resendNotifs"
    response = self.__edupage.session.post(request_url)

    data = response.json()
    if data.get("status") != "ok":
        raise RequestError(f"Failed to resend notifications: {str(data)}")

Resends the confirmation notification to all devices.