From a94afb1febd3f982a5e1ec0bedb1cb4985023496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Thu, 28 Sep 2017 14:04:22 +0200 Subject: Initial implementation --- MoodlePasswordPrimaryAuthenticationProvider.php | 302 ++++++++++++++++++++++++ extension.json | 15 ++ 2 files changed, 317 insertions(+) create mode 100644 MoodlePasswordPrimaryAuthenticationProvider.php create mode 100644 extension.json diff --git a/MoodlePasswordPrimaryAuthenticationProvider.php b/MoodlePasswordPrimaryAuthenticationProvider.php new file mode 100644 index 0000000..212c6bf --- /dev/null +++ b/MoodlePasswordPrimaryAuthenticationProvider.php @@ -0,0 +1,302 @@ +moodleUrl = $params['moodleUrl']; + } + + public function beginPrimaryAuthentication( array $reqs ) { + $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class ); + if ( !$req ) { + return AuthenticationResponse::newAbstain(); + } + + if ( $req->username === null || $req->password === null ) { + return AuthenticationResponse::newAbstain(); + } + + $username = User::getCanonicalName( $req->username, 'usable' ); + if ( $username === false ) { + return AuthenticationResponse::newAbstain(); + } + + $token = $this->getMoodleUserToken( $req->username, $req->password ); + + if ( $token === false ) { + return AuthenticationResponse::newAbstain(); + + } else { + $this->tokens[$username] = $token; + return AuthenticationResponse::newPass( $username ); + } + } + + /** + * Prepares a curl handler to use for querying the Moodle web services. + * + * @param string $url + * @return resource + */ + protected function getMoodleCurlClient( $url ) { + + $curl = curl_init( $url ); + + curl_setopt_array( $curl, [ + CURLOPT_USERAGENT => 'MWAuthMoodleBot/1.0', + CURLOPT_NOBODY => false, + CURLOPT_HEADER => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 10, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => 1, + CURLOPT_SSL_VERIFYHOST => 2, + ]); + + return $curl; + } + + /** + * Attempts to authenticate the user against Moodle and returns the auth token. + * + * @param string $username + * @param string $password + * @return string|bool False on error, token otherwise. + */ + protected function getMoodleUserToken( $username, $password ) { + + $curl = $this->getMoodleCurlClient( $this->moodleUrl.'/login/token.php' ); + + $params = http_build_query( [ + 'username' => $username, + 'password' => $password, + 'service' => 'moodle_mobile_app', + ] ); + + curl_setopt_array( $curl, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $params, + ]); + + $ret = curl_exec( $curl ); + $info = curl_getinfo( $curl ); + $error = curl_error( $curl ); + curl_close( $curl ); + + if ( !empty( $error ) ) { + $this->logger->error( 'AuthMoodle: cURL error: '.$error ); + return false; + + } else if ( $info['http_code'] != 200 ) { + $this->logger->error( 'AuthMoodle: cURL error: unexpected HTTP response code '.$info['http_code'] ); + return false; + + } else { + $decoded = @json_decode( $ret ); + if ( empty( $decoded ) ) { + $this->logger->error( 'AuthMoodle: Unable to decode the JSON response: '.$ret ); + return false; + } + } + + if ( !empty( $decoded->token ) ) { + return $decoded->token; + + } else if ( isset( $decoded->error ) ) { + $this->logger->error( 'AuthMoodle: Remote error: '.$decoded->error ); + return false; + + } else { + $this->logger->error( 'AuthMoodle: Unknown error: '.$ret ); + return false; + } + } + + /** + * @param null|\User $user + * @param AuthenticationResponse $response + */ + public function postAuthentication( $user, AuthenticationResponse $response ) { + if ( $response->status !== AuthenticationResponse::PASS ) { + return; + } + + if ( empty( $this->tokens[$user->getName()] ) ) { + $this->logger->error( 'AuthMoodle: Moodle token not found' ); + return; + } + + $userinfo = $this->getMoodleUserInfo( $user->getName(), $this->tokens[$user->getName()] ); + + if ( empty( $userinfo ) ) { + $this->logger->error( 'AuthMoodle: Empty user info, skipping update '); + return; + } + + if ( $user->getRealName() === '' ) { + // Set the user's real name if they are logging in for the first time. Also note MDLSITE-1293. + $this->logger->debug( 'AuthMoodle: Setting the user real name' ); + $mwdbr = wfGetDB( DB_SLAVE ); + $realname = $userinfo->fullname; + $counter = 1; + while ( $mwdbr->selectField( 'user', 'user_name', ['user_real_name' => $realname] ) && $counter < 100 ) { + $counter++; + $realname = $userinfo->fullname.' '.$counter; + } + $user->setRealName( $realname ); + } + + $user->setEmail( $userinfo->email ); + + // TODO This should not be needed once https://bugzilla.wikimedia.org/show_bug.cgi?id=13963 is fixed. + $user->saveSettings(); + } + + /** + * Loads the Moodle user's real name and email. + * + * @param string $username + * @param string $token + * @return object|bool + */ + protected function getMoodleUserInfo( $username, $token ) { + + $this->logger->debug( 'AuthMoodle: Attempting to get info about the user: '.$username.' using the token: '.$token ); + + // Get the Moodle user id first. + + $params = http_build_query( [ + 'wstoken' => $token, + 'wsfunction' => 'core_webservice_get_site_info', + 'moodlewsrestformat' => 'json', + ] ); + + $curl = $this->getMoodleCurlClient( $this->moodleUrl.'/webservice/rest/server.php?'.$params ); + + $ret = curl_exec( $curl ); + curl_close( $curl ); + + $decoded = @json_decode( $ret ); + + if ( empty( $decoded->userid ) ) { + $this->logger->error( 'AuthMoodle: Unable to get Moodle user id' ); + return false; + } + + if ( strtolower( $decoded->username ) !== strtolower( $username ) ) { + $this->logger->error( 'AuthMoodle: User name mismatch' ); + return false; + } + + $moodleuserid = $decoded->userid; + + // Get the user profile. + + $params = http_build_query( [ + 'wstoken' => $token, + 'wsfunction' => 'core_user_get_users_by_field', + 'moodlewsrestformat' => 'json', + 'field' => 'id', + 'values' => [$moodleuserid], + ] ); + + $curl = $this->getMoodleCurlClient( $this->moodleUrl.'/webservice/rest/server.php?'.$params ); + + $ret = curl_exec( $curl ); + curl_close( $curl ); + + $decoded = @json_decode( $ret ); + + if ( empty( $decoded ) ) { + $this->logger->error( 'AuthMoodle: Unable to get Moodle user profile' ); + return false; + } + + return (object) [ + 'fullname' => $decoded[0]->fullname, + 'email' => $decoded[0]->email, + ]; + } + + public function testUserCanAuthenticate( $username ) { + return $this->testUserExists( $username ); + } + + public function testUserExists( $username, $flags = User::READ_NORMAL ) { + // TODO - there is no easy way to do this without additional web services on the Moodle side. + return false; + } + + public function providerAllowsPropertyChange( $property ) { + return false; + } + + public function providerAllowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true) { + return \StatusValue::newGood( 'ignored' ); + } + + public function providerChangeAuthenticationData( AuthenticationRequest $req ) { + return; + } + + public function accountCreationType() { + return self::TYPE_CREATE; + } + + public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) { + throw new \BadMethodCallException( 'This should not get called' ); + } + + public function getAuthenticationRequests( $action, array $options ) { + switch ( $action ) { + case AuthManager::ACTION_LOGIN: + return [ new PasswordAuthenticationRequest() ]; + default: + return []; + } + } +} diff --git a/extension.json b/extension.json new file mode 100644 index 0000000..7fba642 --- /dev/null +++ b/extension.json @@ -0,0 +1,15 @@ +{ + "name": "AuthMoodle", + "version": "1.0.0-beta", + "author": [ + "David Mudrák" + ], + "url": "https://github.com/moodlehq/mediawiki-authmoodle", + "description": "Extension for MediaWiki allowing to authenticate users against Moodle database via mobile app services" + "license-name": "GPL-3.0+", + "type": "auth", + "AutoloadClasses": { + "MediaWiki\\Auth\\MoodlePasswordPrimaryAuthenticationProvider": "MoodlePasswordPrimaryAuthenticationProvider.php" + }, + "manifest_version": 1 +} -- cgit v1.2.3