Skip to content

Commit

Permalink
Better passwords (#35)
Browse files Browse the repository at this point in the history
* Add support for stronger hashes (#34)

* Add generate_salt function

* Add suport for clear text passwords

If someone wants to shoot themselves in the foot, they are free to do it

* Add support for blowfish

* Add support for extended DES

* Add support for md5crypt

* Fix salt generation call

* Add support for sha256crypt

* Add support for sha512crypt

* Update previous functions

* Add a default cause

* Fix some shenanigans and log cleanup

* Couple minor fixes

* Let password hash checking be done in the password function

* Update the README with new passwords

* Change the default fallback to SSHA

* Put crypt algos in an array ordered by preference so we can fail to the most secure algo available

* Remove superfluous count++

* Updated password hashing code

Co-authored-by: Angelin01 <[email protected]>
  • Loading branch information
wheelybird and Angelin01 committed Aug 3, 2020
1 parent 761684b commit 4c5e337
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 43 deletions.
73 changes: 53 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
LDAP User Manager
--

A PHP web-based interface for LDAP user account management and self-service password change.
This is a PHP LDAP account manager; a web-based GUI interface which allows you to quickly populate a new LDAP directory and easily manage user accounts and groups. It also has a self-service password change module.
It's designed to work with OpenLDAP and to be run as a container. It complements OpenLDAP containers such as [*osixia/openldap*](https://hub.docker.com/r/osixia/openldap/).


Purpose
---

This presents a simple-to-use interface for setting up a new LDAP directory and managing user accounts and groups, as well as providing a way for users to change their own password. It's designed to complement OpenLDAP servers such as *osixia/openldap* (https://hub.docker.com/r/osixia/openldap/).
Features
---

* Setup wizard: this will create the necessary structure to allow you to add users and groups and will set up an initial admin user that can log into the user manager.
* Group creation and management.
Expand Down Expand Up @@ -35,6 +35,14 @@ Screenshots
![self_service_password_change](https://user-images.githubusercontent.com/17613683/59344258-9ffcab80-8d05-11e9-9dc2-27dfd373fcc8.png)


A note on your LDAP schema
---

By default this application will expect the LDAP server to be using the **RFC2307BIS** schema. OpenLDAP (including the **osixia/openldap** image) uses the old NIS schema as its default schema. The user manager will work with either, but RFC2307BIS is recommended as it allows you to use **memberOf** searches. You can enable RFC2307BIS in **osixia/openldap** by setting `LDAP_RFC2307BIS_SCHEMA` to `true` during the initial setup. The application is set to expect BIS by default for backwards-compatibility with older versions.

If you prefer not to use RFC2307BIS then set `LDAP_USES_NIS_SCHEMA` to `TRUE`. This will create groups solely as the **posixGroup** objectclass, and the default for `LDAP_GROUP_MEMBERSHIP_USES_UID` will `TRUE`.


Quick start
---

Expand All @@ -53,9 +61,9 @@ docker run \
-e "LDAP_ADMIN_BIND_PWD=secret"\
-e "LDAP_USES_NIS_SCHEMA=true" \
-e "EMAIL_DOMAIN=example.com"\
wheelybird/ldap-user-manager:v1.1
wheelybird/ldap-user-manager:v1.2
```
Now go to https://lum.example.com/setup.
Change the variable values to suit your environment. You might need to change `LDAP_USES_NIS_SCHEMA` if you're using the BIS schema. Now go to https://lum.example.com/setup.


Configuration
Expand All @@ -64,17 +72,7 @@ Configuration
Configuration is via environmental variables. Please bear the following in mind:

* This tool needs to bind to LDAP as a user with permissions to modify everything under the base DN.
* This interface is designed to work with a fresh LDAP server and should be used with populated LDAP directories with caution and at your own risk.

LDAP_USES_NIS_SCHEMA
----

By default this application will expect the LDAP server to be using the **RFC2307BIS** schema. OpenLDAP (including the **osixia/openldap** image) uses the old NIS schema as its default schema. The user manager will work with either, but RFC2307BIS is recommended as it allows you to use **memberOf** searches. You can enable RFC2307BIS in **osixia/openldap** by setting `LDAP_RFC2307BIS_SCHEMA` to `true` during the initial setup.

If you prefer not to use RFC2307BIS then set `LDAP_USES_NIS_SCHEMA` to `TRUE`. This will create groups solely as the **posixGroup** objectclass, and the default for `LDAP_GROUP_MEMBERSHIP_USES_UID` will `TRUE`. The application is set to expect the BIS schema by default for backwards-compatibility with older releases.



* This interface is designed to work with a fresh LDAP server and should be against populated LDAP directories with caution and at your own risk.

Mandatory:
----
Expand All @@ -93,15 +91,15 @@ Optional:

* `LDAP_USER_OU` (default: *people*): The name of the OU used to store user accounts (without the base DN appended).

* `LDAP_USES_NIS_SCHEMA` (default: *FALSE*): If you use the NIS schema instead of the (preferable) RFC2307BIS schema, set this to `TRUE`. See [LDAP_USES_NIS_SCHEMA](#LDAP_USES_NIS_SCHEMA) for more information.
* `LDAP_USES_NIS_SCHEMA` (default: *FALSE*): If you use the NIS schema instead of the (preferable) RFC2307BIS schema, set this to `TRUE`. See [A note on your LDAP schema](#a-note-on-your-ldap-schema) for more information.

* `LDAP_GROUP_OU` (default: *groups*): The name of the OU used to store groups (without the base DN appended).
* `LDAP_GROUP_MEMBERSHIP_ATTRIBUTE` (default: *memberUID* or *uniqueMember*): The attribute used when adding a user to a group. If `LDAP_USES_NIS_SCHEMA` is `TRUE` the default is `memberUID`, otherwise it's `uniqueMember`. Explicitly setting this variable will override the default.
* `LDAP_GROUP_MEMBERSHIP_USES_UID`(default: *TRUE* or *FALSE*): If *TRUE* then the entry for a member of a group will be just the username. Otherwise it's the member's full DN. If `LDAP_USES_NIS_SCHEMA` is `TRUE` the default is `TRUE`, otherwise it's `FALSE`. Explicitly setting this variable will override the default.

* `LDAP_REQUIRE_STARTTLS` (default: *TRUE*): If *TRUE* then a TLS connection is required for this interface to work. If set to *FALSE* then the interface will work without STARTTLS, but a warning will be displayed on the page.

* `LDAP_TLS_CACERT` (no default): If you need to use a specific CA certificate for TLS connections to the LDAP server (when `LDAP_REQUIRE_STARTTLS` is set) then assign the contents of the CA certificate to this variable. e.g. `-e LDAP_TLS_CERT=$(</path/to/ca.crt)`
* `LDAP_TLS_CACERT` (no default): If you need to use a specific CA certificate for TLS connections to the LDAP server (when `LDAP_REQUIRE_STARTTLS` is set) then assign the contents of the CA certificate to this variable. e.g. `-e LDAP_TLS_CACERT=$(</path/to/ca.crt)`

* `DEFAULT_USER_GROUP` (default: *everybody*): The group that new accounts are automatically added to when created. *NOTE*: If this group doesn't exist then a group is created with the same name as the username and the user is added to that group.
* `DEFAULT_USER_SHELL` (default: */bin/bash*): The shell that will be launched when the user logs into a server.
Expand All @@ -110,7 +108,7 @@ Optional:
* `USERNAME_FORMAT` (default: *{first_name}-{last_name}*): The template used to dynamically generate usernames. See [Username format](#username-format).
* `USERNAME_REGEX` (default: *^[a-z][a-zA-Z0-9\._-]{3,32}$*): The regular expression used to ensure a username (and group name) is valid. See [Username format](#username-format).

* `PASSWORD_HASH` (default: *SSHA*): Select which hashing method which will be used to store passwords in LDAP. Options are `MD5`, `SHA`, `SMD5`, `SSHA` or `CRYPT`.
* `PASSWORD_HASH` (no default): Select which hashing method which will be used to store passwords in LDAP. Options are (in order of precedence) `SHA512CRYPT`, `SHA256CRYPT`, `MD5CRYPT`, `SSHA`, `SHA`, `SMD5`, `MD5`, `CRYPT` & `CLEAR`. If your chosen method isn't available on your system then the strongest available method will be automatically selected - `SSHA` is the strongest method guaranteed to be available. Cleartext passwords should NEVER be used in any situation outside of a test.
* `ACCEPT_WEAK_PASSWORDS` (default: *FALSE*): Set this to *TRUE* to prevent a password being rejected for being too weak. The password strength indicators will still gauge the strength of the password. Don't enable this in a production environment.

* `LOGIN_TIMEOUT_MINS` (default: 10 minutes): How long before an idle session will be timed out.
Expand Down Expand Up @@ -164,3 +162,38 @@ Anything else in the `USERNAME_FORMAT` string is left as defined, but the userna

If `EMAIL_DOMAIN` is set then the email address field will be automatically updated in the form of `username@email_domain`. Entering anything manually in that field will stop the automatic update of the email field.


Testing with an LDAP container
--

This will set up an OpenLDAP container you can use to test the user manager against. It uses the BIS schema. This won't be using HTTPS or TLS, so don't use this in production.
```
docker run \
--detach \
--restart unless-stopped \
--name openldap \
-e LDAP_ORGANISATION=wheelybird \
-e LDAP_DOMAIN=wheelybird.com \
-e LDAP_ADMIN_PASSWORD=change_me \
-e LDAP_RFC2307BIS_SCHEMA=true \
-e LDAP_REMOVE_CONFIG_AFTER_SETUP=true \
-e LDAP_TLS=false \
-p 389:389
--volume /opt/docker/openldap/var_lib_ldap:/var/lib/ldap \
--volume /opt/docker/openldap/etc_ldap_slapd.d:/etc/ldap/slapd.d \
osixia/openldap:latest
docker run \
--detach \
--name=lum \
-p 80:80 \
-e SERVER_HOSTNAME=localhost \
-e LDAP_URI=ldap://172.17.0.1 \
-e LDAP_BASE_DN=dc=wheelybird,dc=com \
-e LDAP_ADMINS_GROUP=admins \
-e LDAP_ADMIN_BIND_DN="cn=admin,dc=wheelybird,dc=com" \
-e LDAP_ADMIN_BIND_PWD=change_me \
-e NO_HTTPS=TRUE \
wheelybird/ldap-user-manager:latest
```
Now go to http://localhost/setup - the password is `change_me` (unless you changed it).
2 changes: 1 addition & 1 deletion www/account_manager/new_user.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ function back_to_hidden(passwordField,confirmField) {
<div class="form-group" id="username_div">
<label for="username" class="col-sm-3 control-label">Username</label>
<div class="col-sm-6">
<input tabindex="3" type="text" class="form-control" id="username" name="username" <?php if (isset($username)){ print " value='$username'"; } ?> onkeyup="check_username_validity(document.getElementById('username').value); update_email();">
<input tabindex="3" type="text" class="form-control" id="username" name="username" <?php if (isset($username)){ print " value='$username'"; } ?> onkeyup="check_entity_name_validity(document.getElementById('username').value,'username_div'); update_email();">
</div>
</div>

Expand Down
3 changes: 1 addition & 2 deletions www/includes/config.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@
$USERNAME_REGEX = (getenv('USERNAME_REGEX') ? getenv('USERNAME_REGEX') : '^[a-z][a-zA-Z0-9\._-]{3,32}$');
#We'll use the username regex for groups too.

$PASSWORD_HASH = (getenv('PASSWORD_HASH') ? getenv('PASSWORD_HASH') : 'SSHA');
if ( ! in_array($PASSWORD_HASH, array('MD5','SMD5','SHA','SSHA','CRYPT'))) { $PASSWORD_HASH = 'SSHA'; }
if (getenv('PASSWORD_HASH')) { $PASSWORD_HASH = strtoupper(getenv('PASSWORD_HASH')); }

$ACCEPT_WEAK_PASSWORDS = ((strcasecmp(getenv('ACCEPT_WEAK_PASSWORDS'),'TRUE') == 0) ? TRUE : FALSE);

Expand Down
127 changes: 107 additions & 20 deletions www/includes/ldap_functions.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,40 +140,131 @@ function ldap_setup_auth($ldap_connection, $password) {
}


#################################

function generate_salt($length) {

$permitted_chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ./';

mt_srand((double)microtime() * 1000000);

$salt = '';
while (strlen($salt) < $length) {
$salt .= substr($permitted_chars, (rand() % strlen($permitted_chars)), 1);
}

return $salt;

}


##################################

function ldap_hashed_password($password) {

global $PASSWORD_HASH;
global $PASSWORD_HASH, $log_prefix;

$permitted_chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$salt = substr(str_shuffle($permitted_chars), 0, 64);
$check_algos = array (
"SHA512CRYPT" => "CRYPT_SHA512",
"SHA256CRYPT" => "CRYPT_SHA256",
# "BLOWFISH" => "CRYPT_BLOWFISH",
# "EXT_DES" => "CRYPT_EXT_DES",
"MD5CRYPT" => "CRYPT_MD5"
);

switch (strtoupper($PASSWORD_HASH)) {
$remaining_algos = array (
"SSHA",
"SHA",
"SMD5",
"MD5",
"CRYPT",
"CLEAR"
);

case 'MD5':
$hashed_pwd = '{MD5}' . base64_encode(md5($password,TRUE));
break;
$available_algos = array();

foreach ($check_algos as $algo_name => $algo_function) {
if (defined($algo_function) and constant($algo_function) != 0) {
array_push($available_algos, $algo_name);
}
else {
error_log("$log_prefix password hashing - the system doesn't support ${algo_name}");
}
}
$available_algos = array_merge($available_algos, $remaining_algos);

if (isset($PASSWORD_HASH)) {
if (!in_array($PASSWORD_HASH, $available_algos)) {
$hash_algo = $available_algos[0];
error_log("$log_prefix LDAP password: the chosen hash method ($PASSWORD_HASH) wasn't available");
}
else {
$hash_algo = $PASSWORD_HASH;
}
}
else {
$hash_algo = $available_algos[0];
}
error_log("$log_prefix LDAP password: using '${hash_algo}' as the hashing method");

$hash_algo = 'SSHA';

switch ($hash_algo) {

case 'SHA512CRYPT':
$hashed_pwd = '{CRYPT}' . crypt($password, '$6$' . generate_salt(8));
break;

case 'SHA256CRYPT':
$hashed_pwd = '{CRYPT}' . crypt($password, '$5$' . generate_salt(8));
break;

# Blowfish & EXT_DES didn't work
# case 'BLOWFISH':
# $hashed_pwd = '{CRYPT}' . crypt($password, '$2a$12$' . generate_salt(13));
# break;

# case 'EXT_DES':
# $hashed_pwd = '{CRYPT}' . crypt($password, '_' . generate_salt(8));
# break;

case 'MD5CRYPT':
$hashed_pwd = '{CRYPT}' . crypt($password, '$1$' . generate_salt(9));
break;

case 'SMD5':
$hashed_pwd = '{SMD5}' . base64_encode(md5($password.$salt,TRUE) . $salt);
break;
$salt = generate_salt(8);
$hashed_pwd = '{SMD5}' . base64_encode(md5($password . $salt, TRUE) . $salt);
break;

case 'MD5':
$hashed_pwd = '{MD5}' . base64_encode(md5($password, TRUE));
break;

case 'SHA':
$hashed_pwd = '{SHA}' . base64_encode(sha1($password,TRUE));
break;
$hashed_pwd = '{SHA}' . base64_encode(sha1($password, TRUE));
break;

case 'SSHA':
$hashed_pwd = '{SSHA}' . base64_encode(sha1($password.$salt,TRUE) . $salt);
break;
$salt = generate_salt(8);
$hashed_pwd = '{SSHA}' . base64_encode(sha1($password . $salt, TRUE) . $salt);
break;

case 'CRYPT':
$hashed_pwd = '{crypt}' . crypt($password, $salt);
break;
$salt = generate_salt(2);
$hashed_pwd = '{CRYPT}' . crypt($password, $salt);
break;

case 'CLEAR':
error_log("$log_prefix password hashing - WARNING - Saving password in cleartext. This is extremely bad practice and should never ever be done in a production environment.");
$hashed_pwd = $password;
break;


}

error_log("$log_prefix password update - algo $hash_algo | pwd $hashed_pwd");

return $hashed_pwd;

}
Expand Down Expand Up @@ -669,11 +760,7 @@ function ldap_change_password($ldap_connection,$username,$new_password) {
return FALSE;
}

#Hash password

$hashed_pass = ldap_hashed_password($new_password);

$entries["userPassword"] = $new_password;
$entries["userPassword"] = ldap_hashed_password($new_password);
$update = @ ldap_mod_replace($ldap_connection, $this_dn, $entries);

if ($update) {
Expand Down

0 comments on commit 4c5e337

Please sign in to comment.