SSO: redeem ticket over HTTPS instead of the central DB
This install is on external hosting and can't reach the central DB's private LAN address (192.168.1.3), so the direct-DB burn always failed with ECONNREFUSED. Redeem via the hub's api/sso_redeem.php over HTTPS (HMAC with HUB_CONTENT_SECRET, ticket folded into the signature) and provision the returned user into the local DB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
9ad0c2b763
commit
7885b0b6ac
1 file changed
+63
-29
+63
-29
@@ -107,47 +107,81 @@ function sso_resolve_session(PDO $pdo): void {
|
|||||||
// Cross-domain handoff: redeem a one-time ticket minted by the hub. This is the
|
// Cross-domain handoff: redeem a one-time ticket minted by the hub. This is the
|
||||||
// real login path for this install, because fitme3070.pt is NOT under
|
// real login path for this install, because fitme3070.pt is NOT under
|
||||||
// .andmore.pt — the am_sso cookie never reaches it, so sso_resolve_session()'s
|
// .andmore.pt — the am_sso cookie never reaches it, so sso_resolve_session()'s
|
||||||
// cookie lookup always comes up empty. Instead the hub appends ?sso_ticket=… to
|
// cookie lookup always comes up empty. The hub appends ?sso_ticket=… to the
|
||||||
// the return URL after authenticating; we burn it here against the central
|
// return URL after authenticating.
|
||||||
// sso_tickets table (atomic single-use, bound to this PLATFORM_SLUG, ~60s TTL)
|
//
|
||||||
// and provision the local row. Returns true when a session was established.
|
// We redeem over HTTPS, NOT the direct central DB: this install lives on
|
||||||
|
// external hosting and can't reach the central DB's private LAN address. The
|
||||||
|
// hub's api/sso_redeem.php burns the ticket against its local DB (single-use,
|
||||||
|
// bound to PLATFORM_SLUG, ~60s TTL) and returns the central user, which we
|
||||||
|
// provision into the LOCAL DB. Auth reuses the content-pull HMAC scheme with
|
||||||
|
// the ticket folded in. Returns true when a session was established.
|
||||||
function sso_redeem_ticket(PDO $pdo, string $ticket): bool {
|
function sso_redeem_ticket(PDO $pdo, string $ticket): bool {
|
||||||
if (!identity_is_federated()) return false;
|
if (!identity_is_federated()) return false;
|
||||||
if (strlen($ticket) !== 64 || !ctype_xdigit($ticket)) return false;
|
if (strlen($ticket) !== 64 || !ctype_xdigit($ticket)) return false;
|
||||||
|
|
||||||
$ssoPdo = _sso_open_pdo();
|
$secret = (string) env('HUB_CONTENT_SECRET', '');
|
||||||
if (!$ssoPdo) return false;
|
if ($secret === '') { error_log('[sso] ticket redeem: HUB_CONTENT_SECRET not set'); return false; }
|
||||||
|
|
||||||
try {
|
$url = rtrim(ANDMORE_ID_URL, '/') . '/api/sso_redeem.php';
|
||||||
// Burn first: the UPDATE only affects a row when the ticket is unused,
|
$path = (string) (parse_url($url, PHP_URL_PATH) ?? '/');
|
||||||
// unexpired and bound to this platform — so a replay or a ticket minted
|
$date = gmdate('c');
|
||||||
// for a sibling platform changes nothing and we bail.
|
$token = hash_hmac('sha256', $date . $path . $ticket, $secret);
|
||||||
$burn = $ssoPdo->prepare("
|
$body = http_build_query(['ticket' => $ticket]);
|
||||||
UPDATE sso_tickets SET used_at = NOW()
|
$headers = [
|
||||||
WHERE ticket = ? AND platform = ? AND used_at IS NULL AND expires_at > NOW()
|
'X-Hub-Platform: ' . PLATFORM_SLUG,
|
||||||
");
|
'X-Hub-Date: ' . $date,
|
||||||
$burn->execute([$ticket, PLATFORM_SLUG]);
|
'X-Hub-Token: ' . $token,
|
||||||
if ($burn->rowCount() !== 1) return false;
|
'Accept: application/json',
|
||||||
|
'Content-Type: application/x-www-form-urlencoded',
|
||||||
|
];
|
||||||
|
|
||||||
$stmt = $ssoPdo->prepare("
|
// cURL first (same transport the notify/content clients use); stream
|
||||||
SELECT t.user_id, u.email, u.name, u.phone, u.is_active
|
// fallback for installs without the extension.
|
||||||
FROM sso_tickets t
|
$raw = false;
|
||||||
JOIN users u ON u.id = t.user_id
|
if (function_exists('curl_init')) {
|
||||||
WHERE t.ticket = ?
|
$ch = curl_init($url);
|
||||||
LIMIT 1
|
curl_setopt_array($ch, [
|
||||||
");
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
$stmt->execute([$ticket]);
|
CURLOPT_POST => true,
|
||||||
$row = $stmt->fetch();
|
CURLOPT_POSTFIELDS => $body,
|
||||||
} catch (PDOException $e) {
|
CURLOPT_TIMEOUT => 12,
|
||||||
error_log('[sso] ticket redeem failed: ' . $e->getMessage());
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
CURLOPT_USERAGENT => 'fitme3070-SSO/1.0',
|
||||||
|
]);
|
||||||
|
$raw = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
}
|
||||||
|
if ($raw === false) {
|
||||||
|
$ctx = stream_context_create(['http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'timeout' => 12,
|
||||||
|
'header' => implode("\r\n", $headers),
|
||||||
|
'content' => $body,
|
||||||
|
'ignore_errors' => true,
|
||||||
|
]]);
|
||||||
|
$raw = @file_get_contents($url, false, $ctx);
|
||||||
|
}
|
||||||
|
if ($raw === false) { error_log('[sso] ticket redeem: no response from hub'); return false; }
|
||||||
|
|
||||||
|
$data = json_decode((string) $raw, true);
|
||||||
|
if (!is_array($data) || empty($data['ok']) || empty($data['user'])) {
|
||||||
|
error_log('[sso] ticket redeem rejected: ' . substr((string) $raw, 0, 200));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$row || !$row['is_active']) return false;
|
$u = $data['user'];
|
||||||
|
$ssoRow = [
|
||||||
|
'user_id' => (int) ($u['user_id'] ?? 0),
|
||||||
|
'email' => (string) ($u['email'] ?? ''),
|
||||||
|
'name' => (string) ($u['name'] ?? ''),
|
||||||
|
'phone' => $u['phone'] ?? null,
|
||||||
|
];
|
||||||
|
if ($ssoRow['user_id'] <= 0 || $ssoRow['email'] === '') return false;
|
||||||
|
|
||||||
// Explicit fresh-login intent — drop any stale local identity, then provision.
|
// Explicit fresh-login intent — drop any stale local identity, then provision.
|
||||||
unset($_SESSION['user_id'], $_SESSION['user']);
|
unset($_SESSION['user_id'], $_SESSION['user']);
|
||||||
return _sso_provision_local($pdo, $row);
|
return _sso_provision_local($pdo, $ssoRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find-or-create the local users row for a central identity, sync name/phone,
|
// Find-or-create the local users row for a central identity, sync name/phone,
|
||||||
|
|||||||
Reference in new issue
Block a user