# Mars ID — integration reference for developers and AI agents > Mars ID is the SSO / OpenID Connect provider of Mars IT School (Tashkent). > People sign in with the Telegram bot @marshubbot — no passwords, no email codes. > Human version of this document: https://marshub.uz/id/dev > Provider base URL: https://id.marshub.uz ## Pick the integration path | Your project lives on | Use | Needs from us | |-----------------------|-----|---------------| | a static site at `.marshub.uz` | site access list (proxy-level gate) | yes — tell us the subdomain | | a single page at `pages.marshub.uz//` | page access mode `marsid` | no — self-serve | | your own backend at `.marshub.uz` | `__mars_id` cookie | routing only | | any other host or domain (Vercel, Render, VPS, localhost) | OAuth2 / OIDC | yes — `client_id` + `client_secret` | | a Telegram Mini App | `POST /api/auth/telegram-webapp` with `initData` | yes — origin allowlist | Ask for keys, routes and allowlist entries: https://t.me/marvinaka ## Path A — cookie (only works on *.marshub.uz) After login the browser holds cookie `__mars_id` on domain `.marshub.uz`, so every subdomain receives it automatically. Cookie: name `__mars_id`, domain `.marshub.uz`, httpOnly, secure, sameSite=lax, JWT signed HS256, 30-day expiry. Client-side JS cannot read it — verify server-side. ### Recommended: verify without any secret ``` GET https://id.marshub.uz/api/verify Authorization: Bearer 200 → {"id":"","name":"Aziz","role":"student","tg":123456789} 401 → {"error":"invalid"} ``` ```javascript const token = req.cookies['__mars_id']; const r = await fetch('https://id.marshub.uz/api/verify', { headers: { Authorization: `Bearer ${token}` }, }); const user = r.ok ? await r.json() : null; ``` ```python token = request.cookies.get("__mars_id") r = requests.get("https://id.marshub.uz/api/verify", headers={"Authorization": f"Bearer {token}"}, timeout=5) user = r.json() if r.ok else None ``` No user → redirect to: `https://id.marshub.uz/login?next=` Logout link: `https://id.marshub.uz/logout` ### Alternative: verify the signature locally Requires the shared `AUTH_SECRET`, which is the master key of the whole ecosystem — it is handed to internal Mars services only, never to third-party or student projects. If you have it: `jwt.verify(token, AUTH_SECRET, { algorithms: ['HS256'] })` — always pin the algorithm. ### Routing gotcha On `*.marshub.uz`, the path `/api/*` is by default captured by the shared MarshHub API, which strips the Cookie header (CSRF defence for student-hosted sites). If your service serves its own `/api/…`, tell us so your route is placed above that matcher. Symptom when forgotten: your `/api/…` answers `{"error":"Требуется авторизация"}`. ## Path B — OAuth2 / OpenID Connect (any host, any domain) Discovery: https://id.marshub.uz/.well-known/openid-configuration | Setting | Value | |---------|-------| | issuer | `https://id.marshub.uz` | | authorization_endpoint | `https://id.marshub.uz/oauth/authorize` | | token_endpoint | `https://id.marshub.uz/oauth/token` | | userinfo_endpoint | `https://id.marshub.uz/oauth/userinfo` | | response_type | `code` | | scopes | `openid profile email` | | id_token alg | `HS256`, signed with your `client_secret` | | client auth | `client_secret_post` or `client_secret_basic` | Not supported: PKCE, refresh tokens, RS256/JWKS (the JWKS endpoint returns an empty key set on purpose). Authorization codes are single-use and expire in 5 minutes; access and id tokens live 1 hour. A browser-only SPA cannot complete this flow — the code exchange must happen on a server that holds the `client_secret`. `redirect_uri` is matched by **exact string equality** — no wildcards, no prefixes, a trailing slash makes it a different URI. Register every environment you need (prod, stage, `http://localhost:PORT/...`). Flow: 1. `GET /oauth/authorize?client_id=…&redirect_uri=…&response_type=code&scope=openid%20profile%20email&state=…` 2. user confirms in Telegram, returns to `redirect_uri?code=…&state=…` (verify `state`) 3. `POST /oauth/token` with `{grant_type:"authorization_code", code, redirect_uri, client_id, client_secret}` → `{access_token, id_token, token_type:"Bearer", expires_in:3600}` 4. `GET /oauth/userinfo` with `Authorization: Bearer ` To get a client, send: project name, exact callback URL(s), and who may sign in (anyone with a Mars ID / students only / staff only). ## Path C — Telegram Mini App ```javascript await fetch('https://id.marshub.uz/api/auth/telegram-webapp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ initData: window.Telegram.WebApp.initData }), }); // → { id, name, handle, role, tg, needs_handle } and the __mars_id cookie is set ``` The server checks the Telegram HMAC of `initData` (valid for 24h). Cross-origin calls need your Mini App origin on the allowlist — send us the URL. ## Path D — no code at all - Whole static site at `.marshub.uz`: we set an access list on it. Empty list = any signed-in Mars ID user; a list of Telegram IDs = only those (others get 403); no list = fully public. Anonymous visitors are redirected to login and bounced back. - Single page at `pages.marshub.uz`: access mode `public`, `password` (shared password, no account needed) or `marsid` (Telegram login, optional Telegram-ID whitelist). Self-serve via the MarshHub panel, API, or the MCP tool `marshub_set_page_access`. ## Identity claims Cookie JWT payload and OIDC id_token/userinfo carry: | Claim | Type | Meaning | |-------|------|---------| | `sub` | string | stable user UUID | | `name` | string | display name from Telegram | | `handle` | string\|null | unique handle, e.g. `aziz`; null before onboarding | | `preferred_username` | string | handle (OIDC only) | | `email` | string | `@marshub.uz` (derived, not independently verified) | | `role` | string | `member`, `student`, `intern`, `tutor`, `mentor`, `team`, `admin` | | `tg` | number | Telegram user ID | | `is_staff` | boolean | present in staff registry (HR) | | `is_student` | boolean | active student in the school's core database | | `core_id` | number\|null | id in the school's core database, when linked | | `exp` | number | cookie JWT: 30 days; OIDC tokens: 1 hour | `role` describes identity, not permissions. Authorization stays in your app — use `is_staff` / `is_student` for coarse gating and your own list for the rest. `/api/verify` and `/api/me` return only `{id, name, role, tg}`; the full claim set comes from the decoded cookie JWT or from OIDC userinfo. ## Endpoints | Endpoint | Method | Auth | Purpose | |----------|--------|------|---------| | `/login?next=URL` | GET | — | login page, returns to URL | | `/logout` | GET | — | clear cookie | | `/api/verify` | GET | Bearer or cookie | verify a token | | `/api/me` | GET | cookie | current user (cross-origin needs origin allowlist) | | `/api/auth/telegram-webapp` | POST | initData HMAC | Mini App login | | `/.well-known/openid-configuration` | GET | — | OIDC discovery | | `/oauth/authorize` | GET | — | OAuth2 authorization | | `/oauth/token` | POST | client_secret | code → tokens | | `/oauth/userinfo` | GET | Bearer | user claims | | `/health` | GET | — | health check | Note: `/api/verify` reads `Authorization: Bearer ` or the cookie. A `?token=` query parameter is **not** supported (older docs claimed otherwise). ## Rules of thumb - Verify identity server-side. Never trust a role, id or "isAdmin" sent by the browser. - Pin `HS256` when verifying signatures yourself. - URL-encode the `next` parameter. - The cookie does not exist on localhost — develop against OIDC with a localhost callback. - A 30-day token keeps stale claims after a role change; re-check via `/api/verify` when it matters. - Anyone who presses Start in @marshubbot has a Mars ID — there is no separate signup for your app. ## Related - What Mars ID is, for non-developers: https://marshub.uz/id - Static hosting for agents: https://marshub.uz/llms.txt and https://marshub.uz/llms-full.txt - Guides: https://learn.marshub.uz/marsid-dev.html (UZ/RU, mobile)