diff --git a/README.md b/README.md
index 5a3ffd5..16459fc 100644
--- a/README.md
+++ b/README.md
@@ -12,31 +12,42 @@ Installation
Usage
-----
-When yourls-cas-plugin is enabled and user was not successfuly authenticated using data specified in yourls_user_passwords, an LDAP authentication attempt will be made. If LDAP authentication is successful, then you will immediately go to the admin interface.
+When yourls-ldap-plugin is enabled and user was not successfuly authenticated using data specified in yourls_user_passwords, an LDAP authentication attempt will be made. If LDAP authentication is successful, then you will immediately go to the admin interface.
-You can also set a privileged account to search the LDAP directory with. This is useful for directories that don't allow anonymous binding.
+You can also set a privileged account to search the LDAP directory with. This is useful for directories that don't allow anonymous binding. If you define a suitable template, the current user will be used binding. This is useful for Active Directory / Samba.
Setting the groups settings will check the user is a member of that group before logging them in and storing their credentials. This check is only performed the first time they auth or when their password changes.
+yourls-ldap-plugin by default will now implement a simple cache of LDAP users. As well as reducing requests to the LDAP server this has the effect of allowing YOURLS API to work with LDAP users.
+
Configuration
-------------
- * define( 'LDAPAUTH_HOST', 'ldaps://ldap.domain.com' ) // LDAP host name, IP or URL. You can use ldaps://host for LDAP with TLS
- * define( 'LDAPAUTH_PORT', '636' ) // LDAP server port - often 389 or 636 for TLS (LDAPS)
- * define( 'LDAPAUTH_BASE', 'dc=domain,dc=com' ) // Base DN (location of users)
- * define( 'LDAPAUTH_USERNAME_FIELD', 'uid') // (optional) LDAP field name in which username is store
+ * define( 'LDAPAUTH_HOST', 'ldaps://ldap.domain.com' ); // LDAP host name, IP or URL. You can use ldaps://host for LDAP with TLS
+ * define( 'LDAPAUTH_PORT', '636' ); // LDAP server port - often 389 or 636 for TLS (LDAPS)
+ * define( 'LDAPAUTH_BASE', 'dc=domain,dc=com' ); // Base DN (location of users)
+ * define( 'LDAPAUTH_USERNAME_FIELD', 'uid'); // (optional) LDAP field name in which username is store
To use a privileged account for the user search:
- * define( 'LDAPAUTH_SEARCH_USER', 'cn=your-user,dc=domain,dc=com' ) // (optional) Privileged user to search with
- * define( 'LDAPAUTH_SEARCH_PASS', 'the-pass') // (optional) (only if LDAPAUTH_SEARCH_USER set) Privileged user pass
+ * define( 'LDAPAUTH_SEARCH_USER', 'cn=your-user,dc=domain,dc=com' ); // (optional) Privileged user to search with
+ * define( 'LDAPAUTH_SEARCH_PASS', 'the-pass'); // (optional) (only if LDAPAUTH_SEARCH_USER set) Privileged user pass
+
+To define a template to bind using the current user for the search: Use %s as the place holder for the current user name
+ * define( 'LDAPAUTH_BIND_WITH_USER_TEMPLATE', '%s@myad.domain' ); // (optional) Use %s as the place holder for the current user name
To check group membership before authenticating:
- * define( 'LDAPAUTH_GROUP_ATTR', 'memberof' ) // (optional) LDAP groups attr
- * define( 'LDAPAUTH_GROUP_REQ', 'the-group;another-admin-group') // (only if LDAPAUTH_GROUP_REQ set) Group/s user must be in. Allows multiple, semicolon delimited
+ * define( 'LDAPAUTH_GROUP_ATTR', 'memberof' ); // (optional) LDAP groups attr
+ * define( 'LDAPAUTH_GROUP_REQ', 'the-group;another-admin-group'); // (only if LDAPAUTH_GROUP_REQ set) Group/s user must be in. Allows multiple, semicolon delimited
+
+To define the scope of group req search:
+ * define( 'LDAPAUTH_GROUP_SCOP', 'sub' ); // if not defined the default is 'sub', and will check for the user in all the subtree. The other option is 'base', that will search only members of the exactly req
+
+To define the type of user cache used:
+ * define( 'LDAPAUTH_USERCACHE_TYPE', 0); // (optional) Defaults to 1, which caches users in the options table. 0 turns off cacheing. Other values are currently undefined, but may be one day
To automatically add LDAP users to config.php:
- * define( 'LDAPAUTH_ADD_NEW', true ) // (optional) Add LDAP users to config.php
-NOTE: This will require config.php to be writable by your webserver user
+ * define( 'LDAPAUTH_ADD_NEW', true ); // (optional) Add LDAP users to config.php
+NOTE: This will require config.php to be writable by your webserver user. This function is now largely unneeded because the database based cache offers similar benefits without the need to make config.php writeable. It is retained for backwards compatability
Troubleshooting
---------------
@@ -44,9 +55,20 @@ Troubleshooting
* Check your webserver logs
* You can try modifying plugin code to print some more debug info
+About the user cache
+--------------------
+When a successful login is made against an LDAP server the plugin will cache the username and encrypted password. Currently this is done by saving them in an array in the YOURLS options table. This has some advantages:
+
+ * It reduces requests to the LDAP server
+ * It means that users can still log in even if the LDAP server is unreachable
+ * It means that the YOURLS API can be used by LDAP users
+
+Unfortunately, the cache will not scale well. This is because it integrates tightly with YOURLS's internal auth mechanism, and that does not scale. If you have a few tens of LDAP users likely to use your YOURLS installation it should be fine. Much more than that and you may see performance issues. If so, you should probably disable the cache. This will mean
+that your LDAP users will not be able to use the API. At least not unless they are also listed in users/config.php, which suffers from the same scaling problems.
+
License
-------
-Copyright 2013 K3A
+Copyright 2013 K3A, #1davoaust
Copyright 2013 Nicholas Waller (code@nicwaller.com) as I used some parts of his CAS authentication plugin :)
This program is free software: you can redistribute it and/or modify
diff --git a/plugin.php b/plugin.php
index 26285ba..6b55d9e 100644
--- a/plugin.php
+++ b/plugin.php
@@ -12,11 +12,12 @@ Author URI: http://k3a.me
// No direct call
if( !defined( 'YOURLS_ABSPATH' ) ) die();
+
// returns true if the environment is set up right
function ldapauth_environment_check() {
$required_params = array(
'LDAPAUTH_HOST', // ldap host
- //'LDAAUTHP_PORT', // ldap port
+ //'LDAPAUTH_PORT', // ldap port
'LDAPAUTH_BASE', // base ldap path
//'LDAPAUTH_USERNAME_FIELD', // field to check the username against
);
@@ -40,6 +41,10 @@ function ldapauth_environment_check() {
if ( !defined( 'LDAPAUTH_ADD_NEW' ) )
define( 'LDAPAUTH_ADD_NEW', false );
+
+ if ( !defined( 'LDAPAUTH_USERCACHE_TYPE' ) )
+ define( 'LDAPAUTH_USERCACHE_TYPE', 1 );
+
global $ldapauth_authorized_admins;
if ( !isset( $ldapauth_authorized_admins ) ) {
@@ -57,21 +62,41 @@ yourls_add_filter( 'is_valid_user', 'ldapauth_is_valid_user' );
// returns true/false
function ldapauth_is_valid_user( $value ) {
- // doesn't work for API...
- if (yourls_is_API())
- return $value;
+ global $yourls_user_passwords;
+ global $ydb;
- @session_start();
-
// Always check & set early
if ( !ldapauth_environment_check() ) {
die( 'Invalid configuration for YOURLS LDAP plugin. Check PHP error log.' );
}
- if ( isset( $_SESSION['LDAPAUTH_AUTH_USER'] ) ) {
+ if( LDAPAUTH_USERCACHE_TYPE == 1) {
+ $ldapauth_usercache = $ydb->option['ldapauth_usercache'];
+ }
+
+ // no point in continuing if the user has already been validated by core
+ if ($value) {
+ ldapauth_debug("Returning from ldapauth_is_valid_user as user is already validated");
+ return $value;
+ }
+
+ // session is only needed if we don't use usercache
+ if (empty(LDAPAUTH_USERCACHE_TYPE)) {
+ @session_start();
+ }
+
+ if ( empty(LDAPAUTH_USERCACHE_TYPE) && isset( $_SESSION['LDAPAUTH_AUTH_USER'] ) ) {
// already authenticated...
$username = $_SESSION['LDAPAUTH_AUTH_USER'];
- if ( ldapauth_is_authorized_user( $username ) ) {
+ // why is this checked here, but not before the cookie is set?
+ if ( ldapauth_is_authorized_user( $username ) ) {
+ if( !isset($yourls_user_passwords[$username]) ) {
+ // set a dummy password to work around the "Stealing cookies" problem
+ // we prepend with 'phpass:' to avoid YOURLS trying to auto-encrypt it and
+ // write it to user/config.php
+ ldapauth_debug('Setting dummy entry in $yourls_user_passwords for user ' . $username);
+ $yourls_user_passwords[$username]='phpass:ThereIsNoPasswordButHey,WhoCares?';
+ }
yourls_set_user( $_SESSION['LDAPAUTH_AUTH_USER'] );
return true;
} else {
@@ -84,14 +109,24 @@ function ldapauth_is_valid_user( $value ) {
$ldapConnection = ldap_connect(LDAPAUTH_HOST, LDAPAUTH_PORT);
if (!$ldapConnection) die("Cannot connect to LDAP " . LDAPAUTH_HOST);
ldap_set_option($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3);
+ //ldap_set_option($ldapConnection, LDAP_OPT_REFERRALS, 0);
- // Check if using a privileged user account to search
- if (defined('LDAPAUTH_SEARCH_USER') && defined('LDAPAUTH_SEARCH_PASS')) {
+ // should we to try and bind using the credentials being logged in with?
+ if (defined('LDAPAUTH_BIND_WITH_USER_TEMPLATE')) {
+ $bindRDN = sprintf(LDAPAUTH_BIND_WITH_USER_TEMPLATE, $_REQUEST['username']);
+ if (!($ldapSuccess = @ldap_bind($ldapConnection, $bindRDN, $_REQUEST['password']))) {
+ error_log('Couldn\'t bind to LDAP server with user ' . $bindRDN);
+ return $value;
+ }
+ }
+
+ // Check if using a privileged user account to search - only if not already bound with current user
+ if (defined('LDAPAUTH_SEARCH_USER') && defined('LDAPAUTH_SEARCH_PASS') && empty($ldapSuccess)) {
if (!@ldap_bind($ldapConnection, LDAPAUTH_SEARCH_USER, LDAPAUTH_SEARCH_PASS)) {
die('Couldn\'t bind search user ' . LDAPAUTH_SEARCH_USER);
}
}
-
+
// Limit the attrs to the ones we need
$attrs = array('dn', LDAPAUTH_USERNAME_FIELD);
if (defined('LDAPAUTH_GROUP_ATTR'))
@@ -102,37 +137,53 @@ function ldapauth_is_valid_user( $value ) {
$searchResult = ldap_get_entries($ldapConnection, $searchDn);
if (!$searchResult) return $value;
$userDn = $searchResult[0]['dn'];
- if (!$userDn) return $value;
- $ldapSuccess = @ldap_bind($ldapConnection, $userDn, $_REQUEST['password']);
- @ldap_close($ldapConnection);
-
+ if (!$userDn && !$ldapSuccess) return $value;
+ if (empty($ldapSuccess)) { // we don't need to do this if we already bound using username and LDAPAUTH_BIND_WITH_USER_TEMPLATE
+ $ldapSuccess = @ldap_bind($ldapConnection, $userDn, $_REQUEST['password']);
+ }
+
// success?
if ($ldapSuccess)
{
// are we checking group auth?
if (defined('LDAPAUTH_GROUP_ATTR') && defined('LDAPAUTH_GROUP_REQ')) {
- if (!array_key_exists(LDAPAUTH_GROUP_ATTR, $searchResult[0])) die('Not in any LDAP groups');
-
- $in_group = false;
- $groups_to_check = explode(";", strtolower(LDAPAUTH_GROUP_REQ)); // This is now an array
-
- foreach($searchResult[0][LDAPAUTH_GROUP_ATTR] as $grps) {
- if (in_array(strtolower($grps), $groups_to_check)) { $in_group = true; break; }
- }
-
- if (!$in_group) die('Not in admin group');
+
+ $in_group = false;
+ $bind = ldap_bind($ldapConnection, LDAPAUTH_SEARCH_USER, LDAPAUTH_SEARCH_PASS);
+
+ $groups_to_check = explode(";", strtolower(LDAPAUTH_GROUP_REQ)); // This is now an array
+ foreach($groups_to_check as $group){
+ $searchGroup = ldap_search($ldapConnection, $group, LDAPAUTH_GROUP_ATTR . "=" . $_REQUEST['username']);
+ $searchG = ldap_get_entries($ldapConnection,$searchGroup);
+ if ( LDAPAUTH_GROUP_SCOP == 'base'){
+ if ($searchG[0]['dn'] == $group) $in_group = true;
+ }
+ else{
+ if ($searchG[0]['dn']) $in_group = true;
+ }
+ }
+ if (!$in_group) die('Not in admin group');
}
- $username = $searchResult[0][LDAPAUTH_USERNAME_FIELD][0];
+ // attribute index returned by ldap_get_entries is lowercased (http://php.net/manual/en/function.ldap-get-entries.php)
+ $username = $searchResult[0][strtolower(LDAPAUTH_USERNAME_FIELD)][0];
yourls_set_user($username);
- global $yourls_user_passwords;
if (LDAPAUTH_ADD_NEW && !array_key_exists($username, $yourls_user_passwords)) {
ldapauth_create_user( $username, $_REQUEST['password'] );
}
+ if (LDAPAUTH_USERCACHE_TYPE == 1) {
+ // store the current user credentials in our cache. This cuts down calls to the LDAP
+ // server, and allows API keys to work with LDAP users
+ $ldapauth_usercache[$username] = 'phpass:' . ldapauth_hash_password($_REQUEST['password']);
+ yourls_update_option('ldapauth_usercache', $ldapauth_usercache);
+ }
+
$yourls_user_passwords[$username] = ldapauth_hash_password($_REQUEST['password']);
- $_SESSION['LDAPAUTH_AUTH_USER'] = $username;
+ if (empty(LDAPAUTH_USERCACHE_TYPE)) {
+ $_SESSION['LDAPAUTH_AUTH_USER'] = $username;
+ }
return true;
} else {
error_log("No LDAP success");
@@ -162,10 +213,31 @@ function ldapauth_is_authorized_user( $username ) {
yourls_add_action( 'logout', 'ldapauth_logout_hook' );
function ldapauth_logout_hook( $args ) {
- unset($_SESSION['LDAPAUTH_AUTH_USER']);
- setcookie('PHPSESSID', '', 0, '/');
+ if (empty(LDAPAUTH_USERCACHE_TYPE)) {
+ unset($_SESSION['LDAPAUTH_AUTH_USER']);
+ setcookie('PHPSESSID', '', 0, '/');
+ }
}
+/* This action, called as early as possible, retrieves our cache of LDAP users and
+ * merges it with $yourls_user_passwords. This enables core to do the authorisation
+ * of previously seen LDAP users, and also means that API signatures for LDAP users
+ * will work. Users that exist in both users/config.php and LDAP will need to use
+ * their LDAP passwords
+ */
+
+yourls_add_action ('plugins_loaded', 'ldapauth_merge_users');
+function ldapauth_merge_users() {
+ global $ydb;
+ global $yourls_user_passwords;
+ if ( !ldapauth_environment_check() ) {
+ die( 'Invalid configuration for YOURLS LDAP plugin. Check PHP error log.' );
+ }
+ if(LDAPAUTH_USERCACHE_TYPE==1 && isset($ydb->option['ldapauth_usercache'])) {
+ ldapauth_debug("Merging text file users and cached LDAP users");
+ $yourls_user_passwords = array_merge($yourls_user_passwords, $ydb->option['ldapauth_usercache']);
+ }
+}
/**
* Create user in config file
* Code reused from yourls_hash_passwords_now()
@@ -198,7 +270,6 @@ function ldapauth_create_user( $user, $new_password ) {
return $pass_hash;
}
-
/**
* Hashes password the same way as yourls_hash_passwords_now()
**/
@@ -209,3 +280,8 @@ function ldapauth_hash_password ($password) {
return $pass_hash;
}
+function ldapauth_debug ($msg) {
+ if (defined('LDAPAUTH_DEBUG') && LDAPAUTH_DEBUG) {
+ error_log("yourls_ldap_auth: " . $msg);
+ }
+}