3. Sign out everywhere
Signing out at account signs you out of every app in this browser.
rc_sign_out() sends the browser to account, which ends the one session behind every app's token. No app has to be told.
This is the code serving the zoo right now: read from disk for this request, from commit
0c1c124a02 (live). The zoo's own self-check fetches this
page and compares it byte for byte with the file it runs.
exhibits/03-sign-out-everywhere.php sha256 f8b218ae5285 · raw
1<?php 2// Exhibit 3. rc_sign_out() forgets this app's token and sends the visitor to account's 3// "sign out everywhere". Every other app drops its token on its next request, because the hint 4// cookie account set on the parent domain is gone. 5// 6// After signing out you are a visitor, so the zoo cannot ask auth_user() who you were. It keeps 7// that in a short-lived signed cookie, only to let you confirm "every card said visitor". 8 9function signout_mark(): string { 10 if (meta("signout_key") === null) meta_set("signout_key", bin2hex(random_bytes(16))); 11 return meta("signout_key"); 12} 13 14return [ 15 "n" => 3, "wing" => "Identity", "kind" => "human", 16 "title" => "Sign out everywhere", 17 "promise" => "Signing out at account signs you out of every app in this browser.", 18 "block" => "rc_sign_out() sends the browser to account, which ends the one session behind every app's token. No app has to be told.", 19 "show" => function (?array $me): string { 20 if ($me) return '<p>Press this, confirm at account, and come back: every "who am I" card (here and on ' 21 . '<a href="' . h(rc_app_url("zoo-broken")) . '/" target="_blank" rel="noopener">the second app</a>) should say visitor.</p>' 22 . '<p><a href="/api/3/signout"><button class="primary">Sign out everywhere</button></a></p>'; 23 [$uid, $t, $sig] = array_pad(explode(".", (string)($_COOKIE["__Host-zoo_out"] ?? "")), 3, ""); 24 if ($sig !== "" && hash_equals(hash_hmac("sha256", "$uid.$t", signout_mark()), $sig) && time() - (int)$t < 3600) { 25 $u = rc_users([(int)$uid])[(int)$uid] ?? null; 26 return '<p>You signed out as <b>@' . h($u["username"] ?? "?") . '</b>. Does every card say visitor, here and on <a href="' 27 . h(rc_app_url("zoo-broken")) . '/" target="_blank" rel="noopener">the second app</a>?</p>' 28 . '<p><button class="seen-out primary">Yes, every card says visitor</button></p>'; 29 } 30 return '<p>Sign in first; then this button signs you out of every app at once.</p>'; 31 }, 32 "api" => function (string $do, ?array $me, array $in, bool $post): ?array { 33 if ($do === "signout") { 34 if ($me) { 35 $t = time(); 36 setcookie("__Host-zoo_out", "{$me['id']}.$t." . hash_hmac("sha256", "{$me['id']}.$t", signout_mark()), 37 ["expires" => $t + 3600, "path" => "/", "secure" => true, "httponly" => true, "samesite" => "Lax"]); 38 } 39 rc_sign_out("https://" . env("RC_HOST") . "/#e3"); 40 } 41 if ($do === "confirm" && $post) { 42 [$uid, $t, $sig] = array_pad(explode(".", (string)($_COOKIE["__Host-zoo_out"] ?? "")), 3, ""); 43 if ($me) return ["error" => "you are signed in again: this card says your name", "status" => 409]; 44 if ($sig === "" || !hash_equals(hash_hmac("sha256", "$uid.$t", signout_mark()), $sig) || time() - (int)$t > 3600) return ["error" => "no recent sign-out from this browser", "status" => 400]; 45 seen(3, ["id" => (int)$uid]); 46 setcookie("__Host-zoo_out", "", ["expires" => 1, "path" => "/", "secure" => true, "httponly" => true, "samesite" => "Lax"]); 47 return ["ok" => true]; 48 } 49 return null; 50 }, 51 "script" => <<<'JS' 52document.querySelector(".seen-out")?.addEventListener("click", async (ev) => { 53 ev.target.disabled = true; 54 const j = await zoo.call("/api/3/confirm", {}); 55 ev.target.textContent = j.error || "Recorded, thank you"; 56 if (!j.error) setTimeout(() => location.reload(), 600); 57}); 58 59JS, 60];