$get('SASSLY_AUTH_ISSUER', 'https://auth.sassly.ai'), 'client_id' => $get('SASSLY_AUTH_CLIENT_ID'), 'client_secret' => $get('SASSLY_AUTH_CLIENT_SECRET'), 'redirect_uri' => $get('SASSLY_AUTH_REDIRECT_URI'), 'post_logout_redirect_uri' => $get('SASSLY_AUTH_POST_LOGOUT_REDIRECT_URI'), 'cache_dir' => $get('SASSLY_AUTH_CACHE_DIR', dirname(__DIR__, 2) . '/storage/cache/sassly-auth'), 'ca_file' => $get('SASSLY_AUTH_CA_FILE'), 'allow_insecure_http' => $get('SASSLY_AUTH_ALLOW_INSECURE_HTTP') === '1', 'revalidate_interval' => (int)$get('SASSLY_AUTH_REVALIDATE_INTERVAL', '300'), ]; } /** ADAPT: unique, host-only session cookie name for THIS app (never shared across subdomains). */ function sassly_session_name(): string { $v = $_ENV['APP_SESSION_NAME'] ?? getenv('APP_SESSION_NAME'); return is_string($v) && $v !== '' ? $v : 'app_sess'; } /** ADAPT: URL paths of the auth routes in this app (e.g. '/auth/login.php' for flat-file apps). */ function sassly_paths(): array { return [ 'login' => '/auth/login', 'logout' => '/auth/logout', 'choose' => '/auth/choose', 'home' => '/', ]; } function sassly_auth(): SasslyAuthClient { static $client = null; if ($client === null) { $client = new SasslyAuthClient(sassly_auth_config()); } return $client; } /** Start the PHP session with safe cookie settings. Use instead of a bare session_start(). */ function sassly_session_start(): void { if (session_status() === PHP_SESSION_ACTIVE) { return; } $secure = str_starts_with((string)sassly_auth_config()['redirect_uri'], 'https://'); ini_set('session.use_strict_mode', '1'); ini_set('session.use_only_cookies', '1'); session_name(sassly_session_name()); session_set_cookie_params([ 'lifetime' => 0, 'path' => '/', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax', // Lax, not Strict: the login callback is a cross-site redirect ]); // no 'domain' => host-only cookie session_start(); } /** * Top of every protected page or endpoint. Returns the active local user row, or ends the request * (redirect to login for pages, 401 JSON for AJAX, 403 page for users disabled in this app). */ function sassly_require_login(): array { sassly_session_start(); $auth = sassly_auth(); if ($auth->check() === SasslyAuthClient::VALID && isset($_SESSION['sassly_user_id'])) { $sub = (string)($auth->identity()['sub'] ?? ''); $user = sassly_load_active_user($_SESSION['sassly_user_id'], $sub); if ($user !== null) { return $user; } // Disabled or unlinked in this app. Do NOT redirect to login: auth.sassly.ai would log the // user straight back in and the app would refuse again (redirect loop). sassly_clear_login(); sassly_render_page(403, 'Akses ditutup', 'Akun Anda tidak aktif lagi di aplikasi ini. Hubungi admin.', true); exit; } sassly_clear_login(); if (sassly_is_ajax()) { http_response_code(401); header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store'); echo json_encode(['error' => 'unauthenticated', 'login_url' => sassly_paths()['login']]); exit; } $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $returnTo = $method === 'GET' ? SasslyAuthClient::sanitizeReturnTo($_SERVER['REQUEST_URI'] ?? '/', '/') : sassly_paths()['home']; header('Cache-Control: no-store'); header('Location: ' . sassly_paths()['login'] . '?return_to=' . rawurlencode($returnTo), true, 302); exit; } /** Called by callback/choose once the app accepted the user. */ function sassly_complete_login(array $identity, array $user): never { sassly_auth()->startSession($identity); // regenerates the session id unset($_SESSION['sassly_choose'], $_SESSION['sassly_denied_hint']); $_SESSION['sassly_user_id'] = $user['id']; sassly_on_login($user, $identity); header('Cache-Control: no-store'); header('Location: ' . SasslyAuthClient::sanitizeReturnTo($identity['return_to'] ?? '/', sassly_paths()['home']), true, 302); exit; } /** Forget the local login (Sassly tokens + app keys). Login attempts in other tabs survive. */ function sassly_clear_login(): void { sassly_auth()->clearLocal(); unset($_SESSION['sassly_user_id'], $_SESSION['sassly_choose']); foreach (sassly_app_session_keys() as $key) { unset($_SESSION[$key]); } } function sassly_is_ajax(): bool { if (strtolower($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'xmlhttprequest') { return true; } $accept = strtolower($_SERVER['HTTP_ACCEPT'] ?? ''); return str_contains($accept, 'application/json') && !str_contains($accept, 'text/html'); } function sassly_csrf_token(): string { if (!is_string($_SESSION['sassly_csrf'] ?? null)) { $_SESSION['sassly_csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['sassly_csrf']; } function sassly_csrf_valid(mixed $token): bool { return is_string($token) && is_string($_SESSION['sassly_csrf'] ?? null) && hash_equals($_SESSION['sassly_csrf'], $token); } function sassly_e(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } /** Minimal standalone page (ADAPT the look to the app if wanted). Never put tokens in it. */ function sassly_render_page(int $status, string $title, string $message, bool $showLogout = false, ?string $retryUrl = null): void { http_response_code($status); header('Content-Type: text/html; charset=utf-8'); header('Cache-Control: no-store'); echo '
' . '' . sassly_e($message) . '
'; if ($retryUrl !== null) { echo ''; } if ($showLogout) { echo ''; } echo ''; }