Moodle authentication plugin for Macaroons

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Authentication plugin: Macaroons
  18. *
  19. * Macaroons: Cookies with Contextual Caveats for Decentralized Authorization
  20. *
  21. * @package auth_macaroons
  22. * @author Brendan Abolivier
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
  24. */
  25. defined('MOODLE_INTERNAL') || die();
  26. require_once($CFG->libdir.'/authlib.php');
  27. require_once($CFG->dirroot.'/auth/macaroons/Macaroons/Macaroon.php');
  28. require_once($CFG->dirroot.'/auth/macaroons/Macaroons/Caveat.php');
  29. require_once($CFG->dirroot.'/auth/macaroons/Macaroons/Packet.php');
  30. require_once($CFG->dirroot.'/auth/macaroons/Macaroons/Utils.php');
  31. require_once($CFG->dirroot.'/auth/macaroons/Macaroons/Verifier.php');
  32. require_once($CFG->dirroot.'/auth/macaroons/Macaroons/Exceptions/CaveatUnsatisfiedException.php');
  33. require_once($CFG->dirroot.'/auth/macaroons/Macaroons/Exceptions/InvalidMacaroonKeyException.php');
  34. require_once($CFG->dirroot.'/auth/macaroons/Macaroons/Exceptions/SignatureMismatchException.php');
  35. use Macaroons\Macaroon;
  36. use Macaroons\Verifier;
  37. /**
  38. * Plugin for no authentication.
  39. */
  40. class auth_plugin_macaroons extends auth_plugin_base {
  41. /*
  42. * The name of the component. Used by the configuration.
  43. */
  44. const COMPONENT_NAME = 'auth_macaroons';
  45. /**
  46. * Constructor.
  47. */
  48. public function __construct() {
  49. $this->authtype = 'macaroons';
  50. $this->config = get_config(self::COMPONENT_NAME);
  51. }
  52. /**
  53. * Old syntax of class constructor. Deprecated in PHP7.
  54. *
  55. * @deprecated since Moodle 3.1
  56. */
  57. public function auth_plugin_macaroons() {
  58. debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
  59. self::__construct();
  60. }
  61. /* Login page hook
  62. *
  63. * Called before displaying the login form, is used to authenticate the user
  64. * and bypass the form.
  65. */
  66. function loginpage_hook() {
  67. global $DB, $login, $CFG;
  68. if(!empty($_COOKIE[$this->config->cookie_name])) {
  69. try {
  70. // Getting the macaroon from the cookie it's stored in
  71. $m = Macaroon::deserialize($_COOKIE[$this->config->cookie_name]);
  72. $callbacks = array();
  73. // Defining the callbacks according to the plugin's configuration
  74. // in order to check all caveats
  75. if(!empty($this->config->caveat1_condition)) {
  76. array_push($callbacks, function($a) {
  77. return !strcmp($a, $this->config->caveat1_condition);
  78. });
  79. }
  80. if(!empty($this->config->caveat2_condition)) {
  81. array_push($callbacks, function($a) {
  82. return !strcmp($a, $this->config->caveat2_condition);
  83. });
  84. }
  85. if(!empty($this->config->caveat3_condition)) {
  86. array_push($callbacks, function($a) {
  87. return !strcmp($a, $this->config->caveat3_condition);
  88. });
  89. }
  90. $v = new Verifier();
  91. $v->setCallbacks($callbacks);
  92. // This will check both the signature and the caveats. Both must be OK
  93. // in order to continue
  94. if($v->verify($m, $this->config->secret)) {
  95. $identifier = explode(";", $m->getIdentifier());
  96. $parsed_id = $this->parse_identifier($identifier);
  97. if(empty($parsed_id["username"])) {
  98. $login = $parsed_id["firstname"].$parsed_id["lastname"];
  99. } else {
  100. $login = $parsed_id["username"];
  101. }
  102. // Checking if the user is accepted by at least one authentication
  103. // method (ours should accept it), and retrieving the user's class
  104. // This will create the user if it doesn't exist
  105. $user = authenticate_user_login($login, null);
  106. if($user) {
  107. if(!empty($parsed_id["firstname"])) {
  108. $user->firstname = $parsed_id["firstname"];
  109. }
  110. if(!empty($parsed_id["lastname"])) {
  111. $user->lastname = $parsed_id["lastname"];
  112. }
  113. // Generating the user's e-mail address according
  114. // to its name and the config's template
  115. $placeholders[0] = "/{{firstname}}/";
  116. $placeholders[1] = "/{{lastname}}/";
  117. $user->email = preg_replace($placeholders, [
  118. $parsed_id["firstname"],
  119. $parsed_id["lastname"]
  120. ], $this->config->email_config);
  121. // Register modifications in DB, and logging the user in
  122. $DB->update_record('user', $user);
  123. complete_user_login($user);
  124. // Authentication is OK, let's redirect the user out of
  125. // the login page
  126. redirect($CFG->wwwroot);
  127. }
  128. }
  129. } catch(Exception $e) {
  130. // We currently do nothing with exceptions
  131. $message = $e->getMessage();
  132. }
  133. }
  134. }
  135. /*
  136. * Parses the macaroon identifier based on the user's config
  137. *
  138. * @param array $identifier The macaroon identifier split based on the separator
  139. * @return array A map linking a field with an user value
  140. */
  141. function parse_identifier($identifier) {
  142. $placeholders = explode(";", $this->config->identifier_format);
  143. $parsed_id = array();
  144. // Check if the identifier has the same number of fields as configured
  145. if(sizeof($placeholders) != sizeof($identifier)) {
  146. // Returning an empty array as the return value is expected to be
  147. // an array
  148. return $parsed_id;
  149. }
  150. // Filling the fields
  151. if(is_numeric($index = array_search("{{username}}", $placeholders))) {
  152. $parsed_id["username"] = $identifier[$index];
  153. }
  154. if(is_numeric($index = array_search("{{firstname}}", $placeholders))) {
  155. $parsed_id["firstname"] = $identifier[$index];
  156. }
  157. if(is_numeric($index = array_search("{{lastname}}", $placeholders))) {
  158. $parsed_id["lastname"] = $identifier[$index];
  159. }
  160. return $parsed_id;
  161. }
  162. /**
  163. * Returns true if the username and password work or don't exist and false
  164. * if the user exists and the password is wrong.
  165. *
  166. * @param string $username The username
  167. * @param string $password The password
  168. * @return bool Authentication success or failure.
  169. */
  170. function user_login ($username, $password) {
  171. global $login;
  172. if($login == $username) {
  173. return true;
  174. }
  175. return false;
  176. }
  177. /**
  178. * Updates the user's password.
  179. *
  180. * called when the user password is updated.
  181. *
  182. * @param object $user User table object
  183. * @param string $newpassword Plaintext password
  184. * @return boolean result
  185. *
  186. */
  187. function user_update_password($user, $newpassword) {
  188. $user = get_complete_user_data('id', $user->id);
  189. // This will also update the stored hash to the latest algorithm
  190. // if the existing hash is using an out-of-date algorithm (or the
  191. // legacy md5 algorithm).
  192. return update_internal_user_password($user, $newpassword);
  193. }
  194. function prevent_local_passwords() {
  195. return false;
  196. }
  197. /**
  198. * Returns true if this authentication plugin is 'internal'.
  199. *
  200. * @return bool
  201. */
  202. function is_internal() {
  203. return false;
  204. }
  205. /**
  206. * Returns true if this authentication plugin can change the user's
  207. * password.
  208. *
  209. * @return bool
  210. */
  211. function can_change_password() {
  212. return true;
  213. }
  214. /**
  215. * Returns the URL for changing the user's pw, or empty if the default can
  216. * be used.
  217. *
  218. * @return moodle_url
  219. */
  220. function change_password_url() {
  221. return null;
  222. }
  223. /**
  224. * Returns true if plugin allows resetting of internal password.
  225. *
  226. * @return bool
  227. */
  228. function can_reset_password() {
  229. return true;
  230. }
  231. /**
  232. * Returns true if plugin can be manually set.
  233. *
  234. * @return bool
  235. */
  236. function can_be_manually_set() {
  237. return true;
  238. }
  239. /**
  240. * Prints a form for configuring this authentication plugin.
  241. *
  242. * This function is called from admin/auth.php, and outputs a full page with
  243. * a form for configuring this plugin.
  244. *
  245. * @param array $page An object containing all the data for this page.
  246. */
  247. function config_form($config, $err, $user_fields) {
  248. include "config.html";
  249. }
  250. /**
  251. * Processes and stores configuration data for this authentication plugin.
  252. */
  253. function process_config($config) {
  254. if(!isset($config->cookie_name)) {
  255. $config->cookie_name = 'das-macaroon';
  256. }
  257. if(!isset($config->secret)) {
  258. $config->secret = 'pocsecret';
  259. }
  260. if(!isset($config->identifier_format)) {
  261. $config->identifier_format = '{{firstname}};{{lastname}}';
  262. }
  263. if(!isset($config->email_config)) {
  264. $config->email_config = '{{firstname}}.{{lastname}}@company.tld';
  265. }
  266. // Caveats
  267. if(!isset($config->caveat1_condition)) {
  268. $config->caveat1_condition = '';
  269. }
  270. if(!isset($config->caveat2_condition)) {
  271. $config->caveat2_condition = '';
  272. }
  273. if(!isset($config->caveat3_condition)) {
  274. $config->caveat3_condition = '';
  275. }
  276. set_config('cookie_name', $config->cookie_name, self::COMPONENT_NAME);
  277. set_config('secret', $config->secret, self::COMPONENT_NAME);
  278. set_config('identifier_format', $config->identifier_format, self::COMPONENT_NAME);
  279. set_config('email_config', $config->email_config, self::COMPONENT_NAME);
  280. // Caveats
  281. set_config('caveat1_condition', $config->caveat1_condition, self::COMPONENT_NAME);
  282. set_config('caveat2_condition', $config->caveat2_condition, self::COMPONENT_NAME);
  283. set_config('caveat3_condition', $config->caveat3_condition, self::COMPONENT_NAME);
  284. return true;
  285. }
  286. function is_synchronised_with_external() {
  287. return false;
  288. }
  289. }