ya-passport-auth
Async Yandex Passport (mobile) authentication library for Music Assistant providers.
Features
- Three login methods — QR code scan, OAuth Device Flow (short
code on
ya.ru/device, plus arefresh_tokenfor silent re-auth), and cookie-based login. - Full token-derivation graph — x_token exchange, music_token refresh, Passport cookie refresh (redirect-following), Quasar CSRF, Glagol device token, account info.
- Security-first —
SecretStrredacts tokens in repr/str/format/tracebacks and blocks pickling; host allow-list with HTTPS-only enforcement; CSRF extraction; per-request rate limiting; response size caps; log redaction viaRedactingFilter. - Async-native — built on
aiohttpwithasyncio.Lock-protected rate limiter and connection management. - Strictly typed —
mypy --strictclean, PEP 561py.typedmarker. - Well tested — 386 tests, 97 % branch coverage.
Installation
pip install ya-passport-auth
With the Music Assistant credential helpers (token refresh/rotation, shared accounts, and cookie parsing used by the MA Yandex providers):
pip install ya-passport-auth[ma]
Quick start
QR login
from ya_passport_auth import PassportClient
async def qr_login():
async with PassportClient.create() as client:
qr = await client.start_qr_login()
print(f"Scan QR: {qr.qr_url}")
creds = await client.poll_qr_until_confirmed(qr)
info = await client.fetch_account_info(creds.x_token)
print(f"Logged in as {info.display_login} (uid={info.uid})")
Device flow
OAuth 2.0 Device Authorization Grant. The user opens ya.ru/device
on any device, enters the short user_code, and the library receives
an x_token plus a long-lived refresh_token for silent re-auth later.
from ya_passport_auth import DeviceCodeSession, PassportClient
async def device_login():
def on_code(session: DeviceCodeSession) -> None:
print(f"Open {session.verification_url} and enter: {session.user_code}")
async with PassportClient.create() as client:
creds = await client.login_device_code(on_code=on_code)
# creds.refresh_token is populated (only for this flow).
# Persist creds — the access token is valid for ~1 year.
After the x_token expires, mint a new one without user interaction:
new_creds = await client.refresh_credentials(creds)
Only device-flow credentials carry a refresh_token; QR/cookie-login
credentials have refresh_token=None and cannot be silently refreshed.
Device flow for a caller-owned OAuth application
Service providers can use the same polling implementation with their own Yandex OAuth application. The returned tokens are not converted to Passport or Yandex Music credentials:
from ya_passport_auth import OAuthDeviceClient
async def service_login():
async with OAuthDeviceClient.create(
client_id="your-client-id",
client_secret="your-client-secret",
scope="cloud_api:disk.read",
) as client:
tokens = await client.login_device_code(
on_code=lambda code: print(code.verification_url, code.user_code)
)
# Persist tokens.refresh_token for silent rotation.
Music Assistant providers present the code in their own setup UI and can use the MA helper for silent rotation:
from ya_passport_auth.ma import refresh_oauth_tokens
tokens = await refresh_oauth_tokens(
client_id=client_id,
client_secret=client_secret,
refresh_token=tokens.refresh_token,
scope="service.scope",
session=mass.http_session,
)
Cookie login
from ya_passport_auth import PassportClient
async def cookie_login():
cookies = "Session_id=...; sessionid2=..." # from browser
async with PassportClient.create() as client:
creds = await client.login_cookies(cookies)
print(f"x_token acquired, music_token ready")
Architecture
PassportClient (public facade)
├── SafeHttpClient (host allow-list, HTTPS enforcement, size caps, rate limiting)
│ └── AsyncMinDelayLimiter
└── Flows
├── QrLoginFlow → CSRF scrape → session create → poll → x_token
├── DeviceCodeFlow → device_code → ya.ru/device → x_token + refresh_token
├── CookieLoginFlow → raw cookies → x_token
├── _token_exchange → cookies→x_token, x_token→music_token (shared)
├── PassportSessionRefresher → x_token → session cookies (follows redirects)
├── AccountInfoFetcher → x_token → uid/login/avatar
├── QuasarCsrfFetcher → CSRF token for IoT API
└── GlagolDeviceTokenFetcher → music_token → Glagol device token
OAuthDeviceClient (caller-owned Yandex OAuth application)
└── DeviceCodeFlow → service-scoped access_token + refresh_token
API overview
PassportClient
| Method | Description |
|---|---|
start_qr_login() |
Begin QR login, returns QrSession |
poll_qr_until_confirmed(qr) |
Poll until scanned, returns Credentials |
complete_qr_login(qr) |
Exchange confirmed QR for tokens |
start_device_login(...) |
Begin Device Flow, returns DeviceCodeSession |
poll_device_until_confirmed(session, ...) |
Poll until confirmed, returns Credentials |
login_device_code(on_code=..., ...) |
Full Device Flow with callback |
refresh_credentials(creds) |
Mint fresh Credentials via refresh_token |
login_cookies(cookies) |
Exchange browser cookies for Credentials |
refresh_music_token(x_token) |
x_token → music_token |
refresh_passport_cookies(x_token) |
Refresh session cookies (follows redirect chain) |
get_quasar_csrf_token() |
Quasar CSRF token |
get_glagol_device_token(music_token, ...) |
Glagol device token |
fetch_account_info(x_token) |
Account metadata |
validate_x_token(x_token) |
Check if token is valid |
OAuthDeviceClient
| Method | Description |
|---|---|
start_device_login(...) |
Begin Device Flow for a caller-owned OAuth app |
poll_device_until_confirmed(session, ...) |
Poll and return unchanged OAuthTokens |
login_device_code(on_code=..., ...) |
Run both phases with a UI callback |
refresh(refresh_token) |
Rotate the service OAuth token pair |
SecretStr
Opaque wrapper — repr() and str() return ***, pickling raises
TypeError. Access plaintext only via get_secret().
Credentials
Frozen, slotted dataclass returned by poll_qr_until_confirmed() and
login_cookies():
| Field | Type |
|---|---|
x_token |
SecretStr |
music_token |
SecretStr | None |
uid |
int | None |
display_login |
str | None |
refresh_token |
SecretStr | None (device flow only) |
Exception hierarchy
YaPassportError
├── NetworkError
│ └── UnexpectedHostError
└── AuthFailedError
├── InvalidCredentialsError
├── CsrfExtractionError
├── RateLimitedError
├── QRPendingError
├── QRTimeoutError
└── DeviceCodeTimeoutError
Security
- HTTPS-only —
_check_host()rejects any non-httpsURL, preventing protocol-downgrade attacks via redirectLocationheaders. - Host allow-list — every request is validated against a frozen set of allowed Yandex hosts. Redirect targets are checked at each hop.
- Token redaction —
SecretStrhides values inrepr/str/format;RedactingFilterscrubs OAuth headers and hex tokens from log output. - No pickling —
SecretStrandCredentialsblockpickle/copy. - Response size caps — 1 MiB for JSON, 2 MiB for HTML.
- See SECURITY.md for the full threat model (T1–T14).
Used by
- ma-provider-yandex-music — Music Assistant provider for Yandex Music
- ma-provider-yandex-ynison — Music Assistant provider for Yandex Ynison (Spotify Connect analog)
- ma-provider-yandex-station — Music Assistant provider for Yandex Station
Security disclaimer
The Passport-specific client interacts with Yandex using public mobile OAuth
client IDs and secrets extracted from official Yandex Android applications.
These values are well-known and present in many open-source projects; they are
treated here as constants, not secrets. OAuthDeviceClient, by contrast,
accepts credentials for an application owned by its caller.
There is no official Yandex API for the mobile Passport flow. Endpoints, response shapes, and regex patterns may break without notice.
License
Metadata
Release files for ya-passport-auth 2.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ya_passport_auth-2.0.1.tar.gz | 85.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ya_passport_auth-2.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 143.5 kB
Release files / ya_passport_auth-2.0.1.tar.gz
| Download URL | ya_passport_auth-2.0.1.tar.gz |
|---|---|
| Size | 85.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
582a5ab4561724cf2f4708d5e0a338aa0446e3ae4e8e69cdf3186d24f1fe90e4
|
|
BLAKE2b-256 checksum How to use checksums |
b1f102634d33db370b5aad012b3529d9d655fcd604f3d498d1add520f9043159
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.
Transparency logRelease files / ya_passport_auth-2.0.1-py3-none-any.whl
| Download URL | ya_passport_auth-2.0.1-py3-none-any.whl |
|---|---|
| Size | 58.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
34ae8956afef3df9619487e74ee2c8956415ba57037b7d796b3fdd04b457373a
|
|
BLAKE2b-256 checksum How to use checksums |
20cfc333cd069f5f3b3d2236804d0c714d61aefeed4d2ea46d458b65cb73a120
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.
Transparency log