From a55c337efd8e6482b0135e5263cbdccc74148e92 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 29 Dec 2018 21:06:27 +0100 Subject: [PATCH 1/8] refactoring --- lam/lib/ldap.inc | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/lam/lib/ldap.inc b/lam/lib/ldap.inc index a307477d..287786a6 100644 --- a/lam/lib/ldap.inc +++ b/lam/lib/ldap.inc @@ -3,7 +3,7 @@ $Id$ This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) - Copyright (C) 2003 - 2017 Roland Gruber + Copyright (C) 2003 - 2018 Roland Gruber This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -57,7 +57,7 @@ class Ldap{ * * @param object $config an object of class Config */ - function __construct($config) { + public function __construct($config) { if (is_object($config)) { $this->conf = $config; } @@ -75,7 +75,7 @@ class Ldap{ * @param boolean $allowAnonymous specifies if anonymous binds are allowed * @return mixed if connect succeeds the 0 is returned, else false or error number */ - function connect($user, $passwd, $allowAnonymous=false) { + public function connect($user, $passwd, $allowAnonymous=false) { // close any prior connection @$this->close(); // do not allow anonymous bind @@ -105,7 +105,7 @@ class Ldap{ } /** Closes connection to server */ - function close() { + public function close() { if ($this->server != null) { @ldap_close($this->server); } @@ -116,7 +116,7 @@ class Ldap{ * * @return object connection handle */ - function server() { + public function server() { if (!$this->is_connected) { $data = $this->decrypt_login(); $this->connect($data[0], $data[1]); @@ -126,14 +126,14 @@ class Ldap{ } /** Closes connection to LDAP server before serialization */ - function __sleep() { + public function __sleep() { $this->close(); // define which attributes to save return array("conf", "username", "password"); } /** Reconnects to LDAP server when deserialized */ - function __wakeup() { + public function __wakeup() { $this->is_connected = false; // delete PDF files and images which are older than 15 min $tmpDir = dirname(__FILE__) . '/../tmp/'; @@ -173,7 +173,7 @@ class Ldap{ * @param string $username LDAP user name * @param string $password LDAP password */ - function encrypt_login($username, $password) { + public function encrypt_login($username, $password) { // encrypt username and password $this->username = base64_encode(lamEncrypt($username)); $this->password = base64_encode(lamEncrypt($password)); @@ -184,7 +184,7 @@ class Ldap{ * * @return array array(user name, password) */ - function decrypt_login() { + public function decrypt_login() { // decrypt username and password $username = lamDecrypt(base64_decode($this->username)); $password = lamDecrypt(base64_decode($this->password)); @@ -192,8 +192,26 @@ class Ldap{ return $ret; } + /** + * Returns the LDAP user name. + * + * @return string user name + */ + public function getUserName() { + return lamDecrypt(base64_decode($this->username)); + } + + /** + * Returns the LDAP password. + * + * @return string password + */ + public function getPassword() { + return lamDecrypt(base64_decode($this->password)); + } + /** Closes connection to LDAP server and deletes encrypted username/password */ - function destroy() { + public function destroy() { $this->close(); $this->username="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; $this->password="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; From fc24f4a2aae1588969c83e2e3677973de8e49e52 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 31 Dec 2018 10:51:44 +0100 Subject: [PATCH 2/8] YubiKey support --- lam/help/help.inc | 4 + lam/lib/2factor.inc | 117 +++++++++- lam/lib/3rdParty/yubico/Yubico.php | 351 +++++++++++++++++++++++++++++ lam/lib/config.inc | 52 ++++- lam/templates/config/confmain.php | 18 +- lam/tests/lib/LAMConfigTest.php | 88 ++++++++ 6 files changed, 611 insertions(+), 19 deletions(-) create mode 100644 lam/lib/3rdParty/yubico/Yubico.php diff --git a/lam/help/help.inc b/lam/help/help.inc index 516f2a75..d176dba1 100644 --- a/lam/help/help.inc +++ b/lam/help/help.inc @@ -311,6 +311,10 @@ $helpArray = array ( "Text" => _('Protect the self service login with a captcha.')), "523" => array ("Headline" => _('Base color'), "Text" => _('Background color for self service pages.')), + "524" => array ("Headline" => _('Client id'), + "Text" => _('Please enter your client id for the verification API.')), + "525" => array ("Headline" => _('Secret key'), + "Text" => _('Please enter your secret key for the verification API.')), "550" => array ("Headline" => _("From address"), "Text" => _("This email address will be set as sender address of all password mails. If empty the system default (php.ini) will be used.")), "551" => array ("Headline" => _("Subject"), diff --git a/lam/lib/2factor.inc b/lam/lib/2factor.inc index f4907667..7bccb039 100644 --- a/lam/lib/2factor.inc +++ b/lam/lib/2factor.inc @@ -53,6 +53,8 @@ interface TwoFactorProvider { */ public function verify2ndFactor($user, $password, $serial, $twoFactorInput); + + } /** @@ -223,6 +225,71 @@ class PrivacyIDEAProvider implements TwoFactorProvider { } +/** + * Authentication via YubiKeys. + * + * @author Roland Gruber + */ +class YubicoProvider implements TwoFactorProvider { + + private $config; + + /** + * Constructor. + * + * @param TwoFactorConfiguration $config configuration + */ + public function __construct(&$config) { + $this->config = $config; + } + + /** + * {@inheritDoc} + * @see \LAM\LIB\TWO_FACTOR\TwoFactorProvider::getSerials() + */ + public function getSerials($user, $password) { + $keyAttributeName = strtolower('yubiKeyId'); + $loginDn = $_SESSION['ldap']->getUserName(); + $handle = getLDAPServerHandle(); + $ldapData = ldapGetDN($loginDn, array($keyAttributeName), $handle); + if (empty($ldapData[$keyAttributeName])) { + return array(); + } + return array(implode(', ', $ldapData[$keyAttributeName])); + } + + /** + * {@inheritDoc} + * @see \LAM\LIB\TWO_FACTOR\TwoFactorProvider::verify2ndFactor() + */ + public function verify2ndFactor($user, $password, $serial, $twoFactorInput) { + include_once(__DIR__ . "/3rdParty/yubico/Yubico.php"); + $serialData = $this->getSerials($user, $password); + if (empty($serialData)) { + return false; + } + $serials = explode(', ', $serialData[0]); + $serialMatched = false; + foreach ($serials as $serial) { + if (strpos($twoFactorInput, $serial) === 0) { + $serialMatched = true; + break; + } + } + if (!$serialMatched) { + throw new \Exception(_('YubiKey id does not match allowed list of key ids.')); + } + $url = $this->config->twoFactorAuthenticationURL; + $httpsverify = !$this->config->twoFactorAuthenticationInsecure; + $clientId = $this->config->twoFactorAuthenticationClientId; + $secretKey = $this->config->twoFactorAuthenticationSecretKey; + $auth = new \Auth_Yubico($clientId, $secretKey, $url, $httpsverify); + $auth->verify($twoFactorInput); + return true; + } + +} + /** * Returns the correct 2 factor provider. */ @@ -232,6 +299,8 @@ class TwoFactorProviderService { const TWO_FACTOR_NONE = 'none'; /** 2factor authentication via privacyIDEA */ const TWO_FACTOR_PRIVACYIDEA = 'privacyidea'; + /** 2factor authentication via YubiKey */ + const TWO_FACTOR_YUBICO = 'yubico'; private $config; @@ -260,6 +329,9 @@ class TwoFactorProviderService { if ($this->config->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA) { return new PrivacyIDEAProvider($this->config); } + elseif ($this->config->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_YUBICO) { + return new YubicoProvider($this->config); + } throw new \Exception('Invalid provider: ' . $this->config->twoFactorAuthentication); } @@ -270,11 +342,11 @@ class TwoFactorProviderService { * @return TwoFactorConfiguration configuration */ private function getConfigSelfService(&$profile) { - $config = new TwoFactorConfiguration(); - $config->twoFactorAuthentication = $profile->twoFactorAuthentication; - $config->twoFactorAuthenticationInsecure = $profile->twoFactorAuthenticationInsecure; - $config->twoFactorAuthenticationURL = $profile->twoFactorAuthenticationURL; - return $config; + $tfConfig = new TwoFactorConfiguration(); + $tfConfig->twoFactorAuthentication = $profile->twoFactorAuthentication; + $tfConfig->twoFactorAuthenticationInsecure = $profile->twoFactorAuthenticationInsecure; + $tfConfig->twoFactorAuthenticationURL = $profile->twoFactorAuthenticationURL; + return $tfConfig; } /** @@ -284,11 +356,13 @@ class TwoFactorProviderService { * @return TwoFactorConfiguration configuration */ private function getConfigAdmin($conf) { - $config = new TwoFactorConfiguration(); - $config->twoFactorAuthentication = $conf->getTwoFactorAuthentication(); - $config->twoFactorAuthenticationInsecure = $conf->getTwoFactorAuthenticationInsecure(); - $config->twoFactorAuthenticationURL = $conf->getTwoFactorAuthenticationURL(); - return $config; + $tfConfig = new TwoFactorConfiguration(); + $tfConfig->twoFactorAuthentication = $conf->getTwoFactorAuthentication(); + $tfConfig->twoFactorAuthenticationInsecure = $conf->getTwoFactorAuthenticationInsecure(); + $tfConfig->twoFactorAuthenticationURL = $conf->getTwoFactorAuthenticationURL(); + $tfConfig->twoFactorAuthenticationClientId = $conf->getTwoFactorAuthenticationClientId(); + $tfConfig->twoFactorAuthenticationSecretKey = $conf->getTwoFactorAuthenticationSecretKey(); + return $tfConfig; } } @@ -299,7 +373,30 @@ class TwoFactorProviderService { * @author Roland Gruber */ class TwoFactorConfiguration { + + /** + * @var string provider id + */ public $twoFactorAuthentication = null; + + /** + * @var service URL + */ public $twoFactorAuthenticationURL = null; + + /** + * @var disable certificate check + */ public $twoFactorAuthenticationInsecure = false; + + /** + * @var client ID for API access + */ + public $twoFactorAuthenticationClientId = null; + + /** + * @var secret key for API access + */ + public $twoFactorAuthenticationSecretKey = null; + } diff --git a/lam/lib/3rdParty/yubico/Yubico.php b/lam/lib/3rdParty/yubico/Yubico.php new file mode 100644 index 00000000..148d6f5f --- /dev/null +++ b/lam/lib/3rdParty/yubico/Yubico.php @@ -0,0 +1,351 @@ +, Olov Danielson + * @author Roland Gruber + * @copyright 2007-2015 Yubico AB + * @copyright 2018 Roland Gruber + * @license https://opensource.org/licenses/bsd-license.php New BSD License + * @version 2.0 + * @link https://www.yubico.com/ + * + * Adapted for LAM. + */ + +/** + * Class for verifying Yubico One-Time-Passcodes + */ +class Auth_Yubico { + + /** + * Yubico client ID + * + * @var string + */ + private $clientId; + + /** + * Yubico client key + * + * @var string + */ + private $clientKey; + + /** + * URL part of validation server + * + * @var string + */ + private $url; + + /** + * Last query to server + * + * @var string + */ + private $lastquery; + + /** + * Response from server + * + * @var string + */ + private $response; + + /** + * Flag whether to verify HTTPS server certificates or not. + * + * @var boolean + */ + private $httpsVerify; + + /** + * Constructor + * + * Sets up the object + * + * @param string $id The client identity + * @param string $key The client MAC key + * @param string $url URL + * @param boolean $httpsverify Flag whether to use verify HTTPS + * server certificates + */ + public function __construct($id, $key, $url, $httpsverify) { + $this->clientId = $id; + $this->clientKey = base64_decode($key); + $this->httpsVerify = $httpsverify; + $this->url = $url; + } + + /** + * Parse input string into password, yubikey prefix, + * ciphertext, and OTP. + * + * @param string Input string to parse + * @param string Optional delimiter re-class, default is '[:]' + * @return array Keyed array with fields + */ + private function parsePasswordOTP($str, $delim = '[:]') { + if (!preg_match("/^((.*)" . $delim . ")?(([cbdefghijklnrtuv]{0,12})([cbdefghijklnrtuv]{32}))$/i", $str, $matches)) { + /* Dvorak? */ + if (!preg_match("/^((.*)" . $delim . ")?(([jxe\\.uidchtnbpygk]{0,12})([jxe\\.uidchtnbpygk]{32}))$/i", $str, $matches)) { + return false; + } + else { + $ret['otp'] = strtr($matches[3], "jxe.uidchtnbpygk", "cbdefghijklnrtuv"); + } + } + else { + $ret['otp'] = $matches[3]; + } + $ret['password'] = $matches[2]; + $ret['prefix'] = $matches[4]; + $ret['ciphertext'] = $matches[5]; + return $ret; + } + + /** + * Parse parameters from last response + * + * example: getParameters("timestamp", "sessioncounter", "sessionuse"); + * + * @param array @parameters Array with strings representing + * parameters to parse + * @return array parameter array from last response + */ + private function getParameters($parameters) { + if ($parameters == null) { + $parameters = array( + 'timestamp', + 'sessioncounter', + 'sessionuse' + ); + } + $param_array = array(); + foreach ($parameters as $param) { + if (!preg_match("/" . $param . "=([0-9]+)/", $this->response, $out)) { + throw new LAMException(_('Error'), 'Could not parse parameter ' . $param . ' from response'); + } + $param_array[$param] = $out[1]; + } + return $param_array; + } + + /** + * Verify Yubico OTP against multiple URLs + * Protocol specification 2.0 is used to construct validation requests + * + * @param string $token Yubico OTP + * @param int $use_timestamp 1=>send request with ×tamp=1 to + * get timestamp and session information + * in the response + * @param string $sl Sync level in percentage between 0 + * and 100 or "fast" or "secure". + * @param int $timeout Max number of seconds to wait + * for responses + */ + public function verify($token, $use_timestamp = null, $sl = null, $timeout = null) { + /* Construct parameters string */ + $ret = $this->parsePasswordOTP($token); + if (!$ret) { + throw new LAMException(_('Error'), 'Could not parse Yubikey OTP'); + } + $params = array( + 'id' => $this->clientId, + 'otp' => $ret['otp'], + 'nonce' => md5(uniqid(getRandomNumber())) + ); + /* Take care of protocol version 2 parameters */ + if ($use_timestamp) { + $params['timestamp'] = 1; + } + if ($sl) { + $params['sl'] = $sl; + } + if ($timeout) { + $params['timeout'] = $timeout; + } + ksort($params); + $parameters = ''; + foreach ($params as $p => $v) { + $parameters .= "&" . $p . "=" . $v; + } + $parameters = ltrim($parameters, "&"); + + /* Generate signature. */ + if ($this->clientKey != "") { + $signature = base64_encode(hash_hmac('sha1', $parameters, $this->clientKey, true)); + $signature = preg_replace('/\+/', '%2B', $signature); + $parameters .= '&h=' . $signature; + } + + /* Generate and prepare request. */ + $mh = curl_multi_init(); + $ch = array(); + $query = $this->url . "?" . $parameters; + + $this->lastquery = $query; + logNewMessage(LOG_DEBUG, 'Yubico url: ' . $query); + + $handle = curl_init($query); + curl_setopt($handle, CURLOPT_USERAGENT, "LAM Auth Yubico"); + curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1); + if (!$this->httpsVerify) { + curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, 0); + } + curl_setopt($handle, CURLOPT_FAILONERROR, true); + /* + * If timeout is set, we better apply it here as well + * in case the validation server fails to follow it. + */ + if ($timeout) { + curl_setopt($handle, CURLOPT_TIMEOUT, $timeout); + } + // TODO single curl call + curl_multi_add_handle($mh, $handle); + + $ch[(int) $handle] = $handle; + + /* Execute and read request. */ + $this->response = null; + $replay = False; + $valid = False; + do { + /* Let curl do its work. */ + while (($mrc = curl_multi_exec($mh, $active)) == CURLM_CALL_MULTI_PERFORM); + + while ($info = curl_multi_info_read($mh)) { + if ($info['result'] == CURLE_OK) { + + /* We have a complete response from one server. */ + + $str = curl_multi_getcontent($info['handle']); + logNewMessage(LOG_DEBUG, 'Yubico answer: ' . $str); + $cinfo = curl_getinfo($info['handle']); + + if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) { + $status = $out[1]; + + /* + * There are 3 cases. + * + * 1. OTP or Nonce values doesn't match - ignore + * response. + * + * 2. We have a HMAC key. If signature is invalid - + * ignore response. Return if status=OK/REPLAYED_OTP/BAD_OTP. + * + * 3. Return if status=OK or status=REPLAYED_OTP. + */ + if (!preg_match("/otp=" . $params['otp'] . "/", $str) || !preg_match("/nonce=" . $params['nonce'] . "/", $str)) { + /* Case 1. Ignore response. */ + } + elseif ($this->clientKey != "") { + /* Case 2. Verify signature first */ + $rows = explode("\r\n", trim($str)); + $response = array(); + foreach ($rows as $key => $val) { + /* + * = is also used in BASE64 encoding so we only replace the first = by # which is not + * used in BASE64 + */ + $val = preg_replace('/=/', '#', $val, 1); + $row = explode("#", $val); + $response[$row[0]] = $row[1]; + } + + $parameters = array( + 'nonce', + 'otp', + 'sessioncounter', + 'sessionuse', + 'sl', + 'status', + 't', + 'timeout', + 'timestamp' + ); + sort($parameters); + $check = Null; + foreach ($parameters as $param) { + if (array_key_exists($param, $response)) { + if ($check) $check = $check . '&'; + $check = $check . $param . '=' . $response[$param]; + } + } + + $checksignature = base64_encode(hash_hmac('sha1', utf8_encode($check), $this->clientKey, true)); + + if ($response['h'] == $checksignature) { + if ($status == 'REPLAYED_OTP') { + $this->response = $str; + $replay = True; + } + if ($status == 'OK') { + $this->response = $str; + $valid = True; + } + // TODO status BAD_OTP + } + else { + // TODO throw invalid signature exception + } + } + else { + /* Case 3. We check the status directly */ + if ($status == 'REPLAYED_OTP') { + $this->response = $str; + $replay = True; + } + if ($status == 'OK') { + $this->response = $str; + $valid = True; + } + // TODO status BAD_OTP + } + } + if ($valid || $replay) { + /* We have status=OK or status=REPLAYED_OTP, return. */ + foreach ($ch as $h) { + curl_multi_remove_handle($mh, $h); + curl_close($h); + } + curl_multi_close($mh); + if ($replay) { + throw new LAMException(_('Error'), 'OTP replay detected.'); + } + if ($valid) { + return; + } + } + + curl_multi_remove_handle($mh, $info['handle']); + curl_close($info['handle']); + unset($ch[(int) $info['handle']]); + } + curl_multi_select($mh); + } + } + while ($active); + + /* + * Typically this is only reached + * when the timeout is reached and there is no + * OK/REPLAYED_REQUEST answer (think firewall). + */ + + foreach ($ch as $h) { + curl_multi_remove_handle($mh, $h); + curl_close($h); + } + curl_multi_close($mh); + + throw new LAMException(_('Error'), 'Invalid answer: ' . print_r($this->response, true)); + } + +} +?> diff --git a/lam/lib/config.inc b/lam/lib/config.inc index c254241c..0232cf2e 100644 --- a/lam/lib/config.inc +++ b/lam/lib/config.inc @@ -591,6 +591,8 @@ class LAMConfig { private $twoFactorAuthentication = TwoFactorProviderService::TWO_FACTOR_NONE; private $twoFactorAuthenticationURL = 'https://localhost'; + private $twoFactorAuthenticationClientId = null; + private $twoFactorAuthenticationSecretKey = null; private $twoFactorAuthenticationInsecure = false; private $twoFactorAuthenticationLabel = null; private $twoFactorAuthenticationOptional = false; @@ -607,7 +609,7 @@ class LAMConfig { 'pwdResetAllowScreenPassword', 'pwdResetForcePasswordChange', 'pwdResetDefaultPasswordOutput', 'scriptUserName', 'scriptSSHKey', 'scriptSSHKeyPassword', 'twoFactorAuthentication', 'twoFactorAuthenticationURL', 'twoFactorAuthenticationInsecure', 'twoFactorAuthenticationLabel', 'twoFactorAuthenticationOptional', - 'twoFactorAuthenticationCaption', 'referentialIntegrityOverlay' + 'twoFactorAuthenticationCaption', 'twoFactorAuthenticationClientId', 'twoFactorAuthenticationSecretKey', 'referentialIntegrityOverlay' ); @@ -867,6 +869,8 @@ class LAMConfig { if (!in_array("pwdResetDefaultPasswordOutput", $saved)) array_push($file_array, "\n" . "pwdResetDefaultPasswordOutput: " . $this->pwdResetDefaultPasswordOutput . "\n"); if (!in_array("twoFactorAuthentication", $saved)) array_push($file_array, "\n" . "twoFactorAuthentication: " . $this->twoFactorAuthentication . "\n"); if (!in_array("twoFactorAuthenticationURL", $saved)) array_push($file_array, "\n" . "twoFactorAuthenticationURL: " . $this->twoFactorAuthenticationURL . "\n"); + if (!in_array("twoFactorAuthenticationClientId", $saved)) array_push($file_array, "\n" . "twoFactorAuthenticationClientId: " . $this->twoFactorAuthenticationClientId . "\n"); + if (!in_array("twoFactorAuthenticationSecretKey", $saved)) array_push($file_array, "\n" . "twoFactorAuthenticationSecretKey: " . $this->twoFactorAuthenticationSecretKey . "\n"); if (!in_array("twoFactorAuthenticationInsecure", $saved)) array_push($file_array, "\n" . "twoFactorAuthenticationInsecure: " . $this->twoFactorAuthenticationInsecure . "\n"); if (!in_array("twoFactorAuthenticationLabel", $saved)) array_push($file_array, "\n" . "twoFactorAuthenticationLabel: " . $this->twoFactorAuthenticationLabel . "\n"); if (!in_array("twoFactorAuthenticationOptional", $saved)) array_push($file_array, "\n" . "twoFactorAuthenticationOptional: " . $this->twoFactorAuthenticationOptional . "\n"); @@ -2151,7 +2155,7 @@ class LAMConfig { /** * Returns the authentication URL. * - * @return string $twoFactorAuthenticationURL authentication URL + * @return string authentication URL */ public function getTwoFactorAuthenticationURL() { return $this->twoFactorAuthenticationURL; @@ -2166,10 +2170,46 @@ class LAMConfig { $this->twoFactorAuthenticationURL = $twoFactorAuthenticationURL; } + /** + * Sets the client id. + * + * @param string $clientId client id + */ + public function setTwoFactorAuthenticationClientId($clientId) { + $this->twoFactorAuthenticationClientId = $clientId; + } + + /** + * Returns the client id. + * + * @return string client id + */ + public function getTwoFactorAuthenticationClientId() { + return $this->twoFactorAuthenticationClientId; + } + + /** + * Sets the secret key. + * + * @param string $secretKey secret key + */ + public function setTwoFactorAuthenticationSecretKey($secretKey) { + $this->twoFactorAuthenticationSecretKey = $secretKey; + } + + /** + * Returns the secret key. + * + * @return string secret key + */ + public function getTwoFactorAuthenticationSecretKey() { + return $this->twoFactorAuthenticationSecretKey; + } + /** * Returns if SSL certificate verification is turned off. * - * @return bool $twoFactorAuthenticationInsecure SSL certificate verification is turned off + * @return bool SSL certificate verification is turned off */ public function getTwoFactorAuthenticationInsecure() { return $this->twoFactorAuthenticationInsecure; @@ -2187,7 +2227,7 @@ class LAMConfig { /** * Returns the authentication label. * - * @return string $twoFactorAuthenticationLabel authentication label + * @return string authentication label */ public function getTwoFactorAuthenticationLabel() { return $this->twoFactorAuthenticationLabel; @@ -2205,7 +2245,7 @@ class LAMConfig { /** * Returns if 2nd factor is optional. * - * @return bool $twoFactorAuthenticationOptional 2nd factor is optional + * @return bool 2nd factor is optional */ public function getTwoFactorAuthenticationOptional() { return $this->twoFactorAuthenticationOptional; @@ -2223,7 +2263,7 @@ class LAMConfig { /** * Returns the caption HTML. * - * @return string $twoFactorAuthenticationCaption caption HTML + * @return string caption HTML */ public function getTwoFactorAuthenticationCaption() { return $this->twoFactorAuthenticationCaption; diff --git a/lam/templates/config/confmain.php b/lam/templates/config/confmain.php index 3e0e7c8d..c79e62be 100644 --- a/lam/templates/config/confmain.php +++ b/lam/templates/config/confmain.php @@ -22,7 +22,7 @@ use \htmlGroup; /* This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) - Copyright (C) 2003 - 2017 Roland Gruber + Copyright (C) 2003 - 2018 Roland Gruber This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -460,19 +460,29 @@ if (extension_loaded('curl')) { $twoFactorOptions = array( _('None') => TwoFactorProviderService::TWO_FACTOR_NONE, 'privacyIDEA' => TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA, + 'YubiKey' => TwoFactorProviderService::TWO_FACTOR_YUBICO, ); $twoFactorSelect = new htmlResponsiveSelect('twoFactor', $twoFactorOptions, array($conf->getTwoFactorAuthentication()), _('Provider'), '514'); $twoFactorSelect->setHasDescriptiveElements(true); $twoFactorSelect->setTableRowsToHide(array( - TwoFactorProviderService::TWO_FACTOR_NONE => array('twoFactorURL', 'twoFactorInsecure', 'twoFactorLabel', 'twoFactorOptional', 'twoFactorCaption') + TwoFactorProviderService::TWO_FACTOR_NONE => array('twoFactorURL', 'twoFactorInsecure', 'twoFactorLabel', + 'twoFactorOptional', 'twoFactorCaption', 'twoFactorClientId', 'twoFactorSecretKey'), + TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA => array('twoFactorClientId', 'twoFactorSecretKey') )); $twoFactorSelect->setTableRowsToShow(array( - TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA => array('twoFactorURL', 'twoFactorInsecure', 'twoFactorLabel', 'twoFactorOptional', 'twoFactorCaption') + TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA => array('twoFactorURL', 'twoFactorInsecure', 'twoFactorLabel', + 'twoFactorOptional', 'twoFactorCaption'), + TwoFactorProviderService::TWO_FACTOR_YUBICO => array('twoFactorURL', 'twoFactorInsecure', 'twoFactorLabel', + 'twoFactorOptional', 'twoFactorCaption', 'twoFactorClientId', 'twoFactorSecretKey'), )); $row->add($twoFactorSelect, 12); $twoFactorUrl = new htmlResponsiveInputField(_("Base URL"), 'twoFactorURL', $conf->getTwoFactorAuthenticationURL(), '515'); $twoFactorUrl->setRequired(true); $row->add($twoFactorUrl, 12); + $twoFactorClientId = new htmlResponsiveInputField(_("Client id"), 'twoFactorClientId', $conf->getTwoFactorAuthenticationClientId(), '524'); + $row->add($twoFactorClientId, 12); + $twoFactorSecretKey = new htmlResponsiveInputField(_("Secret key"), 'twoFactorSecretKey', $conf->getTwoFactorAuthenticationSecretKey(), '525'); + $row->add($twoFactorSecretKey, 12); $twoFactorLabel = new htmlResponsiveInputField(_("Label"), 'twoFactorLabel', $conf->getTwoFactorAuthenticationLabel(), '517'); $row->add($twoFactorLabel, 12); $row->add(new htmlResponsiveInputCheckbox('twoFactorOptional', $conf->getTwoFactorAuthenticationOptional(), _('Optional'), '519'), 12); @@ -677,6 +687,8 @@ function checkInput() { if (extension_loaded('curl')) { $conf->setTwoFactorAuthentication($_POST['twoFactor']); $conf->setTwoFactorAuthenticationURL($_POST['twoFactorURL']); + $conf->setTwoFactorAuthenticationClientId($_POST['twoFactorClientId']); + $conf->setTwoFactorAuthenticationSecretKey($_POST['twoFactorSecretKey']); $conf->setTwoFactorAuthenticationInsecure(isset($_POST['twoFactorInsecure']) && ($_POST['twoFactorInsecure'] == 'on')); $conf->setTwoFactorAuthenticationLabel($_POST['twoFactorLabel']); $conf->setTwoFactorAuthenticationOptional(isset($_POST['twoFactorOptional']) && ($_POST['twoFactorOptional'] == 'on')); diff --git a/lam/tests/lib/LAMConfigTest.php b/lam/tests/lib/LAMConfigTest.php index d792214d..9a661b2e 100644 --- a/lam/tests/lib/LAMConfigTest.php +++ b/lam/tests/lib/LAMConfigTest.php @@ -499,6 +499,94 @@ class LAMConfigTest extends PHPUnit_Framework_TestCase { $this->assertEquals($val, $this->lAMConfig->getHttpAuthentication()); } + /** + * Tests LAMConfig->getTwoFactorAuthentication() and LAMConfig->setTwoFactorAuthentication() + */ + public function testTwoFactorAuthentication() { + $val = '2fid'; + $this->lAMConfig->setTwoFactorAuthentication($val); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthentication()); + $this->doSave(); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthentication()); + } + + /** + * Tests LAMConfig->getTwoFactorAuthenticationURL() and LAMConfig->setTwoFactorAuthenticationURL() + */ + public function testTwoFactorAuthenticationURL() { + $val = 'http://example.com'; + $this->lAMConfig->setTwoFactorAuthenticationURL($val); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationURL()); + $this->doSave(); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationURL()); + } + + /** + * Tests LAMConfig->getTwoFactorAuthenticationClientId() and LAMConfig->setTwoFactorAuthenticationClientId() + */ + public function testTwoFactorAuthenticationClientId() { + $val = '1234'; + $this->lAMConfig->setTwoFactorAuthenticationClientId($val); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationClientId()); + $this->doSave(); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationClientId()); + } + + /** + * Tests LAMConfig->getTwoFactorAuthenticationSecretKey() and LAMConfig->setTwoFactorAuthenticationSecretKey() + */ + public function testTwoFactorAuthenticationSecretKey() { + $val = '3333key'; + $this->lAMConfig->setTwoFactorAuthenticationSecretKey($val); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationSecretKey()); + $this->doSave(); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationSecretKey()); + } + + /** + * Tests LAMConfig->getTwoFactorAuthenticationInsecure() and LAMConfig->setTwoFactorAuthenticationInsecure() + */ + public function testTwoFactorAuthenticationInsecure() { + $val = true; + $this->lAMConfig->setTwoFactorAuthenticationInsecure($val); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationInsecure()); + $this->doSave(); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationInsecure()); + } + + /** + * Tests LAMConfig->getTwoFactorAuthenticationLabel() and LAMConfig->setTwoFactorAuthenticationLabel() + */ + public function testTwoFactorAuthenticationLabel() { + $val = '2falabel'; + $this->lAMConfig->setTwoFactorAuthenticationLabel($val); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationLabel()); + $this->doSave(); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationLabel()); + } + + /** + * Tests LAMConfig->getTwoFactorAuthenticationOptional() and LAMConfig->setTwoFactorAuthenticationOptional() + */ + public function testTwoFactorAuthenticationOptional() { + $val = true; + $this->lAMConfig->setTwoFactorAuthenticationOptional($val); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationOptional()); + $this->doSave(); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationOptional()); + } + + /** + * Tests LAMConfig->getTwoFactorAuthenticationCaption() and LAMConfig->setTwoFactorAuthenticationCaption() + */ + public function testTwoFactorAuthenticationCaption() { + $val = '2facaption'; + $this->lAMConfig->setTwoFactorAuthenticationCaption($val); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationCaption()); + $this->doSave(); + $this->assertEquals($val, $this->lAMConfig->getTwoFactorAuthenticationCaption()); + } + /** * Tests LAMConfig->getLamProMailFrom() and LAMConfig->setLamProMailFrom() */ From 098618704aa5cc0dcf66592099789b2f76f230fa Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 31 Dec 2018 11:03:50 +0100 Subject: [PATCH 3/8] fixed logo copy --- lam/lib/pdfstruct.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lam/lib/pdfstruct.inc b/lam/lib/pdfstruct.inc index 7befa3ac..ca4d3463 100644 --- a/lam/lib/pdfstruct.inc +++ b/lam/lib/pdfstruct.inc @@ -284,7 +284,7 @@ function installPDFTemplates() { $entry = $templateDir->read(); while ($entry){ $path = $basePath . '/logos/' . $entry; - if ((strpos($entry, '.') !== 1) && !is_file($path)) { + if ((strpos($entry, '.') !== 0) && !is_file($path)) { $template = $templatePath . '/' . $entry; logNewMessage(LOG_DEBUG, 'Copy template ' . $template . ' to ' . $path); @copy($template, $path); From 4fea8155c855805ce9358044c472847e8f1db6ee Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 31 Dec 2018 11:42:20 +0100 Subject: [PATCH 4/8] refactoring --- lam/lib/3rdParty/yubico/Yubico.php | 291 ++++++++++------------------- 1 file changed, 96 insertions(+), 195 deletions(-) diff --git a/lam/lib/3rdParty/yubico/Yubico.php b/lam/lib/3rdParty/yubico/Yubico.php index 148d6f5f..36e5ef43 100644 --- a/lam/lib/3rdParty/yubico/Yubico.php +++ b/lam/lib/3rdParty/yubico/Yubico.php @@ -12,7 +12,7 @@ * @version 2.0 * @link https://www.yubico.com/ * - * Adapted for LAM. + * Adapted for LAM. */ /** @@ -41,20 +41,6 @@ class Auth_Yubico { */ private $url; - /** - * Last query to server - * - * @var string - */ - private $lastquery; - - /** - * Response from server - * - * @var string - */ - private $response; - /** * Flag whether to verify HTTPS server certificates or not. * @@ -107,33 +93,6 @@ class Auth_Yubico { return $ret; } - /** - * Parse parameters from last response - * - * example: getParameters("timestamp", "sessioncounter", "sessionuse"); - * - * @param array @parameters Array with strings representing - * parameters to parse - * @return array parameter array from last response - */ - private function getParameters($parameters) { - if ($parameters == null) { - $parameters = array( - 'timestamp', - 'sessioncounter', - 'sessionuse' - ); - } - $param_array = array(); - foreach ($parameters as $param) { - if (!preg_match("/" . $param . "=([0-9]+)/", $this->response, $out)) { - throw new LAMException(_('Error'), 'Could not parse parameter ' . $param . ' from response'); - } - $param_array[$param] = $out[1]; - } - return $param_array; - } - /** * Verify Yubico OTP against multiple URLs * Protocol specification 2.0 is used to construct validation requests @@ -142,12 +101,10 @@ class Auth_Yubico { * @param int $use_timestamp 1=>send request with ×tamp=1 to * get timestamp and session information * in the response - * @param string $sl Sync level in percentage between 0 - * and 100 or "fast" or "secure". - * @param int $timeout Max number of seconds to wait - * for responses + * @throws LAMException if verification failed */ - public function verify($token, $use_timestamp = null, $sl = null, $timeout = null) { + public function verify($token, $use_timestamp = null) { + $timeout = 10; /* Construct parameters string */ $ret = $this->parsePasswordOTP($token); if (!$ret) { @@ -162,12 +119,7 @@ class Auth_Yubico { if ($use_timestamp) { $params['timestamp'] = 1; } - if ($sl) { - $params['sl'] = $sl; - } - if ($timeout) { - $params['timeout'] = $timeout; - } + $params['timeout'] = $timeout; ksort($params); $parameters = ''; foreach ($params as $p => $v) { @@ -182,12 +134,8 @@ class Auth_Yubico { $parameters .= '&h=' . $signature; } - /* Generate and prepare request. */ - $mh = curl_multi_init(); - $ch = array(); $query = $this->url . "?" . $parameters; - $this->lastquery = $query; logNewMessage(LOG_DEBUG, 'Yubico url: ' . $query); $handle = curl_init($query); @@ -198,153 +146,106 @@ class Auth_Yubico { curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, 0); } curl_setopt($handle, CURLOPT_FAILONERROR, true); - /* - * If timeout is set, we better apply it here as well - * in case the validation server fails to follow it. - */ - if ($timeout) { - curl_setopt($handle, CURLOPT_TIMEOUT, $timeout); - } - // TODO single curl call - curl_multi_add_handle($mh, $handle); - - $ch[(int) $handle] = $handle; + curl_setopt($handle, CURLOPT_TIMEOUT, $timeout); /* Execute and read request. */ $this->response = null; - $replay = False; - $valid = False; - do { - /* Let curl do its work. */ - while (($mrc = curl_multi_exec($mh, $active)) == CURLM_CALL_MULTI_PERFORM); + $str = curl_exec($handle); + $httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE); + curl_close($handle); + logNewMessage(LOG_DEBUG, 'Server answer: ' . $str); + if (is_string($str) && ($httpCode == 200) && preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) { + $status = $out[1]; - while ($info = curl_multi_info_read($mh)) { - if ($info['result'] == CURLE_OK) { - - /* We have a complete response from one server. */ - - $str = curl_multi_getcontent($info['handle']); - logNewMessage(LOG_DEBUG, 'Yubico answer: ' . $str); - $cinfo = curl_getinfo($info['handle']); - - if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) { - $status = $out[1]; - - /* - * There are 3 cases. - * - * 1. OTP or Nonce values doesn't match - ignore - * response. - * - * 2. We have a HMAC key. If signature is invalid - - * ignore response. Return if status=OK/REPLAYED_OTP/BAD_OTP. - * - * 3. Return if status=OK or status=REPLAYED_OTP. - */ - if (!preg_match("/otp=" . $params['otp'] . "/", $str) || !preg_match("/nonce=" . $params['nonce'] . "/", $str)) { - /* Case 1. Ignore response. */ - } - elseif ($this->clientKey != "") { - /* Case 2. Verify signature first */ - $rows = explode("\r\n", trim($str)); - $response = array(); - foreach ($rows as $key => $val) { - /* - * = is also used in BASE64 encoding so we only replace the first = by # which is not - * used in BASE64 - */ - $val = preg_replace('/=/', '#', $val, 1); - $row = explode("#", $val); - $response[$row[0]] = $row[1]; - } - - $parameters = array( - 'nonce', - 'otp', - 'sessioncounter', - 'sessionuse', - 'sl', - 'status', - 't', - 'timeout', - 'timestamp' - ); - sort($parameters); - $check = Null; - foreach ($parameters as $param) { - if (array_key_exists($param, $response)) { - if ($check) $check = $check . '&'; - $check = $check . $param . '=' . $response[$param]; - } - } - - $checksignature = base64_encode(hash_hmac('sha1', utf8_encode($check), $this->clientKey, true)); - - if ($response['h'] == $checksignature) { - if ($status == 'REPLAYED_OTP') { - $this->response = $str; - $replay = True; - } - if ($status == 'OK') { - $this->response = $str; - $valid = True; - } - // TODO status BAD_OTP - } - else { - // TODO throw invalid signature exception - } - } - else { - /* Case 3. We check the status directly */ - if ($status == 'REPLAYED_OTP') { - $this->response = $str; - $replay = True; - } - if ($status == 'OK') { - $this->response = $str; - $valid = True; - } - // TODO status BAD_OTP - } - } - if ($valid || $replay) { - /* We have status=OK or status=REPLAYED_OTP, return. */ - foreach ($ch as $h) { - curl_multi_remove_handle($mh, $h); - curl_close($h); - } - curl_multi_close($mh); - if ($replay) { - throw new LAMException(_('Error'), 'OTP replay detected.'); - } - if ($valid) { - return; - } - } - - curl_multi_remove_handle($mh, $info['handle']); - curl_close($info['handle']); - unset($ch[(int) $info['handle']]); + /* + * There are 3 cases. + * + * 1. OTP or Nonce values doesn't match - ignore + * response. + * + * 2. We have a HMAC key. If signature is invalid - + * ignore response. Return if status=OK/REPLAYED_OTP/BAD_OTP. + * + * 3. Return if status=OK or status=REPLAYED_OTP. + */ + if (!preg_match("/otp=" . $params['otp'] . "/", $str) || !preg_match("/nonce=" . $params['nonce'] . "/", $str)) { + if ($status == 'BAD_OTP') { + throw new LAMException(_('Error'), 'OTP not accepted. Maybe key is not registered.'); } - curl_multi_select($mh); + throw new LAMException(_('Error'), 'Invalid answer ' . $str); + } + elseif ($this->clientKey != "") { + /* Case 2. Verify signature first */ + $rows = explode("\r\n", trim($str)); + $response = array(); + foreach ($rows as $val) { + /* + * '=' is also used in BASE64 encoding so we only replace the first = by # which is not + * used in BASE64 + */ + $val = preg_replace('/=/', '#', $val, 1); + $row = explode("#", $val); + $response[$row[0]] = $row[1]; + } + + $parameters = array( + 'nonce', + 'otp', + 'sessioncounter', + 'sessionuse', + 'sl', + 'status', + 't', + 'timeout', + 'timestamp' + ); + sort($parameters); + $check = Null; + foreach ($parameters as $param) { + if (array_key_exists($param, $response)) { + if ($check) { + $check = $check . '&'; + } + $check = $check . $param . '=' . $response[$param]; + } + } + + $checksignature = base64_encode(hash_hmac('sha1', utf8_encode($check), $this->clientKey, true)); + + if ($response['h'] == $checksignature) { + $this->checkStatus($status); + return; + } + else { + throw new LAMException(_('Error'), 'Invalid signature, expected ' . $checksignature); + } + } + else { + /* Case 3. We check the status directly */ + $this->checkStatus($status); + return; } } - while ($active); + throw new LAMException(_('Error'), 'Call to verification service failed with ' . $httpCode); + } - /* - * Typically this is only reached - * when the timeout is reached and there is no - * OK/REPLAYED_REQUEST answer (think firewall). - */ - - foreach ($ch as $h) { - curl_multi_remove_handle($mh, $h); - curl_close($h); + /** + * Checks if the status is ok. + * + * @param string $status status + * @throws LAMException invalid status + */ + private function checkStatus($status) { + if ($status == 'REPLAYED_OTP') { + throw new LAMException(_('Error'), 'OTP replay detected.'); } - curl_multi_close($mh); - - throw new LAMException(_('Error'), 'Invalid answer: ' . print_r($this->response, true)); + elseif ($status == 'BAD_OTP') { + throw new LAMException(_('Error'), 'OTP not accepted. Maybe key is not registered.'); + } + elseif ($status == 'OK') { + return; + } + throw new LAMException(_('Error'), 'Invalid status: ' . $status); } } From 89ab8d0f1f8150ac88a7a5a3f78bde05010d4e15 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 31 Dec 2018 12:39:20 +0100 Subject: [PATCH 5/8] get attribute name from config --- lam/lib/2factor.inc | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lam/lib/2factor.inc b/lam/lib/2factor.inc index 7bccb039..c8e74394 100644 --- a/lam/lib/2factor.inc +++ b/lam/lib/2factor.inc @@ -248,7 +248,7 @@ class YubicoProvider implements TwoFactorProvider { * @see \LAM\LIB\TWO_FACTOR\TwoFactorProvider::getSerials() */ public function getSerials($user, $password) { - $keyAttributeName = strtolower('yubiKeyId'); + $keyAttributeName = strtolower($this->config->twoFactorAuthenticationSerialAttributeName); $loginDn = $_SESSION['ldap']->getUserName(); $handle = getLDAPServerHandle(); $ldapData = ldapGetDN($loginDn, array($keyAttributeName), $handle); @@ -362,6 +362,15 @@ class TwoFactorProviderService { $tfConfig->twoFactorAuthenticationURL = $conf->getTwoFactorAuthenticationURL(); $tfConfig->twoFactorAuthenticationClientId = $conf->getTwoFactorAuthenticationClientId(); $tfConfig->twoFactorAuthenticationSecretKey = $conf->getTwoFactorAuthenticationSecretKey(); + if ($tfConfig->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_YUBICO) { + $moduleSettings = $conf->get_moduleSettings(); + if (!empty($moduleSettings['yubiKeyUser_attributeName'][0])) { + $tfConfig->twoFactorAuthenticationSerialAttributeName = $moduleSettings['yubiKeyUser_attributeName'][0]; + } + else { + $tfConfig->twoFactorAuthenticationSerialAttributeName = 'yubiKeyId'; + } + } return $tfConfig; } @@ -399,4 +408,9 @@ class TwoFactorConfiguration { */ public $twoFactorAuthenticationSecretKey = null; + /** + * @var LDAP attribute name that stores the token serials + */ + public $twoFactorAuthenticationSerialAttributeName = null; + } From d0a97c47a95c84e9efda7d8da9fae1de799f1608 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Tue, 1 Jan 2019 10:54:31 +0100 Subject: [PATCH 6/8] YubiKey for self service --- lam/lib/2factor.inc | 20 ++++++++++++++++++-- lam/lib/selfService.inc | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/lam/lib/2factor.inc b/lam/lib/2factor.inc index c8e74394..d57416d2 100644 --- a/lam/lib/2factor.inc +++ b/lam/lib/2factor.inc @@ -5,7 +5,7 @@ use \LAMConfig; /* This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) - Copyright (C) 2017 - 2018 Roland Gruber + Copyright (C) 2017 - 2019 Roland Gruber This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -249,7 +249,12 @@ class YubicoProvider implements TwoFactorProvider { */ public function getSerials($user, $password) { $keyAttributeName = strtolower($this->config->twoFactorAuthenticationSerialAttributeName); - $loginDn = $_SESSION['ldap']->getUserName(); + if (isset($_SESSION['selfService_clientDN'])) { + $loginDn = lamDecrypt($_SESSION['selfService_clientDN'], 'SelfService'); + } + else { + $loginDn = $_SESSION['ldap']->getUserName(); + } $handle = getLDAPServerHandle(); $ldapData = ldapGetDN($loginDn, array($keyAttributeName), $handle); if (empty($ldapData[$keyAttributeName])) { @@ -346,6 +351,17 @@ class TwoFactorProviderService { $tfConfig->twoFactorAuthentication = $profile->twoFactorAuthentication; $tfConfig->twoFactorAuthenticationInsecure = $profile->twoFactorAuthenticationInsecure; $tfConfig->twoFactorAuthenticationURL = $profile->twoFactorAuthenticationURL; + $tfConfig->twoFactorAuthenticationClientId = $profile->twoFactorAuthenticationClientId; + $tfConfig->twoFactorAuthenticationSecretKey = $profile->twoFactorAuthenticationSecretKey; + if ($tfConfig->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_YUBICO) { + $moduleSettings = $profile->moduleSettings; + if (!empty($moduleSettings['yubiKeyUser_attributeName'][0])) { + $tfConfig->twoFactorAuthenticationSerialAttributeName = $moduleSettings['yubiKeyUser_attributeName'][0]; + } + else { + $tfConfig->twoFactorAuthenticationSerialAttributeName = 'yubiKeyId'; + } + } return $tfConfig; } diff --git a/lam/lib/selfService.inc b/lam/lib/selfService.inc index 5c79ebd5..459ba591 100644 --- a/lam/lib/selfService.inc +++ b/lam/lib/selfService.inc @@ -3,7 +3,7 @@ use \LAM\LIB\TWO_FACTOR\TwoFactorProviderService; /* This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) - Copyright (C) 2006 - 2018 Roland Gruber + Copyright (C) 2006 - 2019 Roland Gruber This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -300,6 +300,38 @@ function isSelfService() { return session_name() == 'SELFSERVICE'; } +/** + * Opens the LDAP connection and returns the handle. No bind is done. + * + * @param selfServiceProfile $profile profile + * @return handle LDAP handle or null if connection failed + */ +function openSelfServiceLdapConnection($profile) { + $server = connectToLDAP($profile->serverURL, $profile->useTLS); + if ($server != null) { + // follow referrals + ldap_set_option($server, LDAP_OPT_REFERRALS, $profile->followReferrals); + } + return $server; +} + +/** + * Binds the LDAP connections with given user and password. + * + * @param handle $handle LDAP handle + * @param selfServiceProfile profile + * @param string $userDn bind DN + * @param string $password bind password + * @return boolean binding successful + */ +function bindLdapUser($handle, $profile, $userDn, $password) { + if ($profile->useForAllOperations) { + $userDn = $profile->LDAPUser; + $password = deobfuscateText($profile->LDAPPassword); + } + return @ldap_bind($handle, $userDn, $password); +} + /** * Includes all settings of a self service profile. @@ -391,6 +423,8 @@ class selfServiceProfile { public $twoFactorAuthenticationLabel = null; public $twoFactorAuthenticationOptional = false; public $twoFactorAuthenticationCaption = ''; + public $twoFactorAuthenticationClientId = ''; + public $twoFactorAuthenticationSecretKey = ''; /** provider for captcha (-/google) */ public $captchaProvider = '-'; @@ -445,6 +479,8 @@ class selfServiceProfile { $this->twoFactorAuthenticationLabel = null; $this->twoFactorAuthenticationOptional = false; $this->twoFactorAuthenticationCaption = ''; + $this->twoFactorAuthenticationClientId = ''; + $this->twoFactorAuthenticationSecretKey = ''; $this->captchaProvider = '-'; $this->reCaptchaSiteKey = ''; $this->reCaptchaSecretKey = ''; From 8c4612c2ff15edd130bc1361e66a5848e112be29 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Tue, 1 Jan 2019 11:24:44 +0100 Subject: [PATCH 7/8] documentation --- Readme.md | 2 +- lam-packaging/debian/copyright | 27 ++++++++++- lam/HISTORY | 3 ++ lam/README | 2 +- lam/copyright | 26 +++++++++- .../manual-sources/chapter-configuration.xml | 48 +++++++++++++++++-- .../manual-sources/chapter-selfService.xml | 46 ++++++++++++++++-- lam/docs/manual-sources/overview.xml | 2 +- 8 files changed, 144 insertions(+), 12 deletions(-) diff --git a/Readme.md b/Readme.md index 2845a2c6..8a9b3f5c 100644 --- a/Readme.md +++ b/Readme.md @@ -25,4 +25,4 @@ There are two modules. Usually, you only need the files inside "lam". LAM is published under the GNU General Public License. The complete list of licenses can be found in the copyright file. -Copyright (C) 2003 - 2018 Roland Gruber \ No newline at end of file +Copyright (C) 2003 - 2019 Roland Gruber \ No newline at end of file diff --git a/lam-packaging/debian/copyright b/lam-packaging/debian/copyright index 5546d938..e48bc7bc 100644 --- a/lam-packaging/debian/copyright +++ b/lam-packaging/debian/copyright @@ -1,4 +1,4 @@ -This software is copyright (c) 2003 - 2018 by Roland Gruber +This software is copyright (c) 2003 - 2019 by Roland Gruber If you purchased a copy of LDAP Account Manager Pro then the following files are licensed under the conditions which you accepted at purchase @@ -199,6 +199,29 @@ B: OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +C: + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list + of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + Programs and licenses with other licenses and/or authors than the main license and authors: @@ -206,6 +229,7 @@ main license and authors: lib/3rdParty/tcpdf/fonts/DejaVu*.ttf A Public Domain, Bitstream, Inc., Tavmjong Bah lib/3rdParty/tcpdf/fonts/DejaVu*.z A Public Domain, Bitstream, Inc., Tavmjong Bah lib/3rdParty/phpseclib B Jim Wigginton +lib/3rdParty/yubico/Yubico.php C 2015 Yubico AB templates/lib/*jquery*.js B 2018 jQuery Foundation and other contributors style/120_jquery-ui*.css B 2016 jQuery Foundation and other contributors templates/lib/*jquery-dropmenu-*.js B 2010 Fred Heusschen @@ -221,4 +245,3 @@ style/610_magnific-popup.css B 2016 Dmitry Semenov style/responsive/105_normalize.css B Nicolas Gallagher and Jonathan Neal style/responsive/110_grid.css B - diff --git a/lam/HISTORY b/lam/HISTORY index 7993f2e5..e9ab58e5 100644 --- a/lam/HISTORY +++ b/lam/HISTORY @@ -1,3 +1,6 @@ +March 2019 + - Added YubiKey as 2-factor authentication provider + 28.12.2018 6.6 - New import/export in tools menu - YubiKey support diff --git a/lam/README b/lam/README index d2b3fcb2..d00576e2 100644 --- a/lam/README +++ b/lam/README @@ -15,7 +15,7 @@ LAM - Readme https://www.ldap-account-manager.org/ - Copyright (C) 2003 - 2018 Roland Gruber + Copyright (C) 2003 - 2019 Roland Gruber Installation and documentation: Please see the LAM manual in docs/manual/index.html. diff --git a/lam/copyright b/lam/copyright index 997cc63e..f5b8159b 100644 --- a/lam/copyright +++ b/lam/copyright @@ -1,4 +1,4 @@ -This software is copyright (c) 2003 - 2018 by Roland Gruber +This software is copyright (c) 2003 - 2019 by Roland Gruber If you purchased a copy of LDAP Account Manager Pro then the following files are licensed under the conditions which you accepted at purchase @@ -198,6 +198,29 @@ B: OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +C: + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list + of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + Programs and licenses with other licenses and/or authors than the main license and authors: @@ -205,6 +228,7 @@ main license and authors: lib/3rdParty/tcpdf/fonts/DejaVu*.ttf A Public Domain, Bitstream, Inc., Tavmjong Bah lib/3rdParty/tcpdf/fonts/DejaVu*.z A Public Domain, Bitstream, Inc., Tavmjong Bah lib/3rdParty/phpseclib B Jim Wigginton +lib/3rdParty/yubico/Yubico.php C 2015 Yubico AB templates/lib/*jquery*.js B 2018 jQuery Foundation and other contributors style/120_jquery-ui*.css B 2016 jQuery Foundation and other contributors templates/lib/*jquery-dropmenu-*.js B 2010 Fred Heusschen diff --git a/lam/docs/manual-sources/chapter-configuration.xml b/lam/docs/manual-sources/chapter-configuration.xml index 28daadfb..3f827103 100644 --- a/lam/docs/manual-sources/chapter-configuration.xml +++ b/lam/docs/manual-sources/chapter-configuration.xml @@ -596,11 +596,53 @@ privacyIdea + + + YubiKey + - By default LAM will enforce to use a token and reject users that - did not setup one. You can set this check to optional. But if a user - has setup a token then this will always be required. + Configuration options: + + privacyIDEA: + + + + Base URL: please enter the URL of your privacyIDEA + instance + + + + YubiKey: + + + + Base URL: please enter the URL of your YubiKey verfication + server. For YubiKey cloud this is + "https://api.yubico.com/wsapi/2.0/verify". If you run a custom + verification API such as yubiserver then enter its URL (e.g. + http://www.example.com:8000/wsapi/2.0/verify). The URL needs to + end with "/wsapi/2.0/verify". + + + + Client id: this is only required for YubiKey cloud. You can + register here: https://upgrade.yubico.com/getapikey/ + + + + Secret key: this is only required for YubiKey cloud. You can + register here: https://upgrade.yubico.com/getapikey/ + + + + Optional: By default LAM will enforce to use a token and reject + users that did not setup one. You can set this check to optional. But + if a user has setup a token then this will always be required. + + Disable certificate check: This should be used on development + instances only. It skips the certificate check when connecting to + verification server. diff --git a/lam/docs/manual-sources/chapter-selfService.xml b/lam/docs/manual-sources/chapter-selfService.xml index ad73d464..dba51c2b 100644 --- a/lam/docs/manual-sources/chapter-selfService.xml +++ b/lam/docs/manual-sources/chapter-selfService.xml @@ -279,11 +279,51 @@ privacyIdea + + + YubiKey + - By default LAM will enforce to use a token and reject users that - did not setup one. You can set this check to optional. But if a user - has setup a token then this will always be required. + privacyIDEA: + + + + Base URL: please enter the URL of your privacyIDEA + instance + + + + YubiKey: + + + + Base URL: please enter the URL of your YubiKey verfication + server. For YubiKey cloud this is + "https://api.yubico.com/wsapi/2.0/verify". If you run a custom + verification API such as yubiserver then enter its URL (e.g. + http://www.example.com:8000/wsapi/2.0/verify). The URL needs to + end with "/wsapi/2.0/verify". + + + + Client id: this is only required for YubiKey cloud. You can + register here: https://upgrade.yubico.com/getapikey/ + + + + Secret key: this is only required for YubiKey cloud. You can + register here: https://upgrade.yubico.com/getapikey/ + + + + Optional: By default LAM will enforce to use a token and reject + users that did not setup one. You can set this check to optional. But + if a user has setup a token then this will always be required. + + Disable certificate check: This should be used on development + instances only. It skips the certificate check when connecting to + verification server. diff --git a/lam/docs/manual-sources/overview.xml b/lam/docs/manual-sources/overview.xml index 0fcedc6a..fb88a123 100644 --- a/lam/docs/manual-sources/overview.xml +++ b/lam/docs/manual-sources/overview.xml @@ -16,7 +16,7 @@ https://www.ldap-account-manager.org/ - Copyright (C) 2003 - 2018 Roland Gruber + Copyright (C) 2003 - 2019 Roland Gruber <post@rolandgruber.de> Key features: From e43d520e52800ad6af16788f4fc5d718460e365c Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Tue, 1 Jan 2019 11:31:43 +0100 Subject: [PATCH 8/8] limit id length --- lam/lib/modules/yubiKeyUser.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lam/lib/modules/yubiKeyUser.inc b/lam/lib/modules/yubiKeyUser.inc index e4481f62..feafd11e 100644 --- a/lam/lib/modules/yubiKeyUser.inc +++ b/lam/lib/modules/yubiKeyUser.inc @@ -5,7 +5,7 @@ use \LAM\PDF\PDFTableRow; /* This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) - Copyright (C) 2018 Roland Gruber + Copyright (C) 2018 - 2019 Roland Gruber This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -308,7 +308,7 @@ class yubiKeyUser extends baseModule { for ($i = 0; $i < sizeof($keys); $i++) { $group = new htmlGroup(); $keyInput = new htmlInputField('yubiKeyId_' . $i, $keys[$i]); - $keyInput->setFieldMaxLength(16384); + $keyInput->setFieldMaxLength(12); $group->addElement($keyInput); $delLink = new htmlLink('', '#', '../../graphics/del.png'); $delLink->setTitle(_('Delete'));