initial import

This commit is contained in:
Paweł Suwiński
2020-11-13 14:50:27 +01:00
parent 1563523513
commit fe35e4e96f
11 changed files with 678 additions and 0 deletions

11
README.md Normal file
View File

@@ -0,0 +1,11 @@
# Email One-Time Password Authentication
Matches only valid email from allowed domain as username. Validates client
credentials and password if exists in current session or generates ones for
session time on empty password treated as one-time password request.
On first login account is created if not prevented on global level
and parts of email address may be mapped to profile fields using
PCRE expressions.
See [setting form help](settings.php) for mapping usage example.

5
TODO.md Normal file
View File

@@ -0,0 +1,5 @@
TODO:
- unit tests
- js snippet to change login form (ex. to add "Generate OTP" button) on valid
email as username

222
auth.php Normal file
View File

@@ -0,0 +1,222 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Email OTP authentication plugin.
*
* @see self::user_login()
* @package auth_emailotp
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/authlib.php');
use core\output\notification;
/**
* Email OTP authentication plugin.
*
* @see self::user_login()
* @package auth_emailotp
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class auth_plugin_emailotp extends auth_plugin_base {
/**
* The name of the component. Used by the configuration.
*/
const COMPONENT_NAME = 'auth_emailotp';
/**
* Constructor.
*/
public function __construct() {
$this->authtype = 'emailotp';
$this->config = get_config(self::COMPONENT_NAME);
}
/**
* Old syntax of class constructor. Deprecated in PHP7.
*
* @deprecated since Moodle 3.1
*/
public function auth_plugin_emailotp() {
debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
self::__construct();
}
/**
* Matches only valid and allowed email as username. Validates credentials
* and password if exists in current session or generates ones for session
* time on empty password treated as one-time password request.
*
* @param string $username The username
* @param string $password The password
* @return bool Authentication success or failure.
*/
public function user_login($username, $password) {
global $CFG, $DB;
if (!validate_email($username) || email_is_not_allowed($username)) {
return false;
}
// OTP already generated and base credentials matches.
if (isset($_SESSION[self::COMPONENT_NAME]) &&
$_SESSION[self::COMPONENT_NAME]['credentials'] === static::get_credentials($username)) {
return empty($password)
? (bool) $this->redirect($username, notification::NOTIFY_INFO)
: password_verify($password, $_SESSION[self::COMPONENT_NAME]['password']);
}
// OTP request - do not proceed on preventaccountcreation when user not exits.
if (!isset($_SESSION[self::COMPONENT_NAME]) && empty($password) && (
empty($CFG->authpreventaccountcreation) || $DB->get_field('user', 'id', [
'username' => $username,
'mnethostid' => $CFG->mnet_localhost_id,
'auth' => $this->authtype,
'deleted' => 0,
]))) {
$this->redirect($username, $this->gen_otp($username)
? notification::NOTIFY_SUCCESS
: notification::NOTIFY_ERROR
);
}
return false;
}
/**
* {@inheritdoc}
*/
public function is_synchronised_with_external() {
return false;
}
/**
* {@inheritdoc}
*/
public function is_internal() {
return false;
}
/**
* {@inheritdoc}
*/
public function can_be_manually_set() {
return true;
}
/**
* {@inheritdoc}
*/
public function user_authenticated_hook(&$user, $username, $password) {
// Destroy credentials - is already used.
if (isset($_SESSION[self::COMPONENT_NAME])) {
unset($_SESSION[self::COMPONENT_NAME]);
}
}
/**
* {@inheritdoc}
*/
public function get_userinfo($username) {
$this->get_custom_user_profile_fields();
$fields = array('email' => $username);
if ($this->config->fieldsmapping_pattern &&
$this->config->fieldsmapping_mapping) {
$fields += array_filter(
(new \auth_emailotp\fields_mapper(
$this->config->fieldsmapping_pattern,
strtolower($username),
))->map(array_map(function($mapping) {
return trim($mapping);
}, explode(PHP_EOL, $this->config->fieldsmapping_mapping))),
function($key) {
return in_array($key, $this->userfields) ||
in_array($key, $this->customfields);
},
ARRAY_FILTER_USE_KEY
);
}
return $fields;
}
/**
* get_credentials
*
* @param string $username
* @return void
*/
protected static function get_credentials($username) {
return array(
'username' => $username,
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'ip' => getremoteaddr(),
);
}
/**
* gen_otp
*
* @param string $username
* @return bool
*/
protected function gen_otp(string $username) {
global $CFG;
$newpassword = generate_password();
$_SESSION[self::COMPONENT_NAME] = array(
'credentials' => static::get_credentials($username),
'password' => password_hash($newpassword, PASSWORD_DEFAULT),
);
$a = (object)array(
'username' => $username,
'password' => $newpassword,
);
return email_to_user(
(object)array(
'id' => -1,
'auth' => $this->authtype,
'username ' => $username,
'email' => $username,
),
core_user::get_support_user(),
sprintf(
'%s: %s',
format_string(get_site()->fullname),
(string)new lang_string('otpgeneratedsubj', self::COMPONENT_NAME, $a, $CFG->lang)
),
(string)new lang_string('otpgeneratedtext', self::COMPONENT_NAME, $a, $CFG->lang)
);
}
/**
* redirect
*
* @param string $username
* @param string $msg
* @return void
*/
protected function redirect(string $username, string $msg) {
global $CFG;
redirect(
get_login_url().'?username='.urlencode($username),
(string)new lang_string('otpsent'.$msg, self::COMPONENT_NAME, null, $CFG->lang),
null,
$msg
);
}
}

127
classes/fields_mapper.php Normal file
View File

@@ -0,0 +1,127 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Fields mapper.
*
* @package auth_emailotp
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace auth_emailotp;
defined('MOODLE_INTERNAL') || die();
/**
* fields_mapper
*
* Example of usage:
*
* (new fields_mapper(
* '#/?P<FIRST>[^\.]+)\.(?P<LAST>[^@]+)@(?P<COMPANY>[^\.]+).*#',
* 'my.name@corp.com'
* ))->map([
* 'firstname:FIRST:ucfirst',
* 'lastname:LAST:ucfirst',
* 'institution:COMPANY:strtoupper,
* ]);
*
* gives:
*
* ['firstname' => 'My', 'lastname' => 'Name', 'institution' => 'CORP']
*
* @package auth_emailotp
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class fields_mapper {
const FORMAT = '^(\w+)\W(.+)';
protected $replacepairs = null;
protected $allowedfilters = ['ucwords', 'ucfirst', 'strtoupper'];
/**
* __construct
*
* @param string $pattern Named capturing groups regexp patttern
* @param string $subject The string being translated
* @return void
*/
public function __construct(string $pattern, string $subject) {
$errhandler = set_error_handler(function ($severity, $message) {
if (error_reporting() & $severity) {
throw new \RuntimeException($message, $severity);
}
});
if (preg_match($pattern, $subject, $replacepairs)) {
array_shift($replacepairs);
$this->replacepairs = $replacepairs;
}
set_error_handler($errhandler);
}
/**
* set_allowed_filters
*
* @param array $allowedfilters Unary string functions names
* @throws InvalidArgumentException|ReflectionException
* @return self
*/
public function set_allowed_filters(array $allowedfilters) {
foreach ($allowedfilters as $filter) {
$reflection = new \ReflectionFunction($filter);
if ($reflection->getNumberOfParameters() !== 1 ||
$reflection->getNumberOfParameters()[0]->getType()->getName() != 'string') {
throw new \InvalidArgumentException('Expected unary string function as a filter!');
}
}
$this->allowedfilters = $allowedfilters;
return $this;
}
/**
* map_fields
*
* @param array $mapping
* @return array
*/
public function map(array $mapping) {
if (empty($this->replacepairs)) {
return array();
}
$allowedfilters = !empty($this->allowedfilters)
? '('.implode('|', $this->allowedfilters).')'
: '';
$fields = array();
foreach ($mapping as $map) {
$matches = array();
!empty($allowedfilters) &&
preg_match('/'.self::FORMAT.'\W'.$allowedfilters.'$/', $map, $matches) ||
preg_match('/'.self::FORMAT.'$/', $map, $matches);
if (count($matches) < 3) {
continue;
}
$value = strtr($matches[2], $this->replacepairs);
if (isset($matches[3])) {
$value = call_user_func($matches[3], $value);
}
$fields[$matches[1]] = $value;
}
return $fields;
}
}

View File

@@ -0,0 +1,41 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for auth_emailotp.
*
* @package auth_emailotp
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace auth_emailotp\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for auth_emailotp implementing null_provider.
*
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* @return string
*/
public static function get_reason() : string {
return 'privacy:metadata';
}
}

31
db/install.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Email OTP authentication plugin install code.
*
* @package auth_emailotp
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Function to install auth_emailotp.
*/
function xmldb_auth_emailotp_install() {
}

36
db/upgrade.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Email OTP authentication plugin upgrade code
*
* @package auth_emailotp
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Function to upgrade auth_emailotp.
* @param int $oldversion the version we are upgrading from
* @return bool result
*/
function xmldb_auth_emailotp_upgrade($oldversion) {
global $CFG;
return true;
}

58
lang/en/auth_emailotp.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component 'auth_emailotp', language 'en'.
*
* @package auth_emailotp
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Email OTP';
$string['otpgeneratedsubj'] = 'One-time password';
$string['otpgeneratedtext'] = 'One-time password for current session: {$a->password}';
$string['otpsentsuccess'] = 'One-time password was sent to given email.';
$string['otpsenterror'] = 'An error occurred while sending one-time password.';
$string['otpsentinfo'] = 'One-time password for current session was already generated and sent to email.';
$string['fieldsmapping'] = 'User profile fields mapping';
$string['fieldsmapping_pattern'] = 'Pattern';
$string['fieldsmapping_pattern_help'] = 'Capturing groups PCRE patttern.';
$string['fieldsmapping_mapping'] = 'Mapping';
$string['fieldsmapping_mapping_help'] = 'Mapping expressions.';
$string['fieldsmapping_help'] = <<<'EOT'
<p> Usage example:</p>
Pattern:<br />
<pre>
'#/?P<FIRST>[^\.]+)\.(?P<LAST>[^@]+)@(?P<COMPANY>[^\.]+).*#',
</pre>
Mapping:<br />
<pre>
firstname:FIRST:ucfirst
lastname:LAST:ucfirst
institution:COMPANY:strtoupper
</pre>
<p>maps <em>my.name@corp.com</em> to:</p>
firstname: My<br />
lastname: Name<br />
institution: CORP<br />
<p>Allowed modifiers: ucfirst, ucwords, strtoupper.</p>
EOT;

58
lang/pl/auth_emailotp.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component 'auth_emailotp', language 'pl'.
*
* @package auth_emailotp
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Email OTP';
$string['otpgeneratedsubj'] = 'Hasło jednorazowe';
$string['otpgeneratedtext'] = 'Hasło jednorazowe dla bieżącej sesji: {$a->password}';
$string['otpsentsuccess'] = 'Hasło jednorazowe zostało wysłane na podany adres email.';
$string['otpsenterror'] = 'Wystąpił błąd podczas wysyłania hasła jednorazowego.';
$string['otpsentinfo'] = 'Hasło jednorazowe dla bieżącej sesji już zostało wygenerowane i wyłane.';
$string['fieldsmapping'] = 'Mapowanie pól profilu użytkownika';
$string['fieldsmapping_pattern'] = 'Wzorzec';
$string['fieldsmapping_pattern_help'] = 'Grupujące wyrażenie regularne PCRE.';
$string['fieldsmapping_mapping'] = 'Mapowanie';
$string['fieldsmapping_mapping_help'] = 'Wyrażenie mapujące.';
$string['fieldsmapping_help'] = <<<'EOT'
<p> Przykład użycia:</p>
Wzorzec:<br />
<pre>
'#/?P<FIRST>[^\.]+)\.(?P<LAST>[^@]+)@(?P<COMPANY>[^\.]+).*#',
</pre>
Mapowanie:<br />
<pre>
firstname:FIRST:ucfirst
lastname:LAST:ucfirst
institution:COMPANY:strtoupper
</pre>
<p>odwzoruje <em>my.name@corp.com</em> na:</p>
firstname: My<br />
lastname: Name<br />
institution: CORP<br />
<p>Dozwolone modyfikatory: ucfirst, ucwords, strtoupper.</p>
EOT;

60
settings.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Admin settings and defaults
* @package auth_emailotp
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
if ($ADMIN->fulltree) {
$settings->add(new admin_setting_heading('auth_emailotp/pluginname',
new lang_string('fieldsmapping', 'auth_emailotp'),
new lang_string('fieldsmapping_help', 'auth_emailotp')));
$settings->add(new class(
'auth_emailotp/fieldsmapping_pattern',
get_string('fieldsmapping_pattern', 'auth_emailotp'),
get_string('fieldsmapping_pattern_help', 'auth_emailotp'),
'', PARAM_RAW_TRIMMED
) extends admin_setting_configtext {
public function validate($data) {
if (true !== $result = parent::validate($data)) {
return $result;
}
try {
new \auth_emailotp\fields_mapper($data, '');
} catch (\RuntimeException $e) {
return $e->getMessage();
}
return true;
}
});
$settings->add(new admin_setting_configtextarea('auth_emailotp/fieldsmapping_mapping',
get_string('fieldsmapping_mapping', 'auth_emailotp'),
get_string('fieldsmapping_mapping_help', 'auth_emailotp'), '', PARAM_RAW_TRIMMED));
// Display locking / mapping of profile fields.
$authplugin = get_auth_plugin('emailotp');
display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields,
get_string('auth_fieldlocks_help', 'auth'), false, false,
$authplugin->get_custom_user_profile_fields());
}

29
version.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Email OTP authentication plugin version information
*
* @package auth_emailotp
* @copyright 2020 Pawel Suwinski <psuw@wp.pl>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2020111002; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2020060900; // Requires this Moodle version.
$plugin->component = 'auth_emailotp'; // Full name of the plugin (used for diagnostics).