From 447dd22d9d1af1eea7b9a0a0cc05e3250008c663 Mon Sep 17 00:00:00 2001 From: Peter Stamfest Date: Mon, 27 Apr 2020 20:05:01 +0200 Subject: [PATCH 1/5] * Provide a login form to use with LDAP This uses an extra LDAP bind operation to do authentication. Because it does not rely on HTTP basic authentication I had to introduce a session (probably resulting in cookies). --- README.md | 6 +++- auth.php | 63 ++++++++++++++++++++++++++++++++++++++++ config/config-sample.ini | 8 +++++ ldap.php | 37 +++++++++++++++++++++++ pagesection.php | 26 ++++++++++++----- public_html/style.css | 4 +++ requesthandler.php | 16 ++++++---- templates/login.php | 41 ++++++++++++++++++++++++++ views/login.php | 26 +++++++++++++++++ 9 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 auth.php create mode 100644 templates/login.php create mode 100644 views/login.php diff --git a/README.md b/README.md index 6524b6c..923947d 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,11 @@ Installation [Full nginx server example](https://github.com/operasoftware/dns-ui/wiki/Example-configuration:-nginx) -5. Set up an authentication module for your virtual host (eg. authnz_ldap for Apache). +5. Set up authentication + + * Either using the old-style way using an authentication module for your virtual host (eg. authnz_ldap for Apache). + + * Or using HTML form-based authentication using LDAP by setting form_based = "ldap" in config.ini and enabling and configuring LDAP there as well. 6. Copy the file `config/config-sample.ini` to `config/config.ini` and edit the settings as required. diff --git a/auth.php b/auth.php new file mode 100644 index 0000000..e1afa91 --- /dev/null +++ b/auth.php @@ -0,0 +1,63 @@ +auth($_POST['username'], $_POST['password'], + $config['ldap']['user_id'], $config['ldap']['dn_user'])) { + $_SESSION['loggedin'] = true; + $_SESSION['user'] = $_POST['username']; + require('views/home.php'); + die; + } else { + error_log("Failed login attempt for user '" . $_POST['username'] . "'"); + } + } + } + + if (! $_SESSION['loggedin']) { + require('views/login.php'); + die; + } + } + if (isset($_SESSION['loggedin']) && $_SESSION['loggedin']) { + if ($relative_request_url == '/logout' ) { + $_SESSION['loggedin'] = false; + $_SESSION['user'] = null; + require('views/home.php'); + die; + } + + $active_user = $user_dir->get_user_by_uid($_SESSION['user']); + } +} + diff --git a/config/config-sample.ini b/config/config-sample.ini index ec9b5df..5056a94 100644 --- a/config/config-sample.ini +++ b/config/config-sample.ini @@ -34,6 +34,14 @@ password = password ; compare the user ID's case? (on by default) user_case_sensitive = 1 +; Set this to "ldap" to enable HTML form-based login to dns-ui. Do not forget +; to complete LDAP configuration in the [ldap] section below. Also make sure +; that php_auth is NOT enabled at the same time. +; If you enable this you usually MUST NOT configure authentication on the +; webserver itself. +; form_based = "ldap" +form_based = false + [php_auth] enabled = 0 admin_group = "systems" diff --git a/ldap.php b/ldap.php index 2b033ed..39e2857 100644 --- a/ldap.php +++ b/ldap.php @@ -46,6 +46,43 @@ private function connect() { } } + public function auth($uid, $pass, $user_id_attr, $basedn) { + if(is_null($this->conn)) $this->connect(); + $filter = sprintf("(%s=%s)", LDAP::escape($user_id_attr), LDAP::escape($uid)); + $r = @ldap_search($this->conn, $basedn, $filter); + + if(! $r) { + return false; + } + + // Fetch entries + $result = @ldap_get_entries($this->conn, $r); + + if ($result['count'] != 1) { + return false; + } + + $authdn = $result[0]['dn']; + + $authconn = ldap_connect($this->host); + if($authconn === false) throw new LDAPConnectionFailureException('Invalid LDAP connection settings'); + if($this->starttls) { + if(!ldap_start_tls($authconn)) throw new LDAPConnectionFailureException('Could not initiate TLS connection to LDAP server'); + } + foreach($this->options as $option => $value) { + ldap_set_option($authconn, $option, $value); + } + + try { + $bound = @ldap_bind($authconn, $authdn, $pass); + return $bound; + } catch (Exception $e) { + return false; + } finally { + @ldap_unbind($authconn); + } + } + public function search($basedn, $filter, $fields = array(), $sort = array()) { if(is_null($this->conn)) $this->connect(); if(empty($fields)) $r = @ldap_search($this->conn, $basedn, $filter); diff --git a/pagesection.php b/pagesection.php index 31912df..9e539d6 100644 --- a/pagesection.php +++ b/pagesection.php @@ -27,13 +27,25 @@ public function __construct($template) { $this->template = $template; $this->data = new StdClass; $this->data->menu_items = array(); - $this->data->menu_items['Zones'] = '/zones'; - if(is_object($active_user) && $active_user->admin) { - $this->data->menu_items['Templates'] = array(); - $this->data->menu_items['Templates']['SOA templates'] = '/templates/soa'; - $this->data->menu_items['Templates']['Nameserver templates'] = '/templates/ns'; - $this->data->menu_items['Users'] = '/users'; - $this->data->menu_items['Settings'] = '/settings'; + + $add_menu_items = true; + if ($config['authentication']['form_based']) { + /* Do NOT add any menu items if we have not been authenticated */ + $add_menu_items = is_form_authenticated(); + } + + if ($add_menu_items) { + $this->data->menu_items['Zones'] = '/zones'; + if(is_object($active_user) && $active_user->admin) { + $this->data->menu_items['Templates'] = array(); + $this->data->menu_items['Templates']['SOA templates'] = '/templates/soa'; + $this->data->menu_items['Templates']['Nameserver templates'] = '/templates/ns'; + $this->data->menu_items['Users'] = '/users'; + $this->data->menu_items['Settings'] = '/settings'; + } + if ($config['authentication']['form_based']) { + $this->data->menu_items['Log out'] = '/logout'; + } } $this->data->relative_request_url = $relative_request_url; $this->data->active_user = $active_user; diff --git a/public_html/style.css b/public_html/style.css index a559bc0..c948bdb 100644 --- a/public_html/style.css +++ b/public_html/style.css @@ -220,6 +220,10 @@ div.stickyHeader th { background-color: white; } +input.authbox { + width: auto; +} + /** * GeSHi (C) 2004 - 2007 Nigel McNie, 2007 - 2008 Benny Baumann * (http://qbnz.com/highlighter/ and http://geshi.org/) diff --git a/requesthandler.php b/requesthandler.php index 26fca44..c75c3db 100644 --- a/requesthandler.php +++ b/requesthandler.php @@ -20,17 +20,21 @@ ob_start(); set_exception_handler('exception_handler'); -if(isset($_SERVER['PHP_AUTH_USER'])) { - $active_user = $user_dir->get_user_by_uid($_SERVER['PHP_AUTH_USER']); -} else { - throw new Exception("Not logged in."); -} - // Work out where we are on the server $request_url = preg_replace('|(.)/$|', '$1', $_SERVER['REQUEST_URI']); $relative_request_url = preg_replace('/^'.preg_quote($relative_frontend_base_url, '/').'/', '', $request_url) ?: '/'; $absolute_request_url = $frontend_root_url.$request_url; +if ($config['authentication']['form_based']) { + require('auth.php'); +} else { + if(isset($_SERVER['PHP_AUTH_USER'])) { + $active_user = $user_dir->get_user_by_uid($_SERVER['PHP_AUTH_USER']); + } else { + throw new Exception("Not logged in."); + } +} + if(empty($config['web']['enabled'])) { require('views/error503.php'); die; diff --git a/templates/login.php b/templates/login.php new file mode 100644 index 0000000..6b04ae6 --- /dev/null +++ b/templates/login.php @@ -0,0 +1,41 @@ + +
+
+ Login +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+
diff --git a/views/login.php b/views/login.php new file mode 100644 index 0000000..31f546f --- /dev/null +++ b/views/login.php @@ -0,0 +1,26 @@ +set('title', 'Login'); +$page->set('content', $content); +$page->set('alerts', array()); + +echo $page->generate(); + From 47ede54fa8a83e8c4f3449c244b474b915db49cf Mon Sep 17 00:00:00 2001 From: Peter Stamfest Date: Tue, 28 Apr 2020 20:56:22 +0200 Subject: [PATCH 2/5] Check if the underlying LDAP object contains the required attributes and throw an exception if it doesn't. Previously the code just failed. --- model/user.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/model/user.php b/model/user.php index d570e3b..6036e1c 100644 --- a/model/user.php +++ b/model/user.php @@ -154,6 +154,12 @@ public function get_details_from_ldap() { $ldapusers = $this->ldap->search($config['ldap']['dn_user'], LDAP::escape($config['ldap']['user_id']).'='.LDAP::escape($this->uid), array_keys(array_flip($attributes))); if($ldapuser = reset($ldapusers)) { $this->auth_realm = 'LDAP'; + + foreach (array('user_id', 'user_name', 'user_email') as $key) { + if (!isset($ldapuser[strtolower($config['ldap'][$key])])) { + throw new UserNotFoundException(sprintf('User misses %s attribute in LDAP directory.', $config['ldap'][$key])); + } + } $this->uid = $ldapuser[strtolower($config['ldap']['user_id'])]; $this->name = $ldapuser[strtolower($config['ldap']['user_name'])]; $this->email = $ldapuser[strtolower($config['ldap']['user_email'])]; From 083ef49023599c319f4798181f3b2f2c838e07f4 Mon Sep 17 00:00:00 2001 From: Peter Stamfest Date: Tue, 28 Apr 2020 21:05:57 +0200 Subject: [PATCH 3/5] Improve form-based login and allow for configuration of session cookie - show login errors as alert on the login page --- auth.php | 73 +++++++++++++++++++++++++++++++++++----- config/config-sample.ini | 24 +++++++++++++ views/login.php | 8 ++++- 3 files changed, 95 insertions(+), 10 deletions(-) diff --git a/auth.php b/auth.php index e1afa91..cbb0526 100644 --- a/auth.php +++ b/auth.php @@ -25,21 +25,75 @@ function is_form_authenticated() { return false; } -if ($config['authentication']['form_based'] == "ldap") { - session_start(); +function dns_ui_start_session() { + global $config; + + $options = array(); + $options['use_strict_mode'] = true; + + if (isset($config['session'])) { + // allow to set some session options from configuration + $whitelisted = array('name', 'cookie_path', 'cookie_lifetime', 'cookie_secure'); + + foreach($config['session'] as $k => $v) { + if (array_search($k, $whitelisted) !== FALSE) { + $options[$k] = $v; + } + } + } + session_start($options); +} + +function auth_by_ldap($user, $pass) { + global $config; + global $ldap; + + if ( ! $config['ldap']['enabled']) { + error_log("Use of LDAP must be enabled to use LDAP form-based authentication"); + throw new Exception('Misconfiguration detected - check the error log'); + } + + return $ldap->auth($user, $pass, $config['ldap']['user_id'], $config['ldap']['dn_user']); +} + +if ($config['authentication']['form_based'] !== false) { + dns_ui_start_session(); if (! isset($_SESSION['loggedin']) || ! $_SESSION['loggedin']) { $_SESSION['loggedin'] = false; if (!empty($_POST) && $relative_request_url == '/login' ) { if (isset($_POST['username']) && isset($_POST['password'])) { - if ($ldap->auth($_POST['username'], $_POST['password'], - $config['ldap']['user_id'], $config['ldap']['dn_user'])) { - $_SESSION['loggedin'] = true; - $_SESSION['user'] = $_POST['username']; - require('views/home.php'); + $authed = false; + + try { + // other authentication methods could be implemented here... + if ($config['authentication']['form_based'] == "ldap") { + $authed = auth_by_ldap($_POST['username'], $_POST['password']); + } + + if ($authed) { + // OK, authenticated - but can we get user details??? + // if we can't this will throw an exception... + $active_user = $user_dir->get_user_by_uid($_POST['username']); + + $_SESSION['loggedin'] = true; + $_SESSION['user'] = $_POST['username']; + require('views/home.php'); + die; + } else { + error_log("Failed login attempt for user '" . $_POST['username'] . "'"); + } + } catch (Exception $e) { + $_SESSION['loggedin'] = false; + $_SESSION['user'] = null; + + error_log($e); + $alert = new UserAlert; + $alert->content = sprintf('Login failed: %s', $e->getMessage()); + $login_alerts = array($alert); + + require('views/login.php'); die; - } else { - error_log("Failed login attempt for user '" . $_POST['username'] . "'"); } } } @@ -49,6 +103,7 @@ function is_form_authenticated() { die; } } + if (isset($_SESSION['loggedin']) && $_SESSION['loggedin']) { if ($relative_request_url == '/logout' ) { $_SESSION['loggedin'] = false; diff --git a/config/config-sample.ini b/config/config-sample.ini index 5056a94..903358a 100644 --- a/config/config-sample.ini +++ b/config/config-sample.ini @@ -42,6 +42,30 @@ user_case_sensitive = 1 ; form_based = "ldap" form_based = false +[session] +; If form based authentication is enabled, the underlying cookie can be configured +; through this section. +; +; NOTE: if you are running multiple instances of the dns-ui on the same hostname +; under different paths, then consider to either change the cookie name for +; at least one instance and/or change the cookie_path for both instances +; +; the cookie name. +name = DNSUI + +; if you are running dns-ui under some path on your server then +; consider to specify this path here as well in order to avoid +; leaking the cookie to other applications +; cookie_path = / + +; the lifetime of the cookie in seconds. This causes "auto logout" +; after some time +cookie_lifetime = 10400 + +; if you are using HTTPS (you should) then consider to set this to true to +; avoid leaking the cookie over HTTP +; cookie_secure = true + [php_auth] enabled = 0 admin_group = "systems" diff --git a/views/login.php b/views/login.php index 31f546f..9f9ed66 100644 --- a/views/login.php +++ b/views/login.php @@ -20,7 +20,13 @@ $page = new PageSection('base'); $page->set('title', 'Login'); $page->set('content', $content); -$page->set('alerts', array()); + +$alerts = array(); +## Argh - what an ugly interface... +if (isset($login_alerts)) { + $alerts = $login_alerts; +} +$page->set('alerts', $alerts); echo $page->generate(); From 461a88fdda0e452f225db48d0473a14a32d733e5 Mon Sep 17 00:00:00 2001 From: Peter Stamfest Date: Tue, 5 May 2020 18:21:27 +0200 Subject: [PATCH 4/5] Add extra_user_filter to [ldap] configuration This allows to filter for users even before a bind attempt is done through the login form. This improves security by allowing to use LDAP filtering to identify eligible users at a very early stage. --- auth.php | 20 +++++++++++++++----- config/config-sample.ini | 19 +++++++++++++++++++ ldap.php | 7 ++++++- model/user.php | 8 +++++++- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/auth.php b/auth.php index cbb0526..1d69483 100644 --- a/auth.php +++ b/auth.php @@ -53,7 +53,12 @@ function auth_by_ldap($user, $pass) { throw new Exception('Misconfiguration detected - check the error log'); } - return $ldap->auth($user, $pass, $config['ldap']['user_id'], $config['ldap']['dn_user']); + return $ldap->auth($user, $pass, + $config['ldap']['user_id'], $config['ldap']['dn_user'], + isset($config['ldap']['extra_user_filter']) + ? $config['ldap']['extra_user_filter'] + : null + ); } if ($config['authentication']['form_based'] !== false) { @@ -76,10 +81,15 @@ function auth_by_ldap($user, $pass) { // if we can't this will throw an exception... $active_user = $user_dir->get_user_by_uid($_POST['username']); - $_SESSION['loggedin'] = true; - $_SESSION['user'] = $_POST['username']; - require('views/home.php'); - die; + if(!$active_user->active) { + // user is no longer active. Behave as if login failed + error_log("Login attempt by inactive user '" . $_POST['username'] . "'"); + } else { + $_SESSION['loggedin'] = true; + $_SESSION['user'] = $_POST['username']; + require('views/home.php'); + die; + } } else { error_log("Failed login attempt for user '" . $_POST['username'] . "'"); } diff --git a/config/config-sample.ini b/config/config-sample.ini index 903358a..adc6084 100644 --- a/config/config-sample.ini +++ b/config/config-sample.ini @@ -121,6 +121,25 @@ group_member_value = uid ; Members of admin_group are given full access to DNS UI web interface admin_group_cn = administrators +; +; An additional filter that is used when looking for an LDAP user. This is ANDed with +; the filter used to lookup the user id. This is very convenient if you have an LDAP +; than supports eg. the memberOf attribute to filter for specific groups. +; The idea is also to have a waterproof filter to avoid any possibility of allowing +; invalid users to authenticate through the form based login only to later get some +; access denied message, indirectly leaking user information or even allowing for a +; brute force authentication attack. +; +; Note that if a user is NOT found using this filter but exists in the local database +; it will be considered to be no longer active. +; +; Example: To make sure that only members of the dns-ui-admin or dns-ui-users groups can +; ever login one may use something like +; +; extra_user_filter = "(|(memberOf=cn=dns-ui-admins,ou=IT,o=ORG)(memberOf=cn=dns-ui-users,ou=IT,o=ORG))" +; +; extra_user_filter = + [powerdns] api_url = "http://localhost:8081/api/v1/servers/localhost" api_key = api_key diff --git a/ldap.php b/ldap.php index 39e2857..207bcf5 100644 --- a/ldap.php +++ b/ldap.php @@ -46,9 +46,14 @@ private function connect() { } } - public function auth($uid, $pass, $user_id_attr, $basedn) { + public function auth($uid, $pass, $user_id_attr, $basedn, $extrafilter) { if(is_null($this->conn)) $this->connect(); + $filter = sprintf("(%s=%s)", LDAP::escape($user_id_attr), LDAP::escape($uid)); + if ( isset($extrafilter) ) { + $filter = sprintf("(&%s%s)", $extrafilter, $filter); + } + $r = @ldap_search($this->conn, $basedn, $filter); if(! $r) { diff --git a/model/user.php b/model/user.php index 6036e1c..87a6945 100644 --- a/model/user.php +++ b/model/user.php @@ -151,7 +151,13 @@ public function get_details_from_ldap() { if(isset($config['ldap']['user_active'])) { $attributes[] = $config['ldap']['user_active']; } - $ldapusers = $this->ldap->search($config['ldap']['dn_user'], LDAP::escape($config['ldap']['user_id']).'='.LDAP::escape($this->uid), array_keys(array_flip($attributes))); + + $filter = sprintf("(%s=%s)", LDAP::escape($config['ldap']['user_id']), LDAP::escape($this->uid)); + if ( isset($config['ldap']['extra_user_filter']) ) { + $filter = sprintf("(&%s%s)", $config['ldap']['extra_user_filter'], $filter); + } + + $ldapusers = $this->ldap->search($config['ldap']['dn_user'], $filter, array_keys(array_flip($attributes))); if($ldapuser = reset($ldapusers)) { $this->auth_realm = 'LDAP'; From ca8a27e225c685d378fb99a8d0b0bcebfe79d1e1 Mon Sep 17 00:00:00 2001 From: Peter Stamfest Date: Mon, 31 Aug 2020 21:35:52 +0200 Subject: [PATCH 5/5] Just a white space change to be able to sign the commit. --- auth.php | 1 - 1 file changed, 1 deletion(-) diff --git a/auth.php b/auth.php index 1d69483..33f7f12 100644 --- a/auth.php +++ b/auth.php @@ -125,4 +125,3 @@ function auth_by_ldap($user, $pass) { $active_user = $user_dir->get_user_by_uid($_SESSION['user']); } } -