-
-
Notifications
You must be signed in to change notification settings - Fork 404
feat(auth): Add LDAP authentication support #1621
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
eb7a05c
a6c27e2
0567188
cc1ba3b
ec6b7a1
4491d04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,3 +50,4 @@ docker-compose.yml.ffsb42 | |
| test_mounts/ | ||
| .gemini/settings.json | ||
| .vscode/mcp.json | ||
| pr_1621_open_review_comments.md | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove before merge |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,13 @@ | ||
| <!-- NetAlertX CSS --> | ||
| <link rel="stylesheet" href="css/app.css"> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why was this removed? |
||
|
|
||
| <?php | ||
|
|
||
| require_once $_SERVER['DOCUMENT_ROOT'].'/php/server/db.php'; | ||
| require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/language/lang.php'; | ||
| require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/security.php'; | ||
|
|
||
| if (empty($_SESSION['csrf_token'])) { | ||
| $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); | ||
| } | ||
|
|
||
| // if (session_status() === PHP_SESSION_NONE) { | ||
| // session_start(); | ||
| // } | ||
|
|
@@ -15,6 +16,29 @@ | |
|
|
||
| const DEFAULT_REDIRECT = '/devices.php'; | ||
|
|
||
| /* ===================================================== | ||
| LDAP Configuration | ||
| $configLines and $api_token are already loaded by security.php | ||
| ===================================================== */ | ||
|
|
||
| // Config file is the single source of truth (Python backend resolves env vars at startup) | ||
| $ldap_enabled = strtolower(trim(getConfigLine('/^LDAP_enabled\s*=/', $configLines)[1] ?? 'false')) === 'true'; | ||
|
|
||
| /** | ||
| * Derive the Python API port from the GRAPHQL_PORT setting in app.conf. | ||
| * Falls back to 20212 (the default) when not set. | ||
| */ | ||
| $gql_line = getConfigLine('/^GRAPHQL_PORT.*=/', $configLines); | ||
| $graphql_port = 20212; | ||
| if ($gql_line !== null && isset($gql_line[1])) { | ||
| $parsed_port = (int) preg_replace('/[^0-9]/', '', $gql_line[1]); | ||
| if ($parsed_port >= 1 && $parsed_port <= 65535) { | ||
| $graphql_port = $parsed_port; | ||
| } | ||
| } | ||
|
|
||
| $ldap_login_url = "http://127.0.0.1:{$graphql_port}/api/auth/login"; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| /* ===================================================== | ||
| Helper Functions | ||
| ===================================================== */ | ||
|
|
@@ -83,8 +107,24 @@ function is_authenticated(): bool { | |
| } | ||
|
|
||
| function login_user(): void { | ||
| global $nax_Password, $api_token, $configLines; | ||
|
|
||
| $_SESSION['login'] = 1; | ||
| session_regenerate_id(true); | ||
| $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); | ||
|
|
||
| // Set remember-me cookie with HMAC when API_TOKEN is available. | ||
| // On first boot the token may not exist yet — skip the cookie gracefully. | ||
| if (!empty($api_token)) { | ||
| $cookie_value = hash_hmac('sha256', $nax_Password, $api_token); | ||
| setcookie(COOKIE_SAVE_LOGIN_NAME, $cookie_value, [ | ||
| 'expires' => time() + 3600 * 24 * 7, | ||
| 'path' => '/', | ||
| 'httponly' => true, | ||
| 'secure' => !empty($_SERVER['HTTPS']), | ||
| 'samesite' => 'Strict', | ||
| ]); | ||
| } | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -114,16 +154,51 @@ function logout_user(): void { | |
| Login Attempt | ||
| ===================================================== */ | ||
|
|
||
| if (!empty($_POST['loginpassword'])) { | ||
| if (!empty($_POST['loginpassword']) && | ||
| isset($_POST['csrf_token']) && | ||
| hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf_token'])) { | ||
|
|
||
| $incomingHash = hash('sha256', $_POST['loginpassword']); | ||
| if ($ldap_enabled) { | ||
| // LDAP path: delegate credential validation to the Python API. | ||
| // The API token is required so only server-side callers can reach the endpoint. | ||
| if (empty($api_token)) { | ||
| throw new RuntimeException('API_TOKEN is not configured'); | ||
| } | ||
|
|
||
| if (hash_equals($nax_Password, $incomingHash)) { | ||
| $ldap_payload = json_encode([ | ||
| 'username' => isset($_POST['loginusername']) ? trim($_POST['loginusername']) : '', | ||
| 'password' => $_POST['loginpassword'], | ||
| ]); | ||
| $stream_opts = [ | ||
| 'http' => [ | ||
| 'method' => 'POST', | ||
| 'header' => "Content-Type: application/json\r\n" | ||
| . "Authorization: Bearer " . $api_token . "\r\n" | ||
| . "X-Forwarded-For: " . ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1') . "\r\n", | ||
| 'content' => $ldap_payload, | ||
| 'timeout' => 5, | ||
| 'ignore_errors' => true, | ||
| ] | ||
| ]; | ||
| $ctx = stream_context_create($stream_opts); | ||
| $raw = @file_get_contents($ldap_login_url, false, $ctx); | ||
| $api_resp = ($raw !== false) ? @json_decode($raw, true) : null; | ||
|
|
||
| if (is_array($api_resp) && $api_resp['success'] === true) { | ||
| login_user(); | ||
| safe_redirect(append_hash($redirectTo)); | ||
| } | ||
| // Fall through to show the login form with an error state. | ||
| } else { | ||
| // Local path: compare SHA-256 digest against the stored hash (same as before). | ||
| $incomingHash = hash('sha256', $_POST['loginpassword']); | ||
|
|
||
| login_user(); | ||
| if (hash_equals($nax_Password, $incomingHash)) { | ||
| login_user(); | ||
|
|
||
| // Redirect to target page, preserving deep link hash if present | ||
| safe_redirect(append_hash($redirectTo)); | ||
| // Redirect to target page, preserving deep link hash if present | ||
| safe_redirect(append_hash($redirectTo)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -179,6 +254,9 @@ function logout_user(): void { | |
| <!-- Favicon --> | ||
| <link id="favicon" rel="icon" type="image/x-icon" href="img/NetAlertX_logo.png"> | ||
| <link rel="stylesheet" href="/css/offline-font.css"> | ||
|
|
||
| <!-- NetAlertX CSS --> | ||
| <link rel="stylesheet" href="css/app.css"> | ||
| </head> | ||
| <body class="hold-transition login-page col-sm-12 col-sx-12"> | ||
| <div class="login-box login-custom"> | ||
|
|
@@ -193,8 +271,19 @@ function logout_user(): void { | |
| ? '?next=' . htmlspecialchars($_GET['next'], ENT_QUOTES, 'UTF-8') | ||
| : ''; | ||
| ?>" method="post"> | ||
| <?php if ($ldap_enabled): ?> | ||
| <div class="form-group has-feedback"> | ||
| <input type="text" class="form-control" | ||
| placeholder="<?= lang('Login_Username');?>" | ||
| name="loginusername" | ||
| autocomplete="username" | ||
| required> | ||
| <span class="glyphicon glyphicon-user form-control-feedback"></span> | ||
| </div> | ||
| <?php endif; ?> | ||
| <div class="form-group has-feedback"> | ||
| <input type="hidden" name="url_hash" id="url_hash"> | ||
| <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>"> | ||
| <input type="password" class="form-control" placeholder="<?= lang('Login_Psw-box');?>" name="loginpassword"> | ||
| <span class="glyphicon glyphicon-lock form-control-feedback"></span> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -135,7 +135,7 @@ function isAppInitialized() { | |
| } | ||
|
|
||
| // check if all required languages chached | ||
| if(parseInt(getCache(CACHE_KEYS.STRINGS_COUNT)) != lang_shouldBeCompletedCalls) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why was this changed? |
||
| if(parseInt(getCache(CACHE_KEYS.STRINGS_COUNT)) < lang_shouldBeCompletedCalls) | ||
| { | ||
| _isAppInitLog(`[isAppInitialized] waiting on cacheStrings: ${getCache(CACHE_KEYS.STRINGS_COUNT)} of ${lang_shouldBeCompletedCalls}`); | ||
| return false; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -211,6 +211,11 @@ | |||||||||||||
| "Device_NoData_Scanning": "Waiting for the first scan - this may take several minutes after the initial setup.", | ||||||||||||||
| "Device_NoData_Title": "No devices found yet", | ||||||||||||||
| "Device_NoMatch_Title": "No devices match the current filter", | ||||||||||||||
| "LDAP library not available": "LDAP library not available", | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lang keys should have no spaces - not tested - replace spaces with underscores _ - @coderabbitai - add learning
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Any PHP, Python, or JS code referencing these keys by the old names (e.g. in ✏️ Learnings added
🧠 Learnings used |
||||||||||||||
| "LDAP server not configured": "LDAP server not configured", | ||||||||||||||
| "User not found": "User not found", | ||||||||||||||
| "User not found or search failed": "User not found or search failed", | ||||||||||||||
| "Username and password are required": "Username and password are required", | ||||||||||||||
| "Device_Save_Failed": "Failed to save device", | ||||||||||||||
| "Device_Save_Unauthorized": "Unauthorized - invalid API token", | ||||||||||||||
| "Device_Saved_Success": "Device saved successfully", | ||||||||||||||
|
|
@@ -398,6 +403,7 @@ | |||||||||||||
| "Login_Remember": "Remember", | ||||||||||||||
| "Login_Remember_small": "(valid for 7 days)", | ||||||||||||||
| "Login_Submit": "Log in", | ||||||||||||||
| "Login_Username": "Username", | ||||||||||||||
| "Login_Toggle_Alert_headline": "Password Alert!", | ||||||||||||||
| "Login_Toggle_Info": "Password Information", | ||||||||||||||
| "Login_Toggle_Info_headline": "Password Information", | ||||||||||||||
|
|
||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.