diff --git a/admin/tool/oauth2/userfieldmappings.php b/admin/tool/oauth2/userfieldmappings.php
index 1bf277d9069..d0961ceb9d8 100644
--- a/admin/tool/oauth2/userfieldmappings.php
+++ b/admin/tool/oauth2/userfieldmappings.php
@@ -15,7 +15,7 @@
// along with Moodle. If not, see .
/**
- * OAuth 2 Endpoing Configuration page.
+ * OAuth 2 Endpoint Configuration page.
*
* @package tool_oauth2
* @copyright 2017 Damyon Wiese
diff --git a/auth/oauth2/auth.php b/auth/oauth2/auth.php
index 0d5ecd7f79a..601dd4b2c4d 100644
--- a/auth/oauth2/auth.php
+++ b/auth/oauth2/auth.php
@@ -27,7 +27,8 @@ defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/authlib.php');
/**
- * Plugin for oauth2 authentication.
+ * Plugin for oauth2 authentication. This is a way to use namespaces even though
+ * moodle expects a non-namespaced file here.
*
* @package auth_oauth2
* @copyright 2017 Damyon Wiese
diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php
index 545a162f2dd..f188e439a8c 100644
--- a/auth/oauth2/classes/auth.php
+++ b/auth/oauth2/classes/auth.php
@@ -179,7 +179,12 @@ class auth extends \auth_plugin_base {
return false;
}
- private function is_ready_for_login_page($issuer) {
+ /**
+ * Do some checks on the identity provider before showing it on the login page.
+ * @param core\oauth2\issuer
+ * @return boolean
+ */
+ private function is_ready_for_login_page(\core\oauth2\issuer $issuer) {
return !empty($issuer->get('clientid')) &&
!empty($issuer->get('clientsecret')) &&
$issuer->is_authentication_supported() &&
@@ -188,6 +193,9 @@ class auth extends \auth_plugin_base {
/**
* Return a list of identity providers to display on the login page.
+ *
+ * @param string|moodle_url $wantsurl The requested URL.
+ * @return array (containing url, iconurl and name).
*/
public function loginpage_idp_list($wantsurl) {
$providers = \core\oauth2\api::get_all_issuers();
diff --git a/auth/oauth2/lang/en/auth_oauth2.php b/auth/oauth2/lang/en/auth_oauth2.php
index d221f328364..ce34fa5aae2 100644
--- a/auth/oauth2/lang/en/auth_oauth2.php
+++ b/auth/oauth2/lang/en/auth_oauth2.php
@@ -24,6 +24,6 @@
$string['auth_oauth2description'] = 'OAuth 2 standards based authentication';
$string['auth_oauth2settings'] = 'OAuth 2 authentication settings.';
-$string['pluginname'] = 'OAuth 2';
-$string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the moodle login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.';
$string['notloggedin'] = 'The login attempt failed.';
+$string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the moodle login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.';
+$string['pluginname'] = 'OAuth 2';
diff --git a/auth/oauth2/login.php b/auth/oauth2/login.php
index 835a39b9ba7..658c5ad134f 100644
--- a/auth/oauth2/login.php
+++ b/auth/oauth2/login.php
@@ -15,7 +15,7 @@
// along with Moodle. If not, see .
/**
- * Open ID authentication.
+ * Open ID authentication. This file is a simple login entry point for OAuth identity providers.
*
* @package auth_oauth2
* @copyright 2017 Damyon Wiese
diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php
index 0302d8bded0..3b4915e1de6 100644
--- a/lib/classes/oauth2/api.php
+++ b/lib/classes/oauth2/api.php
@@ -41,6 +41,10 @@ defined('MOODLE_INTERNAL') || die();
*/
class api {
+ /**
+ * Create a google ready OAuth 2 service.
+ * @return core\oauth2\issuer
+ */
private static function create_google() {
$record = (object) [
'name' => 'Google',
@@ -63,6 +67,10 @@ class api {
return $issuer;
}
+ /**
+ * Create a facebook ready OAuth 2 service.
+ * @return core\oauth2\issuer
+ */
private static function create_facebook() {
// Facebook is a custom setup.
$record = (object) [
@@ -115,6 +123,10 @@ class api {
return $issuer;
}
+ /**
+ * Create a microsoft ready OAuth 2 service.
+ * @return core\oauth2\issuer
+ */
private static function create_microsoft() {
// Microsoft is a custom setup.
$record = (object) [
@@ -182,26 +194,62 @@ class api {
}
}
+ /**
+ * List all the issuers, ordered by the sortorder field
+ * @return core\oauth2\issuer[]
+ */
public static function get_all_issuers() {
return issuer::get_records([], 'sortorder');
}
+ /**
+ * Get a single issuer by id.
+ *
+ * @param int $id
+ * @return core\oauth2\issuer
+ */
public static function get_issuer($id) {
return new issuer($id);
}
+ /**
+ * Get a single endpoint by id.
+ *
+ * @param int $id
+ * @return core\oauth2\endpoint
+ */
public static function get_endpoint($id) {
return new endpoint($id);
}
+ /**
+ * Get a single user field mapping by id.
+ *
+ * @param int $id
+ * @return core\oauth2\user_field_mapping
+ */
public static function get_user_field_mapping($id) {
return new user_field_mapping($id);
}
+ /**
+ * Get the system account for an installed OAuth service.
+ * Never ever ever expose this to a webservice because it contains the refresh token which grants API access.
+ *
+ * @param int $id
+ * @return core\oauth2\user_field_mapping
+ */
public static function get_system_account(issuer $issuer) {
return system_account::get_record(['issuerid' => $issuer->get('id')]);
}
+ /**
+ * Get the full list of system scopes required by an oauth issuer.
+ * This includes the list required for login as well as any scopes injected by the oauth2_system_scopes callback in plugins.
+ *
+ * @param core\oauth2\issuer $issuer
+ * @return string
+ */
public static function get_system_scopes_for_issuer($issuer) {
$scopes = $issuer->get('loginscopesoffline');
@@ -230,6 +278,13 @@ class api {
return $scopes;
}
+ /**
+ * Get an authenticated oauth2 client using the system account.
+ * This call uses the refresh token to get an access token.
+ *
+ * @param core\oauth2\issuer $issuer
+ * @return core\oauth2\client
+ */
public static function get_system_oauth_client(issuer $issuer) {
$systemaccount = self::get_system_account($issuer);
if (empty($systemaccount)) {
@@ -248,6 +303,15 @@ class api {
return $client;
}
+ /**
+ * Get an authenticated oauth2 client using the current user account.
+ * This call does the redirect dance back to the current page after authentication.
+ *
+ * @param core\oauth2\issuer $issuer The desired OAuth issuer
+ * @param moodle_url $url The url to the current page.
+ * @param string $additionalscopes The additional scopes required for authorization.
+ * @return core\oauth2\client
+ */
public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '') {
$client = new \core\oauth2\client($issuer, $currenturl, $additionalscopes);
@@ -257,14 +321,31 @@ class api {
return $client;
}
+ /**
+ * Get the list of defined endpoints for this OAuth issuer
+ *
+ * @param core\oauth2\issuer $issuer The desired OAuth issuer
+ * @return core\oauth2\endpoint[]
+ */
public static function get_endpoints(issuer $issuer) {
return endpoint::get_records(['issuerid' => $issuer->get('id')]);
}
+ /**
+ * Get the list of defined mapping from OAuth user fields to moodle user fields.
+ *
+ * @param core\oauth2\issuer $issuer The desired OAuth issuer
+ * @return core\oauth2\user_field_mapping[]
+ */
public static function get_user_field_mappings(issuer $issuer) {
return user_field_mapping::get_records(['issuerid' => $issuer->get('id')]);
}
+ /**
+ * Guess an image from the discovery URL.
+ *
+ * @param core\oauth2\issuer $issuer The desired OAuth issuer
+ */
protected static function guess_image($issuer) {
if (empty($issuer->get('image'))) {
$baseurl = parse_url($issuer->get('discoveryurl'));
@@ -362,6 +443,12 @@ class api {
return endpoint::count_records(['issuerid' => $issuer->get('id')]);
}
+ /**
+ * Take the data from the mform and update the issuer.
+ *
+ * @param stdClass $data
+ * @return core\oauth2\issuer
+ */
public static function update_issuer($data) {
require_capability('moodle/site:config', context_system::instance());
$issuer = new issuer(0, $data);
@@ -375,6 +462,12 @@ class api {
return $issuer;
}
+ /**
+ * Take the data from the mform and create the issuer.
+ *
+ * @param stdClass $data
+ * @return core\oauth2\issuer
+ */
public static function create_issuer($data) {
require_capability('moodle/site:config', context_system::instance());
$issuer = new issuer(0, $data);
@@ -388,6 +481,12 @@ class api {
return $issuer;
}
+ /**
+ * Take the data from the mform and update the endpoint.
+ *
+ * @param stdClass $data
+ * @return core\oauth2\endpoint
+ */
public static function update_endpoint($data) {
require_capability('moodle/site:config', context_system::instance());
$endpoint = new endpoint(0, $data);
@@ -398,6 +497,12 @@ class api {
return $endpoint;
}
+ /**
+ * Take the data from the mform and create the endpoint.
+ *
+ * @param stdClass $data
+ * @return core\oauth2\endpoint
+ */
public static function create_endpoint($data) {
require_capability('moodle/site:config', context_system::instance());
$endpoint = new endpoint(0, $data);
@@ -407,6 +512,12 @@ class api {
return $endpoint;
}
+ /**
+ * Take the data from the mform and update the user field mapping.
+ *
+ * @param stdClass $data
+ * @return core\oauth2\user_field_mapping
+ */
public static function update_user_field_mapping($data) {
require_capability('moodle/site:config', context_system::instance());
$userfieldmapping = new user_field_mapping(0, $data);
@@ -417,6 +528,12 @@ class api {
return $userfieldmapping;
}
+ /**
+ * Take the data from the mform and create the user field mapping.
+ *
+ * @param stdClass $data
+ * @return core\oauth2\user_field_mapping
+ */
public static function create_user_field_mapping($data) {
require_capability('moodle/site:config', context_system::instance());
$userfieldmapping = new user_field_mapping(0, $data);
@@ -459,6 +576,14 @@ class api {
return $result;
}
+ /**
+ * Reorder this identity issuer.
+ *
+ * Requires moodle/site:config capability at the system context.
+ *
+ * @param int $id The id of the identity issuer to move.
+ * @return boolean
+ */
public static function move_down_issuer($id) {
require_capability('moodle/site:config', context_system::instance());
$current = new issuer($id);
@@ -488,6 +613,14 @@ class api {
return $result;
}
+ /**
+ * Delete an identity issuer.
+ *
+ * Requires moodle/site:config capability at the system context.
+ *
+ * @param int $id The id of the identity issuer to delete.
+ * @return boolean
+ */
public static function delete_issuer($id) {
require_capability('moodle/site:config', context_system::instance());
$issuer = new issuer($id);
@@ -507,6 +640,14 @@ class api {
return $issuer->delete();
}
+ /**
+ * Delete an endpoint.
+ *
+ * Requires moodle/site:config capability at the system context.
+ *
+ * @param int $id The id of the endpoint to delete.
+ * @return boolean
+ */
public static function delete_endpoint($id) {
require_capability('moodle/site:config', context_system::instance());
$endpoint = new endpoint($id);
@@ -515,6 +656,14 @@ class api {
return $endpoint->delete();
}
+ /**
+ * Delete a user_field_mapping.
+ *
+ * Requires moodle/site:config capability at the system context.
+ *
+ * @param int $id The id of the user_field_mapping to delete.
+ * @return boolean
+ */
public static function delete_user_field_mapping($id) {
require_capability('moodle/site:config', context_system::instance());
$userfieldmapping = new user_field_mapping($id);
@@ -523,6 +672,15 @@ class api {
return $userfieldmapping->delete();
}
+ /**
+ * Perform the OAuth dance and get a refresh token.
+ *
+ * Requires moodle/site:config capability at the system context.
+ *
+ * @param core\oauth2\issuer $issuer
+ * @param moodle_url $returnurl The url to the current page (we will be redirected back here after authentication).
+ * @return boolean
+ */
public static function connect_system_account($issuer, $returnurl) {
require_capability('moodle/site:config', context_system::instance());
diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php
index 3947b0c3dac..4595205c1bd 100644
--- a/lib/classes/oauth2/client.php
+++ b/lib/classes/oauth2/client.php
@@ -146,6 +146,11 @@ class client extends \oauth2_client {
return $name;
}
+ /**
+ * Get a list of the mapping user fields in an associative array.
+ *
+ * @return array
+ */
protected function get_userinfo_mapping() {
$fields = user_field_mapping::get_records(['issuerid' => $this->issuer->get('id')]);
@@ -215,6 +220,12 @@ class client extends \oauth2_client {
return true;
}
+ /**
+ * Fetch the user info from the user info endpoint and map all
+ * the fields back into moodle fields.
+ *
+ * @return array (Moodle user fields for the logged in user).
+ */
public function get_userinfo() {
$url = $this->get_issuer()->get_endpoint_url('userinfo');
$response = $this->get($url);
diff --git a/lib/classes/oauth2/endpoint.php b/lib/classes/oauth2/endpoint.php
index 5916455a172..3a62e13e6d5 100644
--- a/lib/classes/oauth2/endpoint.php
+++ b/lib/classes/oauth2/endpoint.php
@@ -57,6 +57,13 @@ class endpoint extends persistent {
);
}
+ /**
+ * Custom validator for end point URLs.
+ * Because we send Bearer tokens we must ensure SSL.
+ *
+ * @param $value The value to check.
+ * @return boolean
+ */
protected function validate_url($value) {
if (strpos($value, 'https://') !== 0) {
return new lang_string('sslonlyaccess', 'error');
diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php
index 040d3e66ae8..5755c5062cd 100644
--- a/lib/classes/oauth2/issuer.php
+++ b/lib/classes/oauth2/issuer.php
@@ -96,6 +96,11 @@ class issuer extends persistent {
);
}
+ /**
+ * Helper the get a named service endpoint.
+ * @param string $type
+ * @return string|false
+ */
public function get_endpoint_url($type) {
$endpoint = endpoint::get_record([
'issuerid' => $this->get('id'),
@@ -108,14 +113,26 @@ class issuer extends persistent {
return false;
}
+ /**
+ * Does this OAuth service support user authentication?
+ * @return boolean
+ */
public function is_authentication_supported() {
return (!empty($this->get_endpoint_url('userinfo')));
}
+ /**
+ * Does this OAuth service support system authentication?
+ * @return boolean
+ */
public function is_system_account_setup_supported() {
return true;
}
+ /**
+ * Do we have a refresh token for a system account?
+ * @return boolean
+ */
public function is_system_account_connected() {
$sys = system_account::get_record(['issuerid' => $this->get('id')]);
if (!empty($sys) and !empty($sys->get('refreshtoken'))) {
diff --git a/lib/classes/oauth2/user_field_mapping.php b/lib/classes/oauth2/user_field_mapping.php
index 1ccf26b0e9e..0cf0178a5e2 100644
--- a/lib/classes/oauth2/user_field_mapping.php
+++ b/lib/classes/oauth2/user_field_mapping.php
@@ -72,6 +72,11 @@ class user_field_mapping extends persistent {
);
}
+ /**
+ * Return the list of internal fields
+ * in a format they can be used for choices in a select menu
+ * @return array
+ */
public function get_internalfield_list() {
return array_combine(self::$userfields, self::$userfields);
}
diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php
index 5f54b5621ce..88193ff83b4 100644
--- a/lib/classes/plugin_manager.php
+++ b/lib/classes/plugin_manager.php
@@ -1701,7 +1701,7 @@ class core_plugin_manager {
'auth' => array(
'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'lti', 'manual', 'mnet',
- 'nntp', 'nologin', 'none', 'pam', 'pop3', 'shibboleth', 'webservice'
+ 'nntp', 'nologin', 'none', 'oauth2', 'pam', 'pop3', 'shibboleth', 'webservice'
),
'availability' => array(
@@ -1902,7 +1902,7 @@ class core_plugin_manager {
'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'cohortroles', 'customlang',
'dbtransfer', 'filetypes', 'generator', 'health', 'innodb', 'installaddon',
'langimport', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound', 'mobile', 'multilangupgrade', 'monitor',
- 'phpunit', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary',
+ 'oauth2', 'phpunit', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary',
'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb'
),