diff --git a/admin/tool/oauth2/issuers.php b/admin/tool/oauth2/issuers.php
index 4a636577c24..55b3eba2f68 100644
--- a/admin/tool/oauth2/issuers.php
+++ b/admin/tool/oauth2/issuers.php
@@ -222,6 +222,12 @@ if ($mform && $mform->is_cancelled()) {
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
echo $renderer->single_button($addurl, get_string('createnewnextcloudissuer', 'tool_oauth2'));
+ // Linkedin template.
+ $docs = 'admin/tool/oauth2/issuers/linkedin';
+ $params = ['action' => 'edittemplate', 'type' => 'linkedin', 'sesskey' => sesskey(), 'docslink' => $docs];
+ $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
+ echo $renderer->single_button($addurl, get_string('linkedin_service', 'tool_oauth2'));
+
// Generic issuer.
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']);
echo $renderer->single_button($addurl, get_string('createnewissuer', 'tool_oauth2'));
diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php
index 6e5545f99b1..50a5eed70f2 100644
--- a/admin/tool/oauth2/lang/en/tool_oauth2.php
+++ b/admin/tool/oauth2/lang/en/tool_oauth2.php
@@ -85,6 +85,7 @@ $string['issuershowonloginpage'] = 'Show on login page';
$string['issuerrequireconfirmation_help'] = 'Require that all users verify their email address before they can log in with OAuth. This applies to newly created accounts as part of the login process, or when an existing Moodle account is connected to an OAuth login via matching email addresses.';
$string['issuerrequireconfirmation'] = 'Require email verification';
$string['issuers'] = 'Issuers';
+$string['linkedin_service'] = 'Create new LinkedIn service';
$string['loginissuer'] = 'Allow login';
$string['notconfigured'] = 'Not configured';
$string['notdiscovered'] = 'Service discovery not successful';
diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php
index 165dc40afe9..59dfb6bab88 100644
--- a/lib/classes/oauth2/api.php
+++ b/lib/classes/oauth2/api.php
@@ -289,6 +289,68 @@ class api {
}
}
+ /**
+ * Create a linkedin OAuth2 issuer
+ *
+ * @return issuer
+ */
+ private static function init_linkedin(): issuer {
+ $record = (object) [
+ 'name' => 'LinkedIn',
+ 'image' => 'https://static.licdn.com/scds/common/u/images/logos/favicons/v1/favicon.ico',
+ 'baseurl' => 'https://api.linkedin.com/v2',
+ 'loginscopes' => 'r_liteprofile r_emailaddress',
+ 'loginscopesoffline' => 'r_liteprofile r_emailaddress',
+ 'showonloginpage' => true
+ ];
+
+ $issuer = new issuer(0, $record);
+ return $issuer;
+ }
+
+ /**
+ * Create endpoints for linkedin issuers.
+ *
+ * @param issuer $issuer
+ * @throws \coding_exception
+ * @throws \core\invalid_persistent_exception
+ */
+ private static function create_endpoints_for_linkedin(issuer $issuer) {
+ $endpoints = [
+ 'authorization_endpoint' => 'https://www.linkedin.com/oauth/v2/authorization',
+ 'token_endpoint' => 'https://www.linkedin.com/oauth/v2/accessToken',
+ 'email_endpoint' => 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))',
+ 'userinfo_endpoint' => "https://api.linkedin.com/v2/me?projection=(localizedFirstName,localizedLastName,"
+ . "profilePicture(displayImage~digitalmediaAsset:playableStreams))",
+ ];
+ foreach ($endpoints as $name => $url) {
+ $record = (object) [
+ 'issuerid' => $issuer->get('id'),
+ 'name' => $name,
+ 'url' => $url
+ ];
+ $endpoint = new endpoint(0, $record);
+ $endpoint->create();
+ }
+
+ // Create the field mappings.
+ $mapping = [
+ 'localizedFirstName' => 'firstname',
+ 'localizedLastName' => 'lastname',
+ 'elements[0]-handle~-emailAddress' => 'email',
+ 'profilePicture-displayImage~-elements[0]-identifiers[0]-identifier' => 'picture'
+ ];
+ foreach ($mapping as $external => $internal) {
+ $record = (object) [
+ 'issuerid' => $issuer->get('id'),
+ 'externalfield' => $external,
+ 'internalfield' => $internal
+ ];
+ $userfieldmapping = new user_field_mapping(0, $record);
+ $userfieldmapping->create();
+ }
+ }
+
/**
* Initializes a record for one of the standard issuers to be displayed in the settings.
* The issuer is not yet created in the database.
@@ -305,6 +367,8 @@ class api {
return self::init_facebook();
} else if ($type == 'nextcloud') {
return self::init_nextcloud();
+ } else if ($type == 'linkedin') {
+ return self::init_linkedin();
} else {
throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
}
@@ -328,6 +392,8 @@ class api {
return self::create_endpoints_for_facebook($issuer);
} else if ($type == 'nextcloud') {
return self::create_endpoints_for_nextcloud($issuer);
+ } else if ($type == 'linkedin') {
+ return self::create_endpoints_for_linkedin($issuer);
} else {
throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
}
@@ -466,8 +532,8 @@ class api {
}
// Get all the scopes!
$scopes = self::get_system_scopes_for_issuer($issuer);
-
- $client = new \core\oauth2\client($issuer, null, $scopes, true);
+ $class = self::get_client_classname($issuer->get('name'));
+ $client = new $class($issuer, null, $scopes, true);
if (!$client->is_logged_in()) {
if (!$client->upgrade_refresh_token($systemaccount)) {
@@ -489,11 +555,30 @@ class api {
*/
public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '',
$autorefresh = false) {
- $client = new \core\oauth2\client($issuer, $currenturl, $additionalscopes, false, $autorefresh);
+ $class = self::get_client_classname($issuer->get('name'));
+ $client = new $class($issuer, $currenturl, $additionalscopes, false, $autorefresh);
return $client;
}
+ /**
+ * Get the client classname for an issuer.
+ *
+ * @param string $type The OAuth issuer name
+ * @return string The classname for the custom client or core client class if the class for the defined type
+ * doesn't exist or null type is defined.
+ */
+ protected static function get_client_classname(?string $type): string {
+ // Default core client class.
+ $classname = 'core\\oauth2\\client';
+
+ if (strpos(strtolower($type), 'linkedin') !== false) {
+ $classname = 'core\\oauth2\\client\\linkedin';
+ }
+
+ return $classname;
+ }
+
/**
* Get the list of defined endpoints for this OAuth issuer
*
@@ -893,8 +978,8 @@ class api {
$scopes = self::get_system_scopes_for_issuer($issuer);
// Allow callbacks to inject non-standard scopes to the auth request.
-
- $client = new client($issuer, $returnurl, $scopes, true);
+ $class = self::get_client_classname($issuer->get('name'));
+ $client = new $class($issuer, $returnurl, $scopes, true);
if (!optional_param('response', false, PARAM_BOOL)) {
$client->log_out();
diff --git a/lib/classes/oauth2/client/linkedin.php b/lib/classes/oauth2/client/linkedin.php
new file mode 100644
index 00000000000..521dd766af8
--- /dev/null
+++ b/lib/classes/oauth2/client/linkedin.php
@@ -0,0 +1,63 @@
+.
+
+namespace core\oauth2\client;
+
+use core\oauth2\client;
+
+/**
+ * Class linkedin - Custom client handler to fetch data from linkedin
+ *
+ * Custom oauth2 client for linkedin as it doesn't support OIDC and has a different way to get
+ * key information for users - firstname, lastname, email.
+ *
+ * @copyright 2021 Peter Dias
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package core
+ */
+class linkedin extends client {
+ /**
+ * Fetch the user info from the userinfo and email endpoint and map fields back
+ *
+ * @return array|false
+ */
+ public function get_userinfo() {
+ $user = array_merge(parent::get_userinfo(), $this->get_useremail());
+ return $user;
+ }
+
+ /**
+ * Get the email address of the user from the email endpoint
+ *
+ * @return array|false
+ */
+ private function get_useremail() {
+ $url = $this->get_issuer()->get_endpoint_url('email');
+
+ $response = $this->get($url);
+ if (!$response) {
+ return false;
+ }
+ $userinfo = new \stdClass();
+ try {
+ $userinfo = json_decode($response);
+ } catch (\Exception $e) {
+ return false;
+ }
+
+ return $this->map_userinfo_to_fields($userinfo);
+ }
+}
diff --git a/lib/db/install.xml b/lib/db/install.xml
index 87a8826c2fd..91aca1f90db 100644
--- a/lib/db/install.xml
+++ b/lib/db/install.xml
@@ -3851,7 +3851,7 @@
-
+
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php
index dcd3c5a8cfa..5bbb35c51e3 100644
--- a/lib/db/upgrade.php
+++ b/lib/db/upgrade.php
@@ -3059,5 +3059,15 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2020110903.05);
}
+ if ($oldversion < 2020110904.05) {
+ // Update the externalfield to be larger.
+ $table = new xmldb_table('oauth2_user_field_mapping');
+ $field = new xmldb_field('externalfield', XMLDB_TYPE_CHAR, '500', null, XMLDB_NOTNULL, false, null, 'issuerid');
+ $dbman->change_field_type($table, $field);
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2020110904.05);
+ }
+
return true;
}
diff --git a/version.php b/version.php
index 50cb993c05d..65c96c75048 100644
--- a/version.php
+++ b/version.php
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
-$version = 2020110904.04; // 20201109 = branching date YYYYMMDD - do not modify!
+$version = 2020110904.05; // 20201109 = branching date YYYYMMDD - do not modify!
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '3.10.4+ (Build: 20210527)';// Human-friendly version name