Merge pull request #70 from LDAPAccountManager/duo

Duo support
This commit is contained in:
gruberroland 2019-08-13 17:36:12 +02:00 committed by GitHub
commit a7f2f753c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1134 additions and 59 deletions

View File

@ -390,6 +390,31 @@ D:
permanent authorization for you to choose that version for the
Library.
E:
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.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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:
@ -411,6 +436,8 @@ templates/lib/*jquery-validationEngine-*.js B 2010 Cedric Dugas and Olivier Re
style/150_jquery-validationEngine*.css B 2010 Cedric Dugas and Olivier Refalo
templates/lib/extra/cropperjs B 2018 Chen Fengyuan
style/600_cropper*.css B 2018 Chen Fengyuan
templates/lib/extra/duo/*.js E 2019 Duo Security
lib/3rdParty/duo/*.php E 2019 Duo Security
templates/lib/600_jquery.magnific-popup.js B 2016 Dmitry Semenov
style/610_magnific-popup.css B 2016 Dmitry Semenov
style/responsive/105_normalize.css B Nicolas Gallagher and Jonathan Neal

View File

@ -1,6 +1,8 @@
September 2019 6.9
- Group account types can show member+owner count in list view
- 2-factor authentication: user name attribute for privacyIDEA can be specified
- 2-factor authentication:
-> Duo support
-> user name attribute for privacyIDEA can be specified
- LAM Pro:
-> New self service settings for login and main page footer

View File

@ -389,6 +389,31 @@ D:
permanent authorization for you to choose that version for the
Library.
E:
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.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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:
@ -410,6 +435,8 @@ templates/lib/*jquery-validationEngine-*.js B 2010 Cedric Dugas and Olivier Re
style/150_jquery-validationEngine*.css B 2010 Cedric Dugas and Olivier Refalo
templates/lib/extra/cropperjs B 2018 Chen Fengyuan
style/600_cropper*.css B 2018 Chen Fengyuan
templates/lib/extra/duo/*.js E 2019 Duo Security
lib/3rdParty/duo/*.php E 2019 Duo Security
templates/lib/600_jquery.magnific-popup.js B 2016 Dmitry Semenov
style/610_magnific-popup.css B 2016 Dmitry Semenov
style/responsive/105_normalize.css B Nicolas Gallagher and Jonathan Neal

View File

@ -625,6 +625,10 @@
<listitem>
<para><ulink url="https://www.yubico.com/">YubiKey</ulink></para>
</listitem>
<listitem>
<para><ulink url="https://duo.com/">Duo</ulink></para>
</listitem>
</itemizedlist>
<para>Configuration options:</para>
@ -639,7 +643,20 @@
<listitem>
<para>User name attribute: please enter the LDAP attribute name
that contains the user ID (e.g. "uid")</para>
that contains the user ID (e.g. "uid").</para>
</listitem>
<listitem>
<para>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.</para>
</listitem>
<listitem>
<para>Disable certificate check: This should be used on
development instances only. It skips the certificate check when
connecting to verification server.</para>
</listitem>
</itemizedlist>
@ -664,15 +681,45 @@
<para>Secret key: this is only required for YubiKey cloud. You can
register here: https://upgrade.yubico.com/getapikey/</para>
</listitem>
<listitem>
<para>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.</para>
</listitem>
<listitem>
<para>Disable certificate check: This should be used on
development instances only. It skips the certificate check when
connecting to verification server.</para>
</listitem>
</itemizedlist>
<para>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.</para>
<para>Duo:</para>
<para>Disable certificate check: This should be used on development
instances only. It skips the certificate check when connecting to
verification server.</para>
<para>This requires to register a new "Web SDK" application in your
Duo admin panel.</para>
<itemizedlist>
<listitem>
<para>User name attribute: please enter the LDAP attribute name
that contains the user ID (e.g. "uid").</para>
</listitem>
<listitem>
<para>Base URL: please enter the API-URL of your Duo instance
(e.g. api-12345.duosecurity.com).</para>
</listitem>
<listitem>
<para>Client id: please enter your integration key. </para>
</listitem>
<listitem>
<para>Secret key: please enter your secret key.</para>
</listitem>
</itemizedlist>
<screenshot>
<mediaobject>

View File

@ -596,7 +596,7 @@
intermediate release.</para>
<section>
<title>6.7 -&gt; 6.8</title>
<title>6.7 -&gt; 6.9</title>
<para>No actions required.</para>
</section>

View File

@ -2,6 +2,9 @@
namespace LAM\LIB\TWO_FACTOR;
use \selfServiceProfile;
use \LAMConfig;
use \htmlScript;
use \htmlInputField;
use \htmlIframe;
/*
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
@ -53,14 +56,87 @@ interface TwoFactorProvider {
*/
public function verify2ndFactor($user, $password, $serial, $twoFactorInput);
/**
* Returns if the service has a custom input form.
* In this case the token field is not displayed.
*
* @return has custom input form
*/
public function hasCustomInputForm();
/**
* Adds the custom input fields to the form.
*
* @param htmlResponsiveRow $row row where to add the input fields
* @param string user DN
*/
public function addCustomInput(&$row, $userDn);
/**
* Returns if the submit button should be shown.
*
* @return bool show submit button
*/
public function isShowSubmitButton();
}
/**
* Base class for 2-factor authentication providers.
*
* @author Roland Gruber
*/
abstract class BaseProvider implements TwoFactorProvider {
protected $config;
/**
* {@inheritDoc}
* @see \LAM\LIB\TWO_FACTOR\TwoFactorProvider::hasCustomInputForm()
*/
public function hasCustomInputForm() {
return false;
}
/**
* {@inheritDoc}
* @see \LAM\LIB\TWO_FACTOR\TwoFactorProvider::addCustomInput()
*/
public function addCustomInput(&$row, $userDn) {
// must be filled by subclass if used
}
/**
* Returns the value of the user attribute in LDAP.
*
* @param string $userDn user DN
* @return string user name
*/
protected function getLoginAttributeValue($userDn) {
$attrName = $this->config->twoFactorAuthenticationSerialAttributeName;
$userData = ldapGetDN($userDn, array($attrName));
if (empty($userData[$attrName])) {
return null;
}
if (is_array($userData[$attrName])) {
return $userData[$attrName][0];
}
return $userData[$attrName];
}
/**
* {@inheritDoc}
* @see \LAM\LIB\TWO_FACTOR\TwoFactorProvider::isShowSubmitButton()
*/
public function isShowSubmitButton() {
return true;
}
}
/**
* Provider for privacyIDEA.
*/
class PrivacyIDEAProvider implements TwoFactorProvider {
private $config;
class PrivacyIDEAProvider extends BaseProvider {
/**
* Constructor.
@ -82,24 +158,6 @@ class PrivacyIDEAProvider implements TwoFactorProvider {
return $this->getSerialsForUser($loginAttribute, $token);
}
/**
* Returns the value of the user attribute in LDAP.
*
* @param string $userDn user DN
* @return string user name
*/
private function getLoginAttributeValue($userDn) {
$attrName = $this->config->twoFactorAuthenticationSerialAttributeName;
$userData = ldapGetDN($userDn, array($attrName));
if (empty($userData[$attrName])) {
return null;
}
if (is_array($userData[$attrName])) {
return $userData[$attrName][0];
}
return $userData[$attrName];
}
/**
* {@inheritDoc}
* @see \LAM\LIB\TWO_FACTOR\TwoFactorProvider::verify2ndFactor()
@ -246,9 +304,7 @@ class PrivacyIDEAProvider implements TwoFactorProvider {
*
* @author Roland Gruber
*/
class YubicoProvider implements TwoFactorProvider {
private $config;
class YubicoProvider extends BaseProvider {
/**
* Constructor.
@ -311,6 +367,103 @@ class YubicoProvider implements TwoFactorProvider {
}
/**
* Provider for DUO.
*/
class DuoProvider extends BaseProvider {
/**
* 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) {
return array('DUO');
}
/**
* {@inheritDoc}
* @see \LAM\LIB\TWO_FACTOR\TwoFactorProvider::isShowSubmitButton()
*/
public function isShowSubmitButton() {
return false;
}
/**
* {@inheritDoc}
* @see \LAM\LIB\TWO_FACTOR\TwoFactorProvider::hasCustomInputForm()
*/
public function hasCustomInputForm() {
return true;
}
/**
* {@inheritDoc}
* @see \LAM\LIB\TWO_FACTOR\BaseProvider::addCustomInput()
*/
public function addCustomInput(&$row, $userDn) {
$loginAttribute = $this->getLoginAttributeValue($userDn);
$aKey = $this->getAKey();
include_once(__DIR__ . "/3rdParty/duo/Web.php");
$signedRequest = \Duo\Web::signRequest($this->config->twoFactorAuthenticationClientId,
$this->config->twoFactorAuthenticationSecretKey,
$aKey,
$loginAttribute);
if ($this->config->isSelfService) {
$row->add(new htmlScript("../lib/extra/duo/Duo-Web-v2.js", false, false), 12);
}
else {
$row->add(new htmlScript("lib/extra/duo/Duo-Web-v2.js", false, false), 12);
}
$iframe = new htmlIframe('duo_iframe');
$iframe->addDataAttribute('host', $this->config->twoFactorAuthenticationURL);
$iframe->addDataAttribute('sig-request', $signedRequest);
$row->add($iframe, 12);
}
/**
* Returns the aKey.
*
* @return String aKey
*/
private function getAKey() {
if (empty($_SESSION['duo_akey'])) {
$_SESSION['duo_akey'] = generateRandomPassword(40);
}
return $_SESSION['duo_akey'];
}
/**
* {@inheritDoc}
* @see \LAM\LIB\TWO_FACTOR\TwoFactorProvider::verify2ndFactor()
*/
public function verify2ndFactor($user, $password, $serial, $twoFactorInput) {
logNewMessage(LOG_DEBUG, 'PrivacyIDEAProvider: Checking 2nd factor for ' . $user);
$loginAttribute = $this->getLoginAttributeValue($user);
$response = $_POST['sig_response'];
include_once(__DIR__ . "/3rdParty/duo/Web.php");
$result = \Duo\Web::verifyResponse(
$this->config->twoFactorAuthenticationClientId,
$this->config->twoFactorAuthenticationSecretKey,
$this->getAKey(),
$response);
if ($result === $loginAttribute) {
return true;
}
logNewMessage(LOG_ERR, 'DUO authentication failed');
return false;
}
}
/**
* Returns the correct 2 factor provider.
*/
@ -322,6 +475,8 @@ class TwoFactorProviderService {
const TWO_FACTOR_PRIVACYIDEA = 'privacyidea';
/** 2factor authentication via YubiKey */
const TWO_FACTOR_YUBICO = 'yubico';
/** 2factor authentication via DUO */
const TWO_FACTOR_DUO = 'duo';
private $config;
@ -353,6 +508,9 @@ class TwoFactorProviderService {
elseif ($this->config->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_YUBICO) {
return new YubicoProvider($this->config);
}
elseif ($this->config->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_DUO) {
return new DuoProvider($this->config);
}
throw new \Exception('Invalid provider: ' . $this->config->twoFactorAuthentication);
}
@ -364,6 +522,7 @@ class TwoFactorProviderService {
*/
private function getConfigSelfService(&$profile) {
$tfConfig = new TwoFactorConfiguration();
$tfConfig->isSelfService = true;
$tfConfig->twoFactorAuthentication = $profile->twoFactorAuthentication;
$tfConfig->twoFactorAuthenticationInsecure = $profile->twoFactorAuthenticationInsecure;
$tfConfig->twoFactorAuthenticationURL = $profile->twoFactorAuthenticationURL;
@ -378,7 +537,8 @@ class TwoFactorProviderService {
$tfConfig->twoFactorAuthenticationSerialAttributeName = 'yubiKeyId';
}
}
if ($tfConfig->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA) {
if (($tfConfig->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA)
|| ($tfConfig->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_DUO)) {
$attrName = $profile->twoFactorAuthenticationAttribute;
if (empty($attrName)) {
$attrName = 'uid';
@ -396,6 +556,7 @@ class TwoFactorProviderService {
*/
private function getConfigAdmin($conf) {
$tfConfig = new TwoFactorConfiguration();
$tfConfig->isSelfService = false;
$tfConfig->twoFactorAuthentication = $conf->getTwoFactorAuthentication();
$tfConfig->twoFactorAuthenticationInsecure = $conf->getTwoFactorAuthenticationInsecure();
$tfConfig->twoFactorAuthenticationURL = $conf->getTwoFactorAuthenticationURL();
@ -410,7 +571,8 @@ class TwoFactorProviderService {
$tfConfig->twoFactorAuthenticationSerialAttributeName = 'yubiKeyId';
}
}
if ($tfConfig->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA) {
if (($tfConfig->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA)
|| ($tfConfig->twoFactorAuthentication == TwoFactorProviderService::TWO_FACTOR_DUO)) {
$tfConfig->twoFactorAuthenticationSerialAttributeName = strtolower($conf->getTwoFactorAuthenticationAttribute());
}
return $tfConfig;
@ -425,6 +587,11 @@ class TwoFactorProviderService {
*/
class TwoFactorConfiguration {
/**
* @var bool is self service
*/
public $isSelfService = false;
/**
* @var string provider id
*/

176
lam/lib/3rdParty/duo/Web.php vendored Normal file
View File

@ -0,0 +1,176 @@
<?php
namespace Duo;
/*
* https://duo.com/docs/duoweb
*/
class Web
{
const DUO_PREFIX = "TX";
const APP_PREFIX = "APP";
const AUTH_PREFIX = "AUTH";
const DUO_EXPIRE = 300;
const APP_EXPIRE = 3600;
const INIT_EXPIRE = 300;
const IKEY_LEN = 20;
const SKEY_LEN = 40;
const AKEY_LEN = 40; // if this changes you have to change ERR_AKEY
const ERR_USER = 'ERR|The username specified is invalid.';
const ERR_IKEY = 'ERR|The Duo integration key specified is invalid.';
const ERR_SKEY = 'ERR|The Duo secret key specified is invalid.';
const ERR_AKEY = 'ERR|The application secret key specified must be at least 40 characters.';
const LIBRARY_NAME = 'duo_php';
const VERSION = '1.0.0';
private static function signVals($key, $vals, $prefix, $expire, $time = null, $algo = "sha1")
{
$exp = ($time ? $time : time()) + $expire;
$val = $vals . '|' . $exp;
$b64 = base64_encode($val);
$cookie = $prefix . '|' . $b64;
$sig = hash_hmac($algo, $cookie, $key);
return $cookie . '|' . $sig;
}
private static function parseVals($key, $val, $prefix, $ikey, $time = null, $algo = "sha1")
{
$ts = ($time ? $time : time());
$parts = explode('|', $val);
if (count($parts) !== 3) {
return null;
}
list($u_prefix, $u_b64, $u_sig) = $parts;
$sig = hash_hmac($algo, $u_prefix . '|' . $u_b64, $key);
if (hash_hmac($algo, $sig, $key) !== hash_hmac($algo, $u_sig, $key)) {
return null;
}
if ($u_prefix !== $prefix) {
return null;
}
$cookie_parts = explode('|', base64_decode($u_b64));
if (count($cookie_parts) !== 3) {
return null;
}
list($user, $u_ikey, $exp) = $cookie_parts;
if ($u_ikey !== $ikey) {
return null;
}
if ($ts >= intval($exp)) {
return null;
}
return $user;
}
public static function signRequest($ikey, $skey, $akey, $username, $time = null)
{
if (!isset($username) || strlen($username) === 0) {
return self::ERR_USER;
}
if (strpos($username, '|') !== false) {
return self::ERR_USER;
}
if (!isset($ikey) || strlen($ikey) !== self::IKEY_LEN) {
return self::ERR_IKEY;
}
if (!isset($skey) || strlen($skey) !== self::SKEY_LEN) {
return self::ERR_SKEY;
}
if (!isset($akey) || strlen($akey) < self::AKEY_LEN) {
return self::ERR_AKEY;
}
$vals = $username . '|' . $ikey;
$duo_sig = self::signVals($skey, $vals, self::DUO_PREFIX, self::DUO_EXPIRE, $time);
$app_sig = self::signVals($akey, $vals, self::APP_PREFIX, self::APP_EXPIRE, $time);
return $duo_sig . ':' . $app_sig;
}
public static function verifyResponse($ikey, $skey, $akey, $sig_response, $time = null)
{
list($auth_sig, $app_sig) = explode(':', $sig_response);
$auth_user = self::parseVals($skey, $auth_sig, self::AUTH_PREFIX, $ikey, $time);
$app_user = self::parseVals($akey, $app_sig, self::APP_PREFIX, $ikey, $time);
if ($auth_user !== $app_user) {
return null;
}
return $auth_user;
}
public static function initAuth($client, $ikey, $akey, $username, $enroll_only = false)
{
if (!isset($username) || strlen($username) === 0) {
return self::ERR_USER;
}
if (strpos($username, '|') !== false) {
return self::ERR_USER;
}
if (!isset($ikey) || strlen($ikey) !== self::IKEY_LEN) {
return self::ERR_IKEY;
}
if (!isset($akey) || strlen($akey) < self::AKEY_LEN) {
return self::ERR_AKEY;
}
$blob = $username . '|' . $ikey;
$signed_blob = self::signVals(
$akey,
$blob,
self::APP_PREFIX,
self::APP_EXPIRE,
null,
'sha512'
);
$expire = time() + self::INIT_EXPIRE;
$client_version = self::LIBRARY_NAME . '/' . self::VERSION;
$response = $client->init(
$username,
$signed_blob,
$expire,
$client_version,
$enroll_only
);
return $response['response']['response']['txid'];
}
public static function verifyAuth($client, $ikey, $akey, $response_txid)
{
$response = $client->auth_response($response_txid);
$username = $response['response']['response']['uname'];
$signed_blob = $response['response']['response']['app_blob'];
$parsed_user = self::parseVals(
$akey,
$signed_blob,
self::APP_PREFIX,
$ikey,
null,
'sha512'
);
if ($username !== $parsed_user) {
return null;
}
return $username;
}
}

View File

@ -330,16 +330,17 @@ function pwd_is_enabled($hash) {
}
/**
* Generates a random password with 12 digits.
* Generates a random password with 12 digits by default.
*
* @param int $length length of password (defaults to 12)
* @return String password
*/
function generateRandomPassword() {
function generateRandomPassword($length = 12) {
$list = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_';
$password = '';
$length = $_SESSION['cfgMain']->passwordMinLength;
if ($length < 12) {
$length = 12;
$minLength = $_SESSION['cfgMain']->passwordMinLength;
if ($minLength > $length) {
$length = $minLength;
}
for ($x = 0; $x < 10000; $x++) {
$password = '';

View File

@ -3715,6 +3715,42 @@ class htmlJavaScript extends htmlElement {
}
/**
* Creates a iframe element.
*
* @package metaHTML
*/
class htmlIframe extends htmlElement {
/** HTML id */
private $id = null;
/**
* Constructor.
*
* @param String $content script
*/
function __construct($id = null) {
$this->id = $id;
}
/**
* {@inheritDoc}
* @see htmlElement::generateHTML()
*/
function generateHTML($module, $input, $values, $restricted, &$tabindex, $scope) {
$return = array();
$idAttr = '';
if (!empty($this->id)) {
$idAttr = ' id="' . $this->id . '"';
}
echo '<iframe ' . $idAttr . $this->getDataAttributesAsString() . '>';
echo '</iframe>';
return $return;
}
}
/**
* Creates a Script element to integrate external JavaScript files.
*

View File

@ -687,7 +687,7 @@ function getSecurityTokenValue() {
function setLAMHeaders() {
if (!headers_sent()) {
header('X-Frame-Options: sameorigin');
header('Content-Security-Policy: frame-ancestors \'self\'; form-action \'self\'; base-uri \'none\'; object-src \'none\'; frame-src \'self\'; worker-src \'self\'');
header('Content-Security-Policy: frame-ancestors \'self\'; form-action \'self\'; base-uri \'none\'; object-src \'none\'; frame-src \'self\' https://*.duosecurity.com; worker-src \'self\'');
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1; mode=block');
}

View File

@ -461,6 +461,7 @@ if (extension_loaded('curl')) {
_('None') => TwoFactorProviderService::TWO_FACTOR_NONE,
'privacyIDEA' => TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA,
'YubiKey' => TwoFactorProviderService::TWO_FACTOR_YUBICO,
'Duo' => TwoFactorProviderService::TWO_FACTOR_DUO,
);
$twoFactorSelect = new htmlResponsiveSelect('twoFactor', $twoFactorOptions, array($conf->getTwoFactorAuthentication()), _('Provider'), '514');
$twoFactorSelect->setHasDescriptiveElements(true);
@ -469,12 +470,15 @@ if (extension_loaded('curl')) {
'twoFactorOptional', 'twoFactorCaption', 'twoFactorClientId', 'twoFactorSecretKey', 'twoFactorAttribute'),
TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA => array('twoFactorClientId', 'twoFactorSecretKey'),
TwoFactorProviderService::TWO_FACTOR_YUBICO => array('twoFactorAttribute'),
TwoFactorProviderService::TWO_FACTOR_DUO => array('twoFactorOptional', 'twoFactorInsecure'),
));
$twoFactorSelect->setTableRowsToShow(array(
TwoFactorProviderService::TWO_FACTOR_PRIVACYIDEA => array('twoFactorURL', 'twoFactorInsecure', 'twoFactorLabel',
'twoFactorOptional', 'twoFactorCaption', 'twoFactorAttribute'),
TwoFactorProviderService::TWO_FACTOR_YUBICO => array('twoFactorURL', 'twoFactorInsecure', 'twoFactorLabel',
'twoFactorOptional', 'twoFactorCaption', 'twoFactorClientId', 'twoFactorSecretKey'),
TwoFactorProviderService::TWO_FACTOR_DUO => array('twoFactorURL', 'twoFactorLabel',
'twoFactorCaption', 'twoFactorClientId', 'twoFactorSecretKey', 'twoFactorAttribute'),
));
$row->add($twoFactorSelect, 12);
$twoFactorAttribute = new htmlResponsiveInputField(_("User name attribute"), 'twoFactorAttribute', $conf->getTwoFactorAuthenticationAttribute(), '528');

View File

@ -0,0 +1,578 @@
/**
* Duo Web SDK v2
* Copyright 2019, Duo Security
*/
(function (root, factory) {
/*eslint-disable */
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
/*eslint-enable */
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
var Duo = factory();
// If the Javascript was loaded via a script tag, attempt to autoload
// the frame.
Duo._onReady(Duo.init);
// Attach Duo to the `window` object
root.Duo = Duo;
}
}(this, function() {
var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/;
var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/;
var DUO_OPEN_WINDOW_FORMAT = /^DUO_OPEN_WINDOW\|/;
var VALID_OPEN_WINDOW_DOMAINS = [
'duo.com',
'duosecurity.com',
'duomobile.s3-us-west-1.amazonaws.com'
];
var postAction,
postArgument,
host,
sigRequest,
duoSig,
appSig,
iframe,
submitCallback;
// We use this function instead of setting initial values in the var
// declarations to make sure the initial values and subsequent
// re-initializations are always the same.
initializeStatefulVariables();
/**
* Set local variables to whatever they should be before you call init().
*/
function initializeStatefulVariables() {
postAction = '';
postArgument = 'sig_response';
host = undefined;
sigRequest = undefined;
duoSig = undefined;
appSig = undefined;
iframe = undefined;
submitCallback = undefined;
}
function throwError(message, givenUrl) {
var url = (
givenUrl ||
'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
);
throw new Error(
'Duo Web SDK error: ' + message +
(url ? ('\n' + 'See ' + url + ' for more information') : '')
);
}
function hyphenize(str) {
return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase();
}
// cross-browser data attributes
function getDataAttribute(element, name) {
if ('dataset' in element) {
return element.dataset[name];
} else {
return element.getAttribute('data-' + hyphenize(name));
}
}
// cross-browser event binding/unbinding
function on(context, event, fallbackEvent, callback) {
if ('addEventListener' in window) {
context.addEventListener(event, callback, false);
} else {
context.attachEvent(fallbackEvent, callback);
}
}
function off(context, event, fallbackEvent, callback) {
if ('removeEventListener' in window) {
context.removeEventListener(event, callback, false);
} else {
context.detachEvent(fallbackEvent, callback);
}
}
function onReady(callback) {
on(document, 'DOMContentLoaded', 'onreadystatechange', callback);
}
function offReady(callback) {
off(document, 'DOMContentLoaded', 'onreadystatechange', callback);
}
function onMessage(callback) {
on(window, 'message', 'onmessage', callback);
}
function offMessage(callback) {
off(window, 'message', 'onmessage', callback);
}
/**
* Parse the sig_request parameter, throwing errors if the token contains
* a server error or if the token is invalid.
*
* @param {String} sig Request token
*/
function parseSigRequest(sig) {
if (!sig) {
// nothing to do
return;
}
// see if the token contains an error, throwing it if it does
if (sig.indexOf('ERR|') === 0) {
throwError(sig.split('|')[1]);
}
// validate the token
if (sig.indexOf(':') === -1 || sig.split(':').length !== 2) {
throwError(
'Duo was given a bad token. This might indicate a configuration ' +
'problem with one of Duo\'s client libraries.'
);
}
var sigParts = sig.split(':');
// hang on to the token, and the parsed duo and app sigs
sigRequest = sig;
duoSig = sigParts[0];
appSig = sigParts[1];
return {
sigRequest: sig,
duoSig: sigParts[0],
appSig: sigParts[1]
};
}
/**
* Validate that a MessageEvent came from the Duo service, and that it
* is a properly formatted payload.
*
* The Google Chrome sign-in page injects some JS into pages that also
* make use of postMessage, so we need to do additional validation above
* and beyond the origin.
*
* @param {MessageEvent} event Message received via postMessage
*/
function isDuoMessage(event) {
return Boolean(
event.origin === ('https://' + host) &&
typeof event.data === 'string' &&
(
event.data.match(DUO_MESSAGE_FORMAT) ||
event.data.match(DUO_ERROR_FORMAT) ||
event.data.match(DUO_OPEN_WINDOW_FORMAT)
)
);
}
/**
* Validate the request token and prepare for the iframe to become ready.
*
* All options below can be passed into an options hash to `Duo.init`, or
* specified on the iframe using `data-` attributes.
*
* Options specified using the options hash will take precedence over
* `data-` attributes.
*
* Example using options hash:
* ```javascript
* Duo.init({
* iframe: "some_other_id",
* host: "api-main.duo.test",
* sig_request: "...",
* post_action: "/auth",
* post_argument: "resp"
* });
* ```
*
* Example using `data-` attributes:
* ```html
* <iframe id="duo_iframe"
* data-host="api-main.duo.test"
* data-sig-request="..."
* data-post-action="/auth"
* data-post-argument="resp"
* >
* </iframe>
* ```
*
* Some browsers (especially embedded browsers) don't like it when the Duo
* Web SDK changes the `src` attribute on the iframe. To prevent this, there
* is an alternative way to use the Duo Web SDK:
*
* Add a div (or any other container element) instead of an iframe to the
* DOM with an id of "duo_iframe", or pass that element to the
* `iframeContainer` parameter of `Duo.init`. An iframe will be created and
* inserted into that container element, preventing `src` change related
* bugs. WARNING: All other elements in the container will be deleted.
*
* The `iframeAttributes` parameter of `Duo.init` is available to set any
* attributes on the inserted iframe if the Duo Web SDK is inserting the
* iframe. For details, see the parameter documentation below.
*
* @param {Object} options
* @param {String} options.host - Hostname for the Duo Prompt.
* @param {String} options.sig_request - Request token.
* @param {String|HTMLElement} [options.iframe] - The iframe, or id of an
* iframe that will be used for the Duo Prompt. If you don't provide
* this or the `iframeContainer` parameter the Duo Web SDK will default
* to using whatever element has an id of "duo_iframe".
* @param {String|HTMLElement} [options.iframeContainer] - The element you
* want the Duo Prompt inserted into, or the id of that element.
* Anything inside this element will be deleted and replaced with an
* iframe hosting the Duo prompt. If you don't provide this or the
* `iframe` parameter the Duo Web SDK will default to using whatever
* element has an id of "duo_iframe".
* @param {Object} [options.iframeAttributes] - Object with names and
* values coresponding to attributes you want added to the Duo Prompt
* iframe, like `title`, `width` and `allow`. WARNING: this parameter
* only works if you use the `iframeContainer` parameter or add an id
* of "duo_iframe" to an element that isn't an iframe. If you have
* added an iframe to the DOM yourself, you should set those attributes
* directly on the iframe.
* @param {String} [options.post_action=''] - URL to POST back to after a
* successful auth.
* @param {String} [options.post_argument='sig_response'] - Parameter name
* to use for response token.
* @param {Function} [options.submit_callback] - If provided, the Duo Web
* SDK will not submit the form. Instead it will execute this callback
* function passing in a reference to the "duo_form" form object.
* `submit_callback`` can be used to prevent the webpage from reloading.
*/
function init(options) {
// If init() is called more than once we have to reset all the local
// variables to ensure init() will work the same way every time. This
// helps people making single page applications. SPAs may periodically
// remove the iframe and add a new one that has to be initialized.
initializeStatefulVariables();
if (options) {
if (options.host) {
host = options.host;
}
if (options.sig_request) {
parseSigRequest(options.sig_request);
}
if (options.post_action) {
postAction = options.post_action;
}
if (options.post_argument) {
postArgument = options.post_argument;
}
if (typeof options.submit_callback === 'function') {
submitCallback = options.submit_callback;
}
}
var promptElement = getPromptElement(options);
if (promptElement) {
// If we can get the element that will host the prompt, set it.
ready(promptElement, options.iframeAttributes || {});
} else {
// If the element that will host the prompt isn't available yet, set
// it up after the DOM finishes loading.
asyncReady(options);
}
// always clean up after yourself!
offReady(init);
}
/**
* Given the options from init(), get the iframe or iframe container that
* should be used for the Duo Prompt. Returns `null` if nothing was found.
*/
function getPromptElement(options) {
var result;
if (options.iframe && options.iframeContainer) {
throwError(
'Passing both `iframe` and `iframeContainer` arguments at the' +
' same time is not allowed.'
);
} else if (options.iframe) {
// If we are getting an iframe, try to get it and raise if the
// element we find is NOT an iframe.
result = getUserDefinedElement(options.iframe);
validateIframe(result);
} else if (options.iframeContainer) {
result = getUserDefinedElement(options.iframeContainer);
validateIframeContainer(result);
} else {
result = document.getElementById('duo_iframe');
}
return result;
}
/**
* When given an HTMLElement, return it. When given a string, get an element
* with that id, else return null.
*/
function getUserDefinedElement(object) {
if (object.tagName) {
return object;
} else if (typeof object == 'string') {
return document.getElementById(object);
}
return null;
}
/**
* Check if the given thing is an iframe.
*/
function isIframe(element) {
return (
element &&
element.tagName &&
element.tagName.toLowerCase() === 'iframe'
);
}
/**
* Throw an error if we are given an element that is NOT an iframe.
*/
function validateIframe(element) {
if (element && !isIframe(element)) {
throwError(
'`iframe` only accepts an iframe element or the id of an' +
' iframe. To use a non-iframe element, use the' +
' `iframeContainer` argument.'
);
}
}
/**
* Throw an error if we are given an element that IS an iframe instead of an
* element that we can insert an iframe into.
*/
function validateIframeContainer(element) {
if (element && isIframe(element)) {
throwError(
'`iframeContainer` only accepts a non-iframe element or the' +
' id of a non-iframe. To use a non-iframe element, use the' +
' `iframeContainer` argument on Duo.init().'
);
}
}
/**
* Generate the URL that goes to the Duo Prompt.
*/
function generateIframeSrc() {
return [
'https://', host, '/frame/web/v1/auth?tx=', duoSig,
'&parent=', encodeURIComponent(document.location.href),
'&v=2.8'
].join('');
}
/**
* This function is called when a message was received from another domain
* using the `postMessage` API. Check that the event came from the Duo
* service domain, and that the message is a properly formatted payload,
* then perform the post back to the primary service.
*
* @param event Event object (contains origin and data)
*/
function onReceivedMessage(event) {
if (isDuoMessage(event)) {
if (event.data.match(DUO_OPEN_WINDOW_FORMAT)) {
var url = event.data.substring("DUO_OPEN_WINDOW|".length);
if (isValidUrlToOpen(url)) {
// Open the URL that comes after the DUO_WINDOW_OPEN token.
window.open(url, "_self");
}
}
else {
// the event came from duo, do the post back
doPostBack(event.data);
// always clean up after yourself!
offMessage(onReceivedMessage);
}
}
}
/**
* Validate that this passed in URL is one that we will actually allow to
* be opened.
* @param url String URL that the message poster wants to open
* @returns {boolean} true if we allow this url to be opened in the window
*/
function isValidUrlToOpen(url) {
if (!url) {
return false;
}
var parser = document.createElement('a');
parser.href = url;
if (parser.protocol === "duotrustedendpoints:") {
return true;
} else if (parser.protocol !== "https:") {
return false;
}
for (var i = 0; i < VALID_OPEN_WINDOW_DOMAINS.length; i++) {
if (parser.hostname.endsWith("." + VALID_OPEN_WINDOW_DOMAINS[i]) ||
parser.hostname === VALID_OPEN_WINDOW_DOMAINS[i]) {
return true;
}
}
return false;
}
/**
* Register a callback to call ready() after the DOM has loaded.
*/
function asyncReady(options) {
var callback = function() {
var promptElement = getPromptElement(options);
if (!promptElement) {
throwError(
'This page does not contain an iframe for Duo to use.' +
' Add an element like' +
' <iframe id="duo_iframe"></iframe> to this page.'
);
}
ready(promptElement, options.iframeAttributes || {});
// Always clean up after yourself.
offReady(callback)
};
onReady(callback);
}
/**
* Point the iframe at Duo, then wait for it to postMessage back to us.
*/
function ready(promptElement, iframeAttributes) {
if (!host) {
host = getDataAttribute(promptElement, 'host');
if (!host) {
throwError(
'No API hostname is given for Duo to use. Be sure to pass ' +
'a `host` parameter to Duo.init, or through the `data-host` ' +
'attribute on the iframe element.'
);
}
}
if (!duoSig || !appSig) {
parseSigRequest(getDataAttribute(promptElement, 'sigRequest'));
if (!duoSig || !appSig) {
throwError(
'No valid signed request is given. Be sure to give the ' +
'`sig_request` parameter to Duo.init, or use the ' +
'`data-sig-request` attribute on the iframe element.'
);
}
}
// if postAction/Argument are defaults, see if they are specified
// as data attributes on the iframe
if (postAction === '') {
postAction = getDataAttribute(promptElement, 'postAction') || postAction;
}
if (postArgument === 'sig_response') {
postArgument = getDataAttribute(promptElement, 'postArgument') || postArgument;
}
if (isIframe(promptElement)) {
iframe = promptElement;
iframe.src = generateIframeSrc();
} else {
// If given a container to put an iframe in, clean out any children
// child elements in case `init()` was called more than once.
while (promptElement.firstChild) {
// We call `removeChild()` instead of doing `innerHTML = ""`
// to make sure we unbind any events.
promptElement.removeChild(promptElement.firstChild)
}
iframe = document.createElement('iframe');
// Set the src and all other attributes on the new iframe.
iframeAttributes['src'] = generateIframeSrc();
for (var name in iframeAttributes) {
iframe.setAttribute(name, iframeAttributes[name]);
}
promptElement.appendChild(iframe);
}
// listen for the 'message' event
onMessage(onReceivedMessage);
}
/**
* We received a postMessage from Duo. POST back to the primary service
* with the response token, and any additional user-supplied parameters
* given in form#duo_form.
*/
function doPostBack(response) {
// create a hidden input to contain the response token
var input = document.createElement('input');
input.type = 'hidden';
input.name = postArgument;
input.value = response + ':' + appSig;
// user may supply their own form with additional inputs
var form = document.getElementById('duo_form');
// if the form doesn't exist, create one
if (!form) {
form = document.createElement('form');
// insert the new form after the iframe
iframe.parentElement.insertBefore(form, iframe.nextSibling);
}
// make sure we are actually posting to the right place
form.method = 'POST';
form.action = postAction;
// add the response token input to the form
form.appendChild(input);
// away we go!
if (typeof submitCallback === "function") {
submitCallback.call(null, form);
} else {
form.submit();
}
}
return {
init: init,
_onReady: onReady,
_parseSigRequest: parseSigRequest,
_isDuoMessage: isDuoMessage,
_doPostBack: doPostBack
};
}));

View File

@ -84,10 +84,10 @@ if (isset($_POST['logout'])) {
exit();
}
if (isset($_POST['submit'])) {
$twoFactorInput = $_POST['2factor'];
$serial = $_POST['serial'];
if (empty($twoFactorInput) || !in_array($serial, $serials)) {
if (isset($_POST['submit']) || isset($_POST['sig_response'])) {
$twoFactorInput = isset($_POST['2factor']) ? $_POST['2factor'] : null;
$serial = isset($_POST['serial']) ? $_POST['serial'] : null;
if (!$provider->hasCustomInputForm() && (empty($twoFactorInput) || !in_array($serial, $serials))) {
$errorMessage = _(sprintf('Please enter "%s".', $twoFactorLabel));
}
else {
@ -148,21 +148,31 @@ echo $config->getTwoFactorAuthenticationCaption();
$row->add(new \htmlStatusMessage('ERROR', $errorMessage), 12);
$row->add(new htmlSpacer('1em', '1em'), 12);
}
// serial
$row->add(new htmlOutputText(_('Serial number')), 12, 12, 12, 'text-left');
$serialSelect = new htmlSelect('serial', $serials);
$row->add($serialSelect, 12);
// token
$row->add(new htmlOutputText($twoFactorLabel), 12, 12, 12, 'text-left');
$twoFactorInput = new htmlInputField('2factor', '');
$twoFactorInput->setFieldSize(null);
$twoFactorInput->setIsPassword(true);
$row->add($twoFactorInput, 12);
if (!$provider->hasCustomInputForm()) {
// serial
$row->add(new htmlOutputText(_('Serial number')), 12, 12, 12, 'text-left');
$serialSelect = new htmlSelect('serial', $serials);
$row->add($serialSelect, 12);
// token
$row->add(new htmlOutputText($twoFactorLabel), 12, 12, 12, 'text-left');
$twoFactorInput = new htmlInputField('2factor', '');
$twoFactorInput->setFieldSize(null);
$twoFactorInput->setIsPassword(true);
$row->add($twoFactorInput, 12);
}
else {
$provider->addCustomInput($row, $user);
}
// buttons
$row->add(new htmlSpacer('1em', '1em'), 12);
$submit = new htmlButton('submit', _("Submit"));
$submit->setCSSClasses(array('fullwidth'));
$row->add($submit, 12, 12, 12, 'fullwidth');
$row->add(new htmlSpacer('0.5em', '0.5em'), 12);
if ($provider->isShowSubmitButton()) {
$submit = new htmlButton('submit', _("Submit"));
$submit->setCSSClasses(array('fullwidth'));
$row->add($submit, 12, 12, 12, 'fullwidth');
$row->add(new htmlSpacer('0.5em', '0.5em'), 12);
}
$logout = new htmlButton('logout', _("Cancel"));
$logout->setCSSClasses(array('fullwidth'));
$row->add($logout, 12);