// // Protect any other PHP page with: // require __DIR__ . '/sso.php'; // $user = sso_require_login(); // signs in silently, or shows login.html // echo htmlspecialchars($user['name']); const SSO = 'https://ae-sso.advantech.com'; define('CLIENT_ID', getenv('SSO_CLIENT_ID')); define('CLIENT_SECRET', getenv('SSO_CLIENT_SECRET')); define('APP_URL', rtrim(getenv('APP_URL'), '/')); define('REDIRECT_URI', APP_URL . '/sso.php?a=callback'); session_set_cookie_params(['httponly' => true, 'secure' => str_starts_with(APP_URL, 'https://'), 'samesite' => 'Lax']); if (session_status() !== PHP_SESSION_ACTIVE) session_start(); function sso_safe_next($v) { return is_string($v) && str_starts_with($v, '/') && !str_starts_with($v, '//') ? $v : '/'; } // ---------- 2. is the user signed in? ---------- function sso_user() { return $_SESSION['user'] ?? null; // null = not signed in } function sso_require_login() { $u = sso_user(); if ($u === null) { // try single sign-on first; login.html is shown only if that fails header('Location: /sso.php?' . http_build_query(['a' => 'login', 'silent' => 1, 'next' => $_SERVER['REQUEST_URI']])); exit; } return $u; } // only route when sso.php itself is requested (not when it is require'd) if (realpath($_SERVER['SCRIPT_FILENAME']) !== __FILE__) return; switch ($_GET['a'] ?? '') { // ---------- 1. sign in ---------- // ?silent=1 adds prompt=none: if the user already signed in to ANY service, // the SSO returns straight away (no page, no click); otherwise it comes back // with error=login_required and we show login.html. case 'login': $state = bin2hex(random_bytes(16)); // remember several pending sign-ins (two tabs, a double click ...), newest 5 $pending = array_slice($_SESSION['pending'] ?? [], -4, null, true); $pending[$state] = sso_safe_next($_GET['next'] ?? '/'); $_SESSION['pending'] = $pending; $q = ['client_id' => CLIENT_ID, 'redirect_uri' => REDIRECT_URI, 'response_type' => 'code', 'state' => $state]; if (($_GET['silent'] ?? '') === '1') $q['prompt'] = 'none'; header('Location: ' . SSO . '/authorize?' . http_build_query($q)); exit; case 'callback': $state = (string)($_GET['state'] ?? ''); $next = $_SESSION['pending'][$state] ?? null; unset($_SESSION['pending'][$state]); if ($state === '' || $next === null) { // unknown / reused state header('Location: /login.html?error=state'); exit; } if (!empty($_GET['error'])) { // login_required (silent try) / access_denied $q = ['next' => $next]; if ($_GET['error'] !== 'login_required') $q['error'] = $_GET['error']; header('Location: /login.html?' . http_build_query($q)); exit; } $ch = curl_init(SSO . '/token'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_POSTFIELDS => http_build_query([ 'grant_type' => 'authorization_code', 'code' => $_GET['code'] ?? '', 'redirect_uri' => REDIRECT_URI, 'client_id' => CLIENT_ID, 'client_secret' => CLIENT_SECRET, ]), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { header('Location: /login.html?error=token'); exit; } session_regenerate_id(true); $_SESSION['user'] = json_decode($body, true)['user']; // sub, email, name, department, picture ... header('Location: ' . $next); exit; case 'logout': $_SESSION = []; session_destroy(); header('Location: ' . SSO . '/logout?' . http_build_query(['redirect_uri' => APP_URL . '/'])); exit; // ---------- 3. user data (used by advantech-sso.js) ---------- case 'me': header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store'); $u = sso_user(); if ($u === null) { http_response_code(401); echo '{"error":"not signed in"}'; exit; } echo json_encode($u, JSON_UNESCAPED_UNICODE); exit; default: http_response_code(404); }