Authentication¶
History: the dead password grant¶
Older clients (szkolny-android, lomber1/py-librus-api) log in with an OAuth
password grant:
POST https://api.librus.pl/OAuth/Token
grant_type=password&client_id=28&username=...&password=...
✅ Dead. Since around 2026-03-28 Librus answers this with HTTP 400
unsupported_grant_type, even with correct credentials. Do not use it.
Current flow (4 steps)¶
The current flow behaves like a browser logging in to the parent portal.
Everything is carried in cookies, so every request must use the same cookie
jar. Implemented in LibrusApiClient.async_login, and based on
emsi/librus_pyapi (MIT).
- Portal redirect.
GET https://synergia.librus.pl/loguj/portalRodzina(do not follow redirects). TheLocationheader points atapi.librus.pl/OAuth/Authorization?client_id=46&response_type=code&scope=mydata&state=.... - Authorization page.
GETthatLocationURL (do not follow redirects). This sets API-side session cookies. The body is not used. - Credentials.
POST https://api.librus.pl/OAuth/Authorization?client_id=46with the form bodyaction=login&login=<login>&pass=<password>and browser-like XHR headers:Origin/Refereronapi.librus.pl, theSec-Fetch-*headers,X-Requested-With: XMLHttpRequestand a desktop ChromeUser-Agent.
✅ On success the response is JSON:
{"status": "ok", "goTo": "/OAuth/Authorization/2FA?client_id=46"}
❓ A response without goTo is treated as wrong credentials. This was
never tested with a real wrong password, to avoid tripping abuse
detection on a real family's account.
4. Redirect chain.
Starting from goTo (resolved against the authorization URL), follow
Location headers manually, keeping cookies, until a response has no
Location (at most 10 hops). After that, an oauth_token cookie must
exist for synergia.librus.pl.
If any response body in steps 3–4 contains a captcha marker (captcha,
recaptcha, g-recaptcha, hcaptcha), the client raises
LibrusCaptchaRequiredError. ❓ This was never observed.
Cookies and session lifetime¶
| Cookie | Domain | Lifetime | Why it matters |
|---|---|---|---|
oauth_token |
synergia.librus.pl | ✅ Max-Age=600 per response, session usable ~24 h |
The actual session. ❓ The 10-minute Max-Age suggests it's re-issued on every request (sliding expiry). |
DZIENNIKSID, SDZIENNIKSID |
both | session | Session companions. |
DeviceCookie |
api.librus.pl, ✅ Path=/OAuth |
✅ 1 year (Max-Age=31536000) |
Marks a known device. ❓ Probably why normal logins get no captcha. Persist it, with its path. |
⚠️ Because DeviceCookie lives under /OAuth, asking a cookie jar for
"cookies for https://api.librus.pl/" does not return it. Walk the jar
(or filter for an /OAuth/... URL) when you persist the session.
- There is no OAuth refresh token to keep instead of the password. A
live session can be extended (next point), but once
oauth_tokenhas expired you have to log in again with the password, so unattended long-running use needs the password stored. - ✅
GET https://synergia.librus.pl/refreshToken(with the session cookies) answers 200 with an empty body and a newoauth_tokencookie; the API keeps working afterwards (confirmed live 2026-10-07). Calling it before the session dies should extend it without a password login. ❓ How long the extended session lasts was not measured.Mealso reportsRefresh: 900.async_ensure_session_validcalls it (async_refresh_session) once the session is 2 hours old and falls back to a password login if it fails. A refresh only counts when that very answer sets a newoauth_token(a 200 without one is a failure); after a failure the client doesn't tryrefreshTokenagain for 30 minutes. - ✅ The session can die before 24 h. The client assumes a 20 h
lifetime, and on an HTTP 401 from a data endpoint it logs in again once
and retries.
Librusdoes this for you - and when several requests hit the same dead session at once, they all wait for one login instead of each logging in. - To resume later, persist
LibrusSessionData(viaexport_session()orLibrus.session_data) and pass it back viaimport_session()orLibrus(..., session_data=...).Librus.session_datacan still be read afterclose()(it holds the cookies the session had when it closed) and reading it never opens a new session.
One account per cookie jar¶
The session lives in the cookie jar, and a jar holds only one
oauth_token per domain. ✅ Two accounts sharing one
aiohttp.ClientSession will overwrite each other's session and receive
each other's data, intermittently. Use one session per account.