From 3c4e79ecfa350b32cced311479baf428e5734058 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Sat, 11 Jul 2026 17:33:42 +0200 Subject: [PATCH] fix(2fa): QR approve uses request host and one-tap link token Build approval URLs from the browser Host header (apps.f0xx.org not acl0); allow approve by QR token without a second TOTP on mobile. Co-authored-by: Cursor --- src/Auth.php | 17 +++++++++++++++++ src/AuthApproval.php | 13 ++++++++++++- src/AuthTwoFactorPage.php | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Auth.php b/src/Auth.php index 3fb45e4..b8be792 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -227,6 +227,23 @@ final class Auth { return (string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); } + /** Origin for outbound links (QR, email) — matches the host the user is browsing. */ + public static function requestPublicOrigin(): string { + $host = trim(explode(',', (string) ($_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'] ?? ''))[0]); + $host = preg_replace('/:\d+$/', '', $host); + if ($host !== '' && !in_array($host, ['127.0.0.1', 'localhost'], true)) { + $scheme = 'http'; + if (function_exists('platform_is_https_request') && platform_is_https_request()) { + $scheme = 'https'; + } elseif (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { + $scheme = 'https'; + } + return $scheme . '://' . $host; + } + $cfg = rtrim((string) cfg('public_origin', ''), '/'); + return $cfg !== '' ? $cfg : 'https://apps.f0xx.org'; + } + public static function logout(): void { self::clearPending2fa(); unset($_SESSION['user']); diff --git a/src/AuthApproval.php b/src/AuthApproval.php index 27c6fee..fb7919d 100644 --- a/src/AuthApproval.php +++ b/src/AuthApproval.php @@ -131,7 +131,18 @@ final class AuthApproval { } /** - * Approve using TOTP (mobile not logged in — scan QR, enter authenticator code). + * One-tap approve from QR / magic link (token is the credential). + */ + public static function approveByLinkToken(string $rawToken, string $approverIp): bool { + $info = self::getForToken($rawToken); + if ($info === null) { + return false; + } + return self::approve($rawToken, (int) ($info['user_id'] ?? 0), $approverIp); + } + + /** + * Approve using TOTP (optional fallback when link alone is not enough). */ public static function approveWithTotp(string $rawToken, string $code, string $approverIp): bool { $info = self::getForToken($rawToken); diff --git a/src/AuthTwoFactorPage.php b/src/AuthTwoFactorPage.php index 35950c0..1fe4cdb 100644 --- a/src/AuthTwoFactorPage.php +++ b/src/AuthTwoFactorPage.php @@ -29,7 +29,7 @@ final class AuthTwoFactorPage { $_SESSION['pending_2fa_approval_token'] = $rawToken; } - $origin = rtrim((string) cfg('public_origin', 'https://apps.f0xx.org'), '/'); + $origin = Auth::requestPublicOrigin(); $approveUrl = $origin . $project . '/two-factor/approve?token=' . urlencode($rawToken); $payload = [