From 6969d2834bc1162a9ebc8af85024551a977042ba Mon Sep 17 00:00:00 2001
From: Juan Leyva <juanleyvadelgado@gmail.com>
Date: Tue, 28 Jun 2022 12:18:58 +0200
Subject: [PATCH] MDL-74862 tool_mobile: Allow to disable QR login IP checks

---
 admin/tool/mobile/classes/api.php            |  2 +-
 admin/tool/mobile/lang/en/tool_mobile.php    |  2 +
 admin/tool/mobile/settings.php               |  5 ++
 admin/tool/mobile/tests/externallib_test.php | 71 ++++++++++++++++++++
 admin/tool/mobile/version.php                |  2 +-
 5 files changed, 80 insertions(+), 2 deletions(-)

diff --git a/admin/tool/mobile/classes/api.php b/admin/tool/mobile/classes/api.php
index 72e1151ab9c..2e450449bfe 100644
--- a/admin/tool/mobile/classes/api.php
+++ b/admin/tool/mobile/classes/api.php
@@ -402,7 +402,7 @@ class api {
         delete_user_key('tool_mobile', $USER->id);
 
         // Create a new key.
-        $iprestriction = getremoteaddr(null);
+        $iprestriction = !empty($mobilesettings->qrsameipcheck) ? getremoteaddr(null) : null;
         $qrkeyttl = !empty($mobilesettings->qrkeyttl) ? $mobilesettings->qrkeyttl : self::LOGIN_QR_KEY_TTL;
         $validuntil = time() + $qrkeyttl;
         return create_user_key('tool_mobile', $USER->id, null, $iprestriction, $validuntil);
diff --git a/admin/tool/mobile/lang/en/tool_mobile.php b/admin/tool/mobile/lang/en/tool_mobile.php
index 578d848435d..caba6915cc7 100644
--- a/admin/tool/mobile/lang/en/tool_mobile.php
+++ b/admin/tool/mobile/lang/en/tool_mobile.php
@@ -125,6 +125,8 @@ $string['qrcodetypeurl'] = 'QR code with site URL';
 $string['qrcodetypelogin'] = 'QR code with automatic login';
 $string['qrkeyttl'] = 'QR authentication key duration';
 $string['qrkeyttl_desc'] = 'The length of time for which a QR code for automatic login is valid.';
+$string['qrsameipcheck'] = 'QR authentication same IP check';
+$string['qrsameipcheck_desc'] = 'This setting forces users to use the same network for both generating and scanning a QR code for login. Only disable it in case your users are reporting issues with QR login.';
 $string['readingthisemailgettheapp'] = 'Reading this in an email? <a href="{$a}">Download the mobile app and receive notifications on your mobile device</a>.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['scanqrcode'] = 'Scan QR code';
diff --git a/admin/tool/mobile/settings.php b/admin/tool/mobile/settings.php
index e9db68c89ba..b2e3630e2a1 100644
--- a/admin/tool/mobile/settings.php
+++ b/admin/tool/mobile/settings.php
@@ -122,6 +122,11 @@ if ($hassiteconfig) {
         new lang_string('qrkeyttl_desc', 'tool_mobile'), tool_mobile\api::LOGIN_QR_KEY_TTL, MINSECS));
     $temp->hide_if('tool_mobile/qrkeyttl', 'tool_mobile/qrcodetype', 'neq', tool_mobile\api::QR_CODE_LOGIN);
 
+    $temp->add(new admin_setting_configcheckbox('tool_mobile/qrsameipcheck',
+                new lang_string('qrsameipcheck', 'tool_mobile'),
+                new lang_string('qrsameipcheck_desc', 'tool_mobile'), 1));
+    $temp->hide_if('tool_mobile/qrsameipcheck', 'tool_mobile/qrcodetype', 'neq', tool_mobile\api::QR_CODE_LOGIN);
+
     $temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
                 new lang_string('forcedurlscheme_key', 'tool_mobile'),
                 new lang_string('forcedurlscheme', 'tool_mobile'), 'moodlemobile', PARAM_NOTAGS));
diff --git a/admin/tool/mobile/tests/externallib_test.php b/admin/tool/mobile/tests/externallib_test.php
index a3af5eec152..c518df5f554 100644
--- a/admin/tool/mobile/tests/externallib_test.php
+++ b/admin/tool/mobile/tests/externallib_test.php
@@ -628,6 +628,7 @@ class externallib_test extends externallib_advanced_testcase {
         $this->setUser($user);
 
         $mobilesettings = get_config('tool_mobile');
+        $mobilesettings->qrsameipcheck = 1;
         $qrloginkey = api::get_qrlogin_key($mobilesettings);
 
         // Generate new tokens, the ones we expect to receive.
@@ -651,6 +652,76 @@ class externallib_test extends externallib_advanced_testcase {
         $result = external::get_tokens_for_qr_login(random_string('64'), $user->id);
     }
 
+    /*
+     * Test get_tokens_for_qr_login ignore ip check.
+     */
+    public function test_get_tokens_for_qr_login_ignore_ip_check() {
+        global $DB, $CFG, $USER;
+
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        $mobilesettings = get_config('tool_mobile');
+        $mobilesettings->qrsameipcheck = 0;
+        $qrloginkey = api::get_qrlogin_key($mobilesettings);
+
+        $key = $DB->get_record('user_private_key', ['value' => $qrloginkey]);
+        $this->assertNull($key->iprestriction);
+
+        // Generate new tokens, the ones we expect to receive.
+        $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
+        $token = external_generate_token_for_current_user($service);
+
+        // Fake the app.
+        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+                'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        $result = external::get_tokens_for_qr_login($qrloginkey, $USER->id);
+        $result = \external_api::clean_returnvalue(external::get_tokens_for_qr_login_returns(), $result);
+
+        $this->assertEmpty($result['warnings']);
+        $this->assertEquals($token->token, $result['token']);
+        $this->assertEquals($token->privatetoken, $result['privatetoken']);
+
+        // Now, try with an invalid key.
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('invalidkey', 'error'));
+        $result = external::get_tokens_for_qr_login(random_string('64'), $user->id);
+    }
+
+    /*
+     * Test get_tokens_for_qr_login ip check fails.
+     */
+    public function test_get_tokens_for_qr_login_ip_check_mismatch() {
+        global $DB, $CFG, $USER;
+
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        $mobilesettings = get_config('tool_mobile');
+        $mobilesettings->qrsameipcheck = 1;
+        $qrloginkey = api::get_qrlogin_key($mobilesettings);
+
+        // Alter expected ip.
+        $DB->set_field('user_private_key', 'iprestriction', '6.6.6.6', ['value' => $qrloginkey]);
+
+        // Generate new tokens, the ones we expect to receive.
+        $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
+        $token = external_generate_token_for_current_user($service);
+
+        // Fake the app.
+        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+                'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('ipmismatch', 'error'));
+        $result = external::get_tokens_for_qr_login($qrloginkey, $USER->id);
+    }
+
     /**
      * Test get_tokens_for_qr_login missing QR code enabled.
      */
diff --git a/admin/tool/mobile/version.php b/admin/tool/mobile/version.php
index 16fcd6afccc..2ad143b924c 100644
--- a/admin/tool/mobile/version.php
+++ b/admin/tool/mobile/version.php
@@ -23,7 +23,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2022041900; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2022041901; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2022041200; // Requires this Moodle version.
 $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
 $plugin->dependencies = array(