M-ID je jednotné přihlášení pro aplikace rodiny melichar.dev na adrese https://id.melichar.dev. Uživatel má jeden účet a jedno heslo (nebo passkey) a do každé aplikace se přihlašuje přes M-ID. Aplikace tak nikdy nevidí jeho heslo: dostane jen potvrzení, kdo se přihlásil.
Technicky jde o autorizační kód s PKCE ve stylu OAuth 2.1, doplněný o OpenID Connect (popis serveru najdete na /.well-known/openid-configuration). Tento text projde celý tok krok za krokem a u každého kroku vysvětlí, před jakým útokem chrání.
Kdo se toku účastní
- Prohlížeč uživatele, který jen přenáší přesměrování mezi stranami.
- Server aplikace (například Attesto), která chce vědět, kdo je uživatel.
- M-ID, které uživatele přihlásí a aplikaci vydá jednorázový kód.
Důležité je, co se odehrává na serveru aplikace a co v prohlížeči. Tajné hodnoty se potkávají jen na serveru; prohlížeč přenáší pouze to, co smí vidět kdokoli.
Registrace aplikace
Každá aplikace má v M-ID záznam klienta (ClientApp) s identifikátorem client_id a seznamem povolených návratových adres redirectUris.
Návratová adresa (redirect_uri) musí s registrovanou adresou souhlasit přesně, znak po znaku: schéma, doména, port i cesta. Neplatí žádné zástupné znaky, žádné query parametry ani shoda podle začátku adresy. Díky tomu útočník nemůže nechat kód poslat na svou adresu.
Aplikace si redirect_uri skládá z pevně nastavené veřejné adresy (proměnná APP_URL), ne z hlavičky Host příchozího požadavku. Za reverzní proxy nebo v Dockeru může hlavička obsahovat interní adresu a přihlášení by selhalo, nebo by se dalo zneužít.
Klient je buď veřejný (nemá tajný klíč), nebo důvěrný (má client_secret). U veřejného klienta je PKCE povinné vždy. Má-li klient tajný klíč, M-ID ho při výměně kódu vyžaduje. Attesto je důvěrný klient a PKCE používá také.
Krok 1: začátek přihlášení
Když uživatel klikne na „Přihlásit“, server aplikace připraví tři hodnoty:
state: alespoň 32 náhodných bajtů v kódování base64url. Spojuje začátek toku s jeho koncem.code_verifier: 32 náhodných bajtů v base64url (43 znaků). Je to tajemství, které nikdy neopustí server aplikace a cookie.code_challenge: base64url(SHA-256(code_verifier)), tedy otisk verifieru. Metoda je vždy S256; metoduplainM-ID odmítne.
Hodnoty state a code_verifier si aplikace uloží do krátkodobé „pending“ cookie: HttpOnly (JavaScript na stránce ji nepřečte), SameSite=Lax, platnost 15 minut a na HTTPS s příznakem Secure a prefixem __Host-.
Pak prohlížeč přesměruje na přihlašovací stránku M-ID s parametry response_type=code, client_id, redirect_uri, scope=openid email profile, state, code_challenge a code_challenge_method=S256.
Proč otisk, a ne samotný verifier? Adresa s parametry projde prohlížečem a může skončit v historii nebo v logu. Z otisku SHA-256 verifier spočítat nejde, takže kdo zachytí adresu, nemá v ruce nic, co by mu k výměně kódu stačilo.
Krok 2: návrat do aplikace
Po přihlášení M-ID přesměruje prohlížeč zpět na redirect_uri s parametry code a state.
Aplikace nejprve načte pending cookie a porovná state z adresy s hodnotou v cookie. Porovnání probíhá v konstantním čase (crypto.timingSafeEqual), aby z délky odpovědi nešlo hodnotu postupně uhodnout. Pak cookie hned smaže: platí jen jednou.
Pokud tento prohlížeč tok nezačal, cookie chybí nebo state nesouhlasí, a aplikace kód nevymění. Tím se brání útoku login CSRF, kdy by útočník podstrčil oběti svůj vlastní kód a oběť by nevědomky pracovala pod účtem útočníka.
Pokud adresa místo kódu nese parametr error, aplikace uživateli srozumitelně oznámí, že přihlášení se nepovedlo.
Krok 3: výměna kódu za tokeny
Výměna probíhá jen na serveru aplikace: server pošle POST /api/auth/exchange s grant_type=authorization_code, kódem, client_id, stejnou redirect_uri jako v kroku 1 a code_verifier z cookie. Důvěrný klient přidá client_secret.
M-ID spočítá SHA-256 z došlého verifieru a porovná ho s code_challenge z kroku 1. Ukradený kód je tak bez verifieru k ničemu.
Autorizační kód platí 60 sekund a jen jednou: první pokus o výměnu ho spotřebuje, ať uspěje, nebo ne. Neznámý, prošlý, už použitý kód, jiná redirect_uri nebo špatný verifier skončí chybou invalid_grant; chybný nebo chybějící tajný klíč chybou invalid_client.
Co aplikace dostane
Úspěšná výměna vrátí:
access_tokens platností 15 minut (expires_in: 900) a volitelněid_token;- objekt
users položkamiid,email,isEmailVerified, jménem a adresou avatara; - seznam
organizations, pokud aplikace pracuje s organizacemi.
Obnovovací (refresh) token klientské aplikace nedostávají. Proto si aplikace po přihlášení vytvoří vlastní session (u Attesta podepsaná cookie na 30 dní) a token M-ID dál nepotřebuje. Když session vyprší, uživatel projde tokem znovu; má-li v M-ID platné přihlášení, proběhne to bez zadávání hesla.
Místní účet aplikace páruje podle user.id (v OpenID Connect je to sub), ne podle e-mailu: e-mail se může změnit, identifikátor ne. E-mailu aplikace věří, jen když isEmailVerified je true.
Odhlášení
Odhlášení má dva kroky a záleží na jejich pořadí. Aplikace nejprve smaže svou session a potom přesměruje prohlížeč na /api/auth/end-session s client_id a post_logout_redirect_uri.
Bez druhého kroku zůstane v prohlížeči přihlašovací cookie M-ID a další přihlášení na stejném zařízení proběhne potichu, bez hesla. Adresa post_logout_redirect_uri musí mít stejný původ (origin) jako některá registrovaná redirect_uri klienta; jinak M-ID uživatele pošle na svou přihlašovací stránku.
Odebrání přístupu a webhooky
Uživatel může aplikaci v M-ID kdykoli odebrat přístup (Dashboard → Aplikace → Odebrat přístup). M-ID pak smaže nevyužité kódy pro tuto aplikaci a při dalším přihlášení po uživateli chce nový souhlas tlačítkem Povolit přístup. Souhlasovou obrazovku zobrazuje M-ID, aplikace ji neimplementuje.
Aby aplikace o odebrání věděla hned, přihlásí se k webhookům. M-ID pošle POST s událostí user.app_revoked a aplikace smaže všechny místní session daného uživatele. Událost user.updated hlásí změnu profilu, ping je zkušební.
Každý webhook nese hlavičku X-Melichar-Signature: t=<unix čas>,v1=<hex>. Aplikace spočítá HMAC-SHA256 se sdíleným tajemstvím nad řetězcem "<t>.<tělo požadavku>" a porovná ho s v1. Požadavek, jehož čas t se od aktuálního liší o víc než 300 sekund, odmítne, aby nešlo znovu přehrát starou zprávu.
Shrnutí
redirect_uripřesně podle registrace a z pevnéAPP_URL.- PKCE S256: v prohlížeči jen otisk, verifier zůstává na serveru.
statev HttpOnly cookie, ověřený v konstantním čase, cookie smazaná po prvním použití.- Výměna kódu jen na serveru; kód platí 60 sekund a jednou.
- Účet podle
user.id, e-mail jen ověřený, vlastní session místo refresh tokenu. - Odhlášení: nejdřív vlastní session, pak
end-session. - Webhooky s ověřeným podpisem a oknem 300 sekund; na
user.app_revokedsmazat session.