From 015333afe98d72a42b72b2f4879fa7a1b9aa0460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20P=C3=A9rez-Cerezo?= Date: Wed, 17 Apr 2019 09:00:02 +0200 Subject: Repurpose plugin as minetest authentication. --- MinetestPasswordPrimaryAuthenticationProvider.php | 201 ++++++++++++++ MoodlePasswordPrimaryAuthenticationProvider.php | 310 ---------------------- README.md | 34 ++- extension.json | 11 +- minetestauth.py | 77 ++++++ 5 files changed, 309 insertions(+), 324 deletions(-) create mode 100644 MinetestPasswordPrimaryAuthenticationProvider.php delete mode 100644 MoodlePasswordPrimaryAuthenticationProvider.php create mode 100644 minetestauth.py diff --git a/MinetestPasswordPrimaryAuthenticationProvider.php b/MinetestPasswordPrimaryAuthenticationProvider.php new file mode 100644 index 0000000..7d2fb19 --- /dev/null +++ b/MinetestPasswordPrimaryAuthenticationProvider.php @@ -0,0 +1,201 @@ +minetestUrl = $params['minetestUrl']; + } + + 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->getMinetestUserToken( $req->username, $req->password ); + + if ( $token === false ) { + return AuthenticationResponse::newAbstain(); + + } else { + return AuthenticationResponse::newPass( $username ); + } + } + + /** + * Prepares a curl handler to use for querying the Minetest web services. + * + * @param string $url + * @return resource + */ + protected function getMinetestCurlClient( $url ) { + + $curl = curl_init( $url ); + + curl_setopt_array( $curl, [ + CURLOPT_USERAGENT => 'MWAuthMinetestBot/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 Minetest. Checks if user is authenticated. + * + * @param string $username + * @param string $password + * @return bool False on error, true otherwise + */ + protected function getMinetestUserToken( $username, $password ) { + + $curl = $this->getMinetestCurlClient( $this->minetestUrl.'/query' ); + + $params = http_build_query( [ + 'name' => $username, + 'password' => $password, + ] ); + + 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 ); + + sleep(2); + + $query2 = $this->getMinetestCurlClient( $this->minetestUrl.'/status/'.$username ); + $ret = curl_exec ( $query2 ); + + + if ( !empty( $error ) ) { + $this->logger->error( 'AuthMinetest: cURL error: '.$error ); + return false; + + } else if ( $info['http_code'] != 200 ) { + $this->logger->error( 'AuthMinetest: cURL error: unexpected HTTP response code '.$info['http_code'] ); + return false; + + } + if ( $ret == "True" ) { + return true; + + } else { + return false; + } + + } + + /** + * @param null|\User $user + * @param AuthenticationResponse $response + */ + public function postAuthentication( $user, AuthenticationResponse $response ) { + if ( $response->status !== AuthenticationResponse::PASS ) { + return; + } + return; + } + + + 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 Minetest 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/MoodlePasswordPrimaryAuthenticationProvider.php b/MoodlePasswordPrimaryAuthenticationProvider.php deleted file mode 100644 index 2799e9a..0000000 --- a/MoodlePasswordPrimaryAuthenticationProvider.php +++ /dev/null @@ -1,310 +0,0 @@ -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->exception ) ) { - $this->logger->error( 'AuthMoodle: Remote exception: '.$decoded->exception ); - return false; - - } 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 ); - $user->confirmEmail(); - $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; - } - - if ( isset( $decoded->exception ) ) { - $this->logger->error( 'AuthMoodle: Remote exception: '.$decoded->exception ); - 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/README.md b/README.md index fcddb68..34b6bfc 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,52 @@ -# AuthMoodle +# AuthMinetest -Extension for MediaWiki allowing to authenticate users against Moodle database via mobile app service. +Extension for MediaWiki allowing to authenticate users against a Minetest server ## Requirements: * MediaWiki 1.27+ -* Moodle 3.1+ with mobile app service enabled +* Minetest with auth_export mod enabled +* minetestauth.py duct-tape http server ## Installation and setup -Clone / unzip into your MediaWiki's extension/AuthMoodle/ folder. +Clone / unzip into your MediaWiki's extension/AuthMinetest/ folder. Configure your MediaWiki authentication manager to use this extension as the primary authentication provider: - wfLoadExtension( 'AuthMoodle' ); + wfLoadExtension( 'AuthMinetest' ); $wgAuthManagerAutoConfig['primaryauth'] = [ - MediaWiki\Auth\MoodlePasswordPrimaryAuthenticationProvider::class => [ - 'class' => MediaWiki\Auth\MoodlePasswordPrimaryAuthenticationProvider::class, + MediaWiki\Auth\MinetestPasswordPrimaryAuthenticationProvider::class => [ + 'class' => MediaWiki\Auth\MinetestPasswordPrimaryAuthenticationProvider::class, 'args' => [ [ - 'moodleUrl' => 'https://your.moodle.url', + 'minetestUrl' => 'http://your.minetest.url', ] ], 'sort' => 0, ], - ]; + ]; + +Enable the auth\_export mod on your minetest server, and install the +minetestauth.py script. You then need to point both the auth\_export +mod and this plugin to the minetestauth.py script like this: + + auth_export ←→ minetestauth.py ←→ AuthMinetest + +Keep in mind that stuff is sent in plain text, so you should have all +of these listening on localhost or use encrypted tunnels between the +hosts, such as ssh or vpn tunnels. ## Copying +This extension is based on +[AuthMoodle](https://github.com/moodlehq/mediawiki-authmoodle) by +David Mudrák. + Copyright 2017 David Mudrák +Copyright 2019 Gabriel Pérez-Cerezo This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/extension.json b/extension.json index 024966a..46a0ab1 100644 --- a/extension.json +++ b/extension.json @@ -1,15 +1,16 @@ { - "name": "AuthMoodle", + "name": "AuthMinetest", "version": "1.0.0", "author": [ - "David Mudrák" + "Gabriel Pérez-Cerezo", + "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", + "url": "https://git.bananach.space/MinetestAuth.git", + "description": "Extension for MediaWiki allowing to authenticate users against Minetest servers", "license-name": "GPL-3.0+", "type": "auth", "AutoloadClasses": { - "MediaWiki\\Auth\\MoodlePasswordPrimaryAuthenticationProvider": "MoodlePasswordPrimaryAuthenticationProvider.php" + "MediaWiki\\Auth\\MinetestPasswordPrimaryAuthenticationProvider": "MinetestPasswordPrimaryAuthenticationProvider.php" }, "manifest_version": 1 } diff --git a/minetestauth.py b/minetestauth.py new file mode 100644 index 0000000..eec3ba1 --- /dev/null +++ b/minetestauth.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# +# Primitive server to interface minetest with mediawiki +# Functionality: +# - responds to minetest server's requests for authentication +# - mediawiki posts request to server on the URL /query +# - mediawiki queries user auth status on the URL /status/$USERNAME +# +from http.server import BaseHTTPRequestHandler, HTTPServer +import socketserver +import cgi +import json +queue = [] +auth_status = {} +allowed = ["127.0.0.1"] +class S(BaseHTTPRequestHandler): + def check_allowed(self) : + if not self.client_address[0] in allowed : + self.send_response(403) + self.end_headers() + return False + return True + def _set_headers(self): + + self.send_response(200) + self.send_header('Content-type', 'text/json') + self.send_header('Connection', 'keepalive') + self.end_headers() + return True + + def do_GET(self): + if not self.check_allowed() : + return + self._set_headers() + if self.path == "/api/minetest/channel" : + if queue : + k = queue.pop() + self.wfile.write((' { "data" : { "name": %s, "password" : %s }, "type": "auth"} ' % (json.dumps(k[0]),json.dumps(k[1]))).encode()) + else : + self.wfile.write("{}".encode()) + elif self.path.startswith("/status/"): + name = self.path.split("/")[-1] + if not name in auth_status : + self.wfile.write("Unknown".encode()) + else : + self.wfile.write(str(auth_status[name]).encode()) + del auth_status[name] + def do_POST(self): + if not self.check_allowed() : + return + self._set_headers() + if self.path == "/query" : + form = cgi.FieldStorage( + fp=self.rfile, + headers=self.headers, + environ={'REQUEST_METHOD': 'POST'} + ) + user = form.getvalue("name") + pwd = form.getvalue("password") + queue.append((user,pwd)) + else: # User has been identified + js = self.rfile.read().decode() + k = json.loads(js) + auth_status[k["data"]["name"]] = k["data"]["success"] + +def run(port=8000): + server_address = ('127.0.0.1', port) + httpd = HTTPServer(server_address, S) + httpd.serve_forever() + +if __name__ == "__main__": + from sys import argv + + if len(argv) == 2: + run(port=int(argv[1])) + else: + run() -- cgit v1.2.3