MDL-59889 oauth2: Remove check for storing new refresh token
[moodle.git] / lib / classes / oauth2 / client.php
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/>.
17 /**
18  * Configurable oauth2 client class.
19  *
20  * @package    core
21  * @copyright  2017 Damyon Wiese
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 namespace core\oauth2;
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->libdir . '/oauthlib.php');
29 require_once($CFG->libdir . '/filelib.php');
31 use moodle_url;
32 use moodle_exception;
33 use stdClass;
35 /**
36  * Configurable oauth2 client class where the urls come from DB.
37  *
38  * @copyright  2017 Damyon Wiese
39  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class client extends \oauth2_client {
43     /** @var \core\oauth2\issuer $issuer */
44     private $issuer;
46     /** @var bool $system */
47     protected $system = false;
49     /**
50      * Constructor.
51      *
52      * @param issuer $issuer
53      * @param moodle_url|null $returnurl
54      * @param string $scopesrequired
55      * @param boolean $system
56      */
57     public function __construct(issuer $issuer, $returnurl, $scopesrequired, $system = false) {
58         $this->issuer = $issuer;
59         $this->system = $system;
60         $scopes = $this->get_login_scopes();
61         $additionalscopes = explode(' ', $scopesrequired);
63         foreach ($additionalscopes as $scope) {
64             if (!empty($scope)) {
65                 if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) {
66                     $scopes .= ' ' . $scope;
67                 }
68             }
69         }
70         if (empty($returnurl)) {
71             $returnurl = new moodle_url('/');
72         }
73         parent::__construct($issuer->get('clientid'), $issuer->get('clientsecret'), $returnurl, $scopes);
74     }
76     /**
77      * Returns the auth url for OAuth 2.0 request
78      * @return string the auth url
79      */
80     protected function auth_url() {
81         return $this->issuer->get_endpoint_url('authorization');
82     }
84     /**
85      * Get the oauth2 issuer for this client.
86      *
87      * @return \core\oauth2\issuer Issuer
88      */
89     public function get_issuer() {
90         return $this->issuer;
91     }
93     /**
94      * Override to append additional params to a authentication request.
95      *
96      * @return array (name value pairs).
97      */
98     public function get_additional_login_parameters() {
99         $params = '';
100         if ($this->system) {
101             if (!empty($this->issuer->get('loginparamsoffline'))) {
102                 $params = $this->issuer->get('loginparamsoffline');
103             }
104         } else {
105             if (!empty($this->issuer->get('loginparams'))) {
106                 $params = $this->issuer->get('loginparams');
107             }
108         }
109         if (empty($params)) {
110             return [];
111         }
112         $result = [];
113         parse_str($params, $result);
114         return $result;
115     }
117     /**
118      * Override to change the scopes requested with an authentiction request.
119      *
120      * @return string
121      */
122     protected function get_login_scopes() {
123         if ($this->system) {
124             return $this->issuer->get('loginscopesoffline');
125         } else {
126             return $this->issuer->get('loginscopes');
127         }
128     }
130     /**
131      * Returns the token url for OAuth 2.0 request
132      *
133      * We are overriding the parent function so we get this from the configured endpoint.
134      *
135      * @return string the auth url
136      */
137     protected function token_url() {
138         return $this->issuer->get_endpoint_url('token');
139     }
141     /**
142      * We want a unique key for each issuer / and a different key for system vs user oauth.
143      *
144      * @return string The unique key for the session value.
145      */
146     protected function get_tokenname() {
147         $name = 'oauth2-state-' . $this->issuer->get('id');
148         if ($this->system) {
149             $name .= '-system';
150         }
151         return $name;
152     }
154     /**
155      * Get a list of the mapping user fields in an associative array.
156      *
157      * @return array
158      */
159     protected function get_userinfo_mapping() {
160         $fields = user_field_mapping::get_records(['issuerid' => $this->issuer->get('id')]);
162         $map = [];
163         foreach ($fields as $field) {
164             $map[$field->get('externalfield')] = $field->get('internalfield');
165         }
166         return $map;
167     }
169     /**
170      * Upgrade a refresh token from oauth 2.0 to an access token
171      *
172      * @param \core\oauth2\system_account $systemaccount
173      * @return boolean true if token is upgraded succesfully
174      * @throws moodle_exception Request for token upgrade failed for technical reasons
175      */
176     public function upgrade_refresh_token(system_account $systemaccount) {
177         $refreshtoken = $systemaccount->get('refreshtoken');
179         $params = array('refresh_token' => $refreshtoken,
180             'client_id' => $this->issuer->get('clientid'),
181             'client_secret' => $this->issuer->get('clientsecret'),
182             'grant_type' => 'refresh_token'
183         );
185         // Requests can either use http GET or POST.
186         if ($this->use_http_get()) {
187             $response = $this->get($this->token_url(), $params);
188         } else {
189             $response = $this->post($this->token_url(), $this->build_post_data($params));
190         }
192         if ($this->info['http_code'] !== 200) {
193             throw new moodle_exception('Could not upgrade oauth token');
194         }
196         $r = json_decode($response);
198         if (!empty($r->error)) {
199             throw new moodle_exception($r->error . ' ' . $r->error_description);
200         }
202         if (!isset($r->access_token)) {
203             return false;
204         }
206         // Store the token an expiry time.
207         $accesstoken = new stdClass;
208         $accesstoken->token = $r->access_token;
209         if (isset($r->expires_in)) {
210             // Expires 10 seconds before actual expiry.
211             $accesstoken->expires = (time() + ($r->expires_in - 10));
212         }
213         $accesstoken->scope = $this->scope;
214         // Also add the scopes.
215         $this->store_token($accesstoken);
217         if (isset($r->refresh_token)) {
218             $systemaccount->set('refreshtoken', $r->refresh_token);
219             $systemaccount->update();
220             $this->refreshtoken = $r->refresh_token;
221         }
223         return true;
224     }
226     /**
227      * Fetch the user info from the user info endpoint and map all
228      * the fields back into moodle fields.
229      *
230      * @return array|false Moodle user fields for the logged in user (or false if request failed)
231      */
232     public function get_userinfo() {
233         $url = $this->get_issuer()->get_endpoint_url('userinfo');
234         $response = $this->get($url);
235         if (!$response) {
236             return false;
237         }
238         $userinfo = new stdClass();
239         try {
240             $userinfo = json_decode($response);
241         } catch (\Exception $e) {
242             return false;
243         }
245         $map = $this->get_userinfo_mapping();
247         $user = new stdClass();
248         foreach ($map as $openidproperty => $moodleproperty) {
249             // We support nested objects via a-b-c syntax.
250             $getfunc = function($obj, $prop) use (&$getfunc) {
251                 $proplist = explode('-', $prop, 2);
252                 if (empty($proplist[0]) || empty($obj->{$proplist[0]})) {
253                     return false;
254                 }
255                 $obj = $obj->{$proplist[0]};
257                 if (count($proplist) > 1) {
258                     return $getfunc($obj, $proplist[1]);
259                 }
260                 return $obj;
261             };
263             $resolved = $getfunc($userinfo, $openidproperty);
264             if (!empty($resolved)) {
265                 $user->$moodleproperty = $resolved;
266             }
267         }
269         if (empty($user->username) && !empty($user->email)) {
270             $user->username = $user->email;
271         }
273         if (!empty($user->picture)) {
274             $user->picture = download_file_content($user->picture, null, null, false, 10, 10, true, null, false);
275         } else {
276             $pictureurl = $this->issuer->get_endpoint_url('userpicture');
277             if (!empty($pictureurl)) {
278                 $user->picture = $this->get($pictureurl);
279             }
280         }
282         if (!empty($user->picture)) {
283             // If it doesn't look like a picture lets unset it.
284             if (function_exists('imagecreatefromstring')) {
285                 $img = @imagecreatefromstring($user->picture);
286                 if (empty($img)) {
287                     unset($user->picture);
288                 } else {
289                     imagedestroy($img);
290                 }
291             }
292         }
294         return (array)$user;
295     }