[Bug] Login does not work due to change in website
Describe the bug I try to login as described in the example
Your code
from edupage_api import Edupage
from edupage_api.exceptions import BadCredentialsException, CaptchaException
edupage = Edupage()
USER = "###"
PASSWORD = "###"
SCHOOL = "###"
try:
second_factor = edupage.login(USER, PASSWORD, SCHOOL)
if second_factor is not None:
user = input("Hit enter when 2FA is done...")
second_factor.finish()
except BadCredentialsException:
print("Wrong username or password!")
except CaptchaException:
print("Captcha required!")
Error message
Traceback (most recent call last):
File "/home/ber/Projekte/Edupage/main.py", line 10, in <module>
second_factor = edupage.login(USER, PASSWORD, SCHOOL)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ber/Projekte/Edupage/.venv/lib/python3.12/site-packages/edupage_api/__init__.py", line 69, in login
return Login(self).login(username, password, subdomain)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ber/Projekte/Edupage/.venv/lib/python3.12/site-packages/edupage_api/login.py", line 209, in login
csrf_token = data.split('csrfauth" value="')[1].split('"')[0]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^
IndexError: list index out of range```
Expected behavior Login to proceed
Version
- Edupage API version: 0.12.3
- Python version: 3.12.3
I assume the error is due to a change of edupage. In login.py:208 html is searched for csrfauth" value=" which looks to me like a form element. But in my version of Edupage the form is build dynamically. Important information are stored in javascript variable
var elem = $j('#jwc897fed5_3c167b31').get(0);
var props = {"username":"##MY USERNAME##","deviceNames":["##MY DEVICE##"],"email":"##MY EMAIL##","edupage":"##MY SCHOOL/SUBDOMAIN##","requestid":"## 40byte hex##","gu":null,"au":"","storedUsers":[]};
Current implementation is working for me, so it is hard for me to simulate your case.
Based on information provided, I did code changes. It would be great if you can test it. Please let me know if this worked for you.
file: login.py
import json
import re
from dataclasses import dataclass
from html import unescape
from html.parser import HTMLParser
from json import JSONDecodeError
from typing import Optional
from edupage_api.exceptions import (
BadCredentialsException,
CaptchaException,
MissingDataException,
RequestError,
SecondFactorFailedException,
)
from edupage_api.module import EdupageModule, Module
@dataclass
class TwoFactorLogin:
__authentication_endpoint: str
__authentication_token: str
__csrf_token: str
__edupage: EdupageModule
__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):
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(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)
class Login(Module):
class _InputValueParser(HTMLParser):
def __init__(self, key: str):
super().__init__()
self._key = key
self.value: Optional[str] = None
def handle_starttag(self, tag, attrs):
if tag.lower() != "input" or self.value is not None:
return
attr_map = {name.lower(): (value or "") for name, value in attrs}
if attr_map.get("name") == self._key or attr_map.get("id") == self._key:
found = attr_map.get("value")
if found:
self.value = found
@staticmethod
def _extract_input_value(html: str, key: str) -> str:
parser = Login._InputValueParser(key)
parser.feed(html)
if parser.value:
return parser.value
# Fallback for cases where the token is embedded in scripts or attributes
fallback_patterns = [
rf'{key}"?\s*value="([^"]+)"',
rf'name=["\']{key}["\'][^>]*value=["\']([^"\']+)["\']',
rf'id=["\']{key}["\'][^>]*value=["\']([^"\']+)["\']',
rf'{key}\s*[:=]\s*"([^"]+)"',
rf'{key}\s*[:=]\s*\'([^\']+)\'',
]
for pattern in fallback_patterns:
match = re.search(pattern, html, re.IGNORECASE)
if match and match.group(1):
return unescape(match.group(1))
# Some schools now embed login props inside inline JSON objects (e.g. `var props = {...}`)
object_pattern = re.compile(
r"var\s+\w+\s*=\s*(\{.*?\});", re.IGNORECASE | re.DOTALL
)
for candidate in object_pattern.findall(html):
try:
payload = json.loads(candidate)
except JSONDecodeError:
continue
value = payload.get(key)
if value is not None and value != "":
return str(value)
raise MissingDataException(f"Could not find `{key}` input in HTML response")
def __parse_login_data(self, data):
json_string = (
data.split("userhome(", 1)[1]
.rsplit(");", 2)[0]
.replace("\t", "")
.replace("\n", "")
.replace("\r", "")
)
self.edupage.data = json.loads(json_string)
self.edupage.is_logged_in = True
self.edupage.gsec_hash = data.split('ASC.gsechash="')[1].split('"')[0]
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.
"""
request_url = f"https://{subdomain}.edupage.org/login/?cmd=MainLogin"
response = self.edupage.session.get(request_url)
data = response.content.decode()
csrf_token: Optional[str] = None
try:
json_payload = json.loads(data)
csrf_token = json_payload.get("csrftoken")
except JSONDecodeError:
pass
if not csrf_token:
match = re.search(r'"csrftoken"\s*:\s*"([^"]+)"', data)
if match:
csrf_token = match.group(1)
if not csrf_token:
try:
csrf_token = self._extract_input_value(data, "csrfauth")
except MissingDataException:
csrf_token = None
if not csrf_token:
try:
csrf_token = self._extract_input_value(data, "requestid")
except MissingDataException as exc:
raise MissingDataException(
"Could not locate `csrftoken`, `csrfauth`, or `requestid` in login response"
) from exc
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()
data = response.content.decode()
if subdomain == "login1":
subdomain = data.split("-->")[0].split(" ")[-1]
self.edupage.subdomain = subdomain
self.edupage.username = username
if "twofactor" not in response.url:
# 2FA not needed
self.__parse_login_data(data)
return
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()
csrf_token = self._extract_input_value(data, "csrfauth")
authentication_token = self._extract_input_value(data, "au")
authentication_endpoint = self._extract_input_value(data, "gu")
return TwoFactorLogin(
authentication_endpoint, authentication_token, csrf_token, self.edupage
)
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}")
I think you are on the right track, but as I said there is no csrfauth in the page (https://gist.github.com/BerengarWLehr/03d82d6ddd2aa6a49ceab8262fe79f4a).
The page regularly sends updates via POST Content-Type
application/x-www-form-urlencoded; charset=UTF-8, payload
eqap=dz:BUCxCQAwDLrGG5y6hD4imQti7x8kXit6/4AD3gI=
eqacs=2a3fadb795d449f2694bf0048c33d096b23d5a47
eqaz=1
to https://SCHOOLNAME.edupage.org/login/twofactor?akcia=checkIfConfirmed&akcia=checkIfConfirmed&eqav=1&maxEqav=7
When I enter a code, it sends via POST Content-Type application/x-www-form-urlencoded; charset=UTF-8, the payload eqap=dz:Zck7CsAgDMbx03iApoNThz7mLj1BENNFq/go9PYqQhC6hHy/f/DKY0AbFyE3AZCAMGpVPzGvbeuY2oC9XqDTXfhqrt+QyAXLYeKQcscnG9Pl/gmOIo8C eqacs=b3c07e34302c2ce4dff3fe44ac6959185e5a75eb eqaz=1
I tried to decode/debase64 that, but it just binary data.
Update:
So I found edubarUtils.js:256 where at least the payload is made a little bit more obvious.
var obj = {
eqap: cs0,
eqacs: sha1(cs0),
eqaz: useEncryption ? '1' : '0',
}
I can confirm, that eqacs is just sha1
$: echo -n 'dz:Zck7CsAgDMbx03iApoNThz7mLj1BENNFq/go9PYqQhC6hHy/f/DKY0AbFyE3AZCAMGpVPzGvbeuY2oC9XqDTXfhqrt+QyAXLYeKQcscnG9Pl/gmOIo8C' | sha1sum
b3c07e34302c2ce4dff3fe44ac6959185e5a75eb -
so now I'm hunting for mysterious cs0
This explains where cs0 is coming from:
So its kind-a zip compressed version of `rpcparams={}
By the way, if you actually enter a code (I testes "1234") cs = urlencode(rpcparams={"t2fasec":"1234","2fNoSave":"y","2fform":"1","tu":null,"gu":null,"au":null})
{
"t2fasec":"1234",
"2fNoSave":"y",
"2fform":"1",
"tu":null,
"gu":null,
"au":null
}
Not even the request ID is required to build this payload.
It's a bit of a challenge to emulate bitewise equality to
var gz = new Zlib.RawDeflate(encoder.encode(cs))
var compressed = gz.compress()
from the zlib library at https://github.com/imaya/zlib.js/raw/master/bin/rawdeflate.min.js
But it's actually not necessary to have bitwise equality b/c the ZLib algorithm might produce different encoded results depending on internal parameters, but the python function is roundtrip equivalent to JavaScript (and probably EduPage's PHP function) so the following function should work
# Python equivalent generating payload for entering the code manually, no need to extract any parameters from the form
import json
from urllib.parse import urlencode
import zlib
import base64
import hashlib
def sha1(data: str) -> str:
sha1_hash = hashlib.sha1()
sha1_hash.update(data.encode('utf-8'))
return sha1_hash.hexdigest()
def buildPayload(code: str) -> dict:
data = { "rpcparams": json.dumps({
"t2fasec": code,
"2fNoSave": "y",
"2fform": "1",
"tu": None,
"gu": None,
"au": None
}, separators=(',', ':'))}
cmprs = zlib.compress(urlencode(data).encode('utf-8'), level=9, wbits=-15)
cs0 = "dz:" + base64.b64encode(cmprs).decode('ascii')
return {
"eqap": cs0,
"eqacs": sha1(cs0),
"eqaz": '1'
}
Thanks @BerengarWLehr for detailed analysis. Here is new version of code to test.
What was changed:
- Login now finds CSRF tokens whether Edupage renders hidden inputs or only exposes them in inline var props = {...} objects. _extract_input_value parses those JSON blobs, and the login flow falls back to requestid when csrfauth isn’t present.
- Two-factor completion no longer depends on scraping csrfauth/gu/au. If those fields exist we keep the old flow; otherwise we build the encrypted payload (eqap/eqacs/eqaz) exactly as the browser does (raw-deflate of rpcparams, base64 with dz: prefix, SHA‑1 hash) and post it to the twofactor endpoint, then reload the session cookie. That allows manual code entry on the new login pages.
What is still open:
- The 2FA polling/notification endpoints (is_confirmed, resend_notifications) currently POST without the new payload. If the your school requires the encrypted params for those calls too, we’ll need to extend _build_eq_payload usage there. Please let me know if this is the case
What to test:
- I have retested the code and it still works for my school, should be back compatible
- If polling still fails or requires eqap, please share your response details
import base64
import hashlib
import json
import re
import zlib
from dataclasses import dataclass
from html import unescape
from html.parser import HTMLParser
from json import JSONDecodeError
from typing import Optional
from urllib.parse import urlencode
from edupage_api.exceptions import (
BadCredentialsException,
CaptchaException,
MissingDataException,
RequestError,
SecondFactorFailedException,
)
from edupage_api.module import EdupageModule, Module
def _build_eq_payload(params: dict) -> dict:
rpc_payload = json.dumps(params, separators=(",", ":"))
encoded = urlencode({"rpcparams": rpc_payload})
compressor = zlib.compressobj(level=9, wbits=-15)
compressed = compressor.compress(encoded.encode("utf-8")) + compressor.flush()
cs0 = "dz:" + base64.b64encode(compressed).decode("ascii")
sha1_hash = hashlib.sha1(cs0.encode("utf-8")).hexdigest()
return {"eqap": cs0, "eqacs": sha1_hash, "eqaz": "1"}
@dataclass
class TwoFactorLogin:
__authentication_endpoint: Optional[str]
__authentication_token: Optional[str]
__csrf_token: Optional[str]
__edupage: EdupageModule
__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.__csrf_token
and self.__authentication_endpoint
and self.__authentication_token
):
self.__finish_with_legacy_parameters(code)
else:
self.__finish_with_eq_payload(code)
def __finish_with_legacy_parameters(self, code: str):
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:
self.__reload_after_twofactor()
return
raise SecondFactorFailedException(
f"Second factor failed! (wrong/expired code? expired session?)"
)
def __finish_with_eq_payload(self, code: str):
request_url = (
f"https://{self.__edupage.subdomain}.edupage.org/login/twofactor?akcia=checkIfConfirmed"
)
rpc_params = {
"t2fasec": code,
"2fNoSave": "y",
"2fform": "1",
"tu": None,
"gu": self.__authentication_endpoint,
"au": self.__authentication_token,
}
payload = _build_eq_payload(rpc_params)
response = self.__edupage.session.post(request_url, payload)
try:
data = response.json()
except ValueError as exc:
raise SecondFactorFailedException(
f"Second factor failed! Invalid response: {response.text}"
) from exc
if data.get("status") != "ok":
raise SecondFactorFailedException(
f"Second factor failed: {json.dumps(data)}"
)
self.__reload_after_twofactor()
def __reload_after_twofactor(self):
cookies = self.__edupage.session.cookies.get_dict(
f"{self.__edupage.subdomain}.edupage.org"
)
session_id = cookies.get("PHPSESSID")
if not session_id:
raise SecondFactorFailedException("Second factor failed: missing PHPSESSID")
Login(self.__edupage).reload_data(
self.__edupage.subdomain, session_id, self.__edupage.username
)
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)
class Login(Module):
class _InputValueParser(HTMLParser):
def __init__(self, key: str):
super().__init__()
self._key = key
self.value: Optional[str] = None
def handle_starttag(self, tag, attrs):
if tag.lower() != "input" or self.value is not None:
return
attr_map = {name.lower(): (value or "") for name, value in attrs}
if attr_map.get("name") == self._key or attr_map.get("id") == self._key:
found = attr_map.get("value")
if found:
self.value = found
@staticmethod
def _extract_input_value(html: str, key: str) -> str:
parser = Login._InputValueParser(key)
parser.feed(html)
if parser.value:
return parser.value
# Fallback for cases where the token is embedded in scripts or attributes
fallback_patterns = [
rf'{key}"?\s*value="([^"]+)"',
rf'name=["\']{key}["\'][^>]*value=["\']([^"\']+)["\']',
rf'id=["\']{key}["\'][^>]*value=["\']([^"\']+)["\']',
rf'{key}\s*[:=]\s*"([^"]+)"',
rf'{key}\s*[:=]\s*\'([^\']+)\'',
]
for pattern in fallback_patterns:
match = re.search(pattern, html, re.IGNORECASE)
if match and match.group(1):
return unescape(match.group(1))
# Some schools now embed login props inside inline JSON objects (e.g. `var props = {...}`)
object_pattern = re.compile(
r"var\s+\w+\s*=\s*(\{.*?\});", re.IGNORECASE | re.DOTALL
)
for candidate in object_pattern.findall(html):
try:
payload = json.loads(candidate)
except JSONDecodeError:
continue
value = payload.get(key)
if value is not None:
return str(value)
raise MissingDataException(f"Could not find `{key}` input in HTML response")
def __parse_login_data(self, data):
json_string = (
data.split("userhome(", 1)[1]
.rsplit(");", 2)[0]
.replace("\t", "")
.replace("\n", "")
.replace("\r", "")
)
self.edupage.data = json.loads(json_string)
self.edupage.is_logged_in = True
self.edupage.gsec_hash = data.split('ASC.gsechash="')[1].split('"')[0]
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.
"""
request_url = f"https://{subdomain}.edupage.org/login/?cmd=MainLogin"
response = self.edupage.session.get(request_url)
data = response.content.decode()
csrf_token: Optional[str] = None
try:
json_payload = json.loads(data)
csrf_token = json_payload.get("csrftoken")
except JSONDecodeError:
pass
if not csrf_token:
match = re.search(r'"csrftoken"\s*:\s*"([^"]+)"', data)
if match:
csrf_token = match.group(1)
if not csrf_token:
try:
csrf_token = self._extract_input_value(data, "csrfauth")
except MissingDataException:
csrf_token = None
if not csrf_token:
try:
csrf_token = self._extract_input_value(data, "requestid")
except MissingDataException as exc:
raise MissingDataException(
"Could not locate `csrftoken`, `csrfauth`, or `requestid` in login response"
) from exc
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()
data = response.content.decode()
if subdomain == "login1":
subdomain = data.split("-->")[0].split(" ")[-1]
self.edupage.subdomain = subdomain
self.edupage.username = username
if "twofactor" not in response.url:
# 2FA not needed
self.__parse_login_data(data)
return
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()
def _optional_input(key: str) -> Optional[str]:
try:
return self._extract_input_value(data, key)
except MissingDataException:
return None
csrf_token = _optional_input("csrfauth")
authentication_token = _optional_input("au")
authentication_endpoint = _optional_input("gu")
return TwoFactorLogin(
authentication_endpoint, authentication_token, csrf_token, self.edupage
)
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}")
A few little changes and still only with entering the code. Just accepting 2FA in the App does currently not work:
The correct URL for sending 2FA code L110-112:
request_url = (
f"https://{self.__edupage.subdomain}.edupage.org/login/?cmd=MainLogin&akcia=login"
)
A prefix control added in L124:
if response.text[:4] != "eqz:":
raise SecondFactorFailedException(
f"Second factor failed! Invalid prefix: {response.text}"
)
Data must be base64 decoded before parsing json:
data = json.loads(base64.b64decode(response.text[4:]).decode("utf8"))
For some reason data.status is now "OK" and not "ok" and if you leave the code it will still send status: "OK" but also need2fa: 1:
if data.get("status").lower() != "ok" or data.get("need2fa") is not None:
So this is my adjusted function
def __finish_with_eq_payload(self, code: str):
request_url = (
f"https://{self.__edupage.subdomain}.edupage.org/login/?cmd=MainLogin&akcia=login"
)
rpc_params = {
"t2fasec": code,
"2fNoSave": "y",
"2fform": "1",
"tu": None,
"gu": self.__authentication_endpoint,
"au": self.__authentication_token,
}
payload = _build_eq_payload(rpc_params)
response = self.__edupage.session.post(request_url, payload)
if response.text[:4] != "eqz:":
raise SecondFactorFailedException(
f"Second factor failed! Invalid prefix: {response.text}"
)
try:
data = json.loads(base64.b64decode(response.text[4:]).decode("utf8"))
print(data)
except ValueError as exc:
raise SecondFactorFailedException(
f"Second factor failed! Invalid response: {response.text}"
) from exc
if data.get("status").lower() != "ok" or data.get("need2fa") is not None:
raise SecondFactorFailedException(
f"Second factor failed: {json.dumps(data)}"
)
Will report back when logging in via App alone works
Update: I just checked and logging in without code doesn't work in the official flow for me neither.