mirror of
https://github.com/moodle/moodle.git
synced 2025-01-17 13:38:32 +01:00
Merge branch 'MDL-80835-main' of https://github.com/snake/moodle
This commit is contained in:
commit
d1b1d9d771
@ -115,7 +115,7 @@ class auth_plugin_lti extends \auth_plugin_base {
|
||||
if (isloggedin()) {
|
||||
// If a different user is currently logged in, authenticate the linked user instead.
|
||||
global $USER;
|
||||
if ((int) $USER->id !== $user->id) {
|
||||
if ($USER->id !== $user->id) {
|
||||
complete_user_login($user);
|
||||
}
|
||||
// If the linked user is already logged in, skip the call to complete_user_login() because this affects deep linking
|
||||
|
50
auth/lti/classes/local/ltiadvantage/event/event_handler.php
Normal file
50
auth/lti/classes/local/ltiadvantage/event/event_handler.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace auth_lti\local\ltiadvantage\event;
|
||||
|
||||
use auth_lti\local\ltiadvantage\utility\cookie_helper;
|
||||
use core\event\user_loggedin;
|
||||
|
||||
/**
|
||||
* Event handler for auth_lti.
|
||||
*
|
||||
* @package auth_lti
|
||||
* @copyright 2024 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class event_handler {
|
||||
|
||||
/**
|
||||
* Allows the plugin to augment Set-Cookie headers when the user_loggedin event is fired as part of complete_user_login() calls.
|
||||
*
|
||||
* @param user_loggedin $event the event
|
||||
* @return void
|
||||
*/
|
||||
public static function handle_user_loggedin(user_loggedin $event): void {
|
||||
// The event data isn't important here. The intent of this listener is to ensure that the MoodleSession cookie gets the
|
||||
// 'Partitioned' attribute, when required - an opt-in flag needed to use Chrome's partitioning mechanism, CHIPS. During LTI
|
||||
// auth, the auth class (auth/lti/auth.php) calls complete_user_login(), which generates a new session cookie as part of its
|
||||
// login process. This handler makes sure that this new cookie is intercepted and partitioned, if needed.
|
||||
if (cookie_helper::cookies_supported()) {
|
||||
if (cookie_helper::get_cookies_supported_method() == cookie_helper::COOKIE_METHOD_EXPLICIT_PARTITIONING) {
|
||||
global $CFG;
|
||||
cookie_helper::add_attributes_to_cookie_response_header('MoodleSession' . $CFG->sessioncookie,
|
||||
['Partitioned', 'Secure']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
295
auth/lti/classes/local/ltiadvantage/utility/cookie_helper.php
Normal file
295
auth/lti/classes/local/ltiadvantage/utility/cookie_helper.php
Normal file
@ -0,0 +1,295 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace auth_lti\local\ltiadvantage\utility;
|
||||
|
||||
/**
|
||||
* Helper class providing utils dealing with cookies in LTI, particularly 3rd party cookies.
|
||||
*
|
||||
* This class primarily provides a means to augment outbound cookie headers, in order to satisfy browser-specific
|
||||
* requirements for setting 3rd party cookies.
|
||||
*
|
||||
* @package auth_lti
|
||||
* @copyright 2024 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class cookie_helper {
|
||||
|
||||
/** @var int Cookies are not supported. */
|
||||
public const COOKIE_METHOD_NOT_SUPPORTED = 0;
|
||||
|
||||
/** @var int Cookies are supported without explicit partitioning. */
|
||||
public const COOKIE_METHOD_NO_PARTITIONING = 1;
|
||||
|
||||
/** @var int Cookies are supported via explicit partitioning. */
|
||||
public const COOKIE_METHOD_EXPLICIT_PARTITIONING = 2;
|
||||
|
||||
/**
|
||||
* Make sure the given attributes are set on the Set-Cookie response header identified by name=$cookiename.
|
||||
*
|
||||
* This function only affects Set-Cookie headers and modifies the headers directly with the required changes, if any.
|
||||
*
|
||||
* @param string $cookiename the cookie name.
|
||||
* @param array $attributes the attributes to set/ensure are set.
|
||||
* @return void
|
||||
*/
|
||||
public static function add_attributes_to_cookie_response_header(string $cookiename, array $attributes): void {
|
||||
|
||||
$setcookieheaders = array_filter(headers_list(), function($val) {
|
||||
return preg_match("/Set-Cookie:/i", $val);
|
||||
});
|
||||
if (empty($setcookieheaders)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$updatedheaders = self::cookie_response_headers_add_attributes($setcookieheaders, [$cookiename], $attributes);
|
||||
|
||||
// Note: The header_remove() method is quite crude and removes all headers of that header name.
|
||||
header_remove('Set-Cookie');
|
||||
foreach ($updatedheaders as $header) {
|
||||
header($header, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of HTTP header strings, return a list of HTTP header strings where the matched 'Set-Cookie' headers
|
||||
* have been updated with the attributes defined in $attribute - an array of strings.
|
||||
*
|
||||
* This method does not verify whether a given attribute is valid or not. It blindly sets it and returns the header
|
||||
* strings. It's up to calling code to determine whether an attribute makes sense or not.
|
||||
*
|
||||
* @param array $headerstrings the array of header strings.
|
||||
* @param array $cookiestomatch the array of cookie names to match.
|
||||
* @param array $attributes the attributes to set on each matched 'Set-Cookie' header.
|
||||
* @param bool $casesensitive whether to match the attribute in a case-sensitive way.
|
||||
* @return array the updated array of header strings.
|
||||
*/
|
||||
public static function cookie_response_headers_add_attributes(array $headerstrings, array $cookiestomatch, array $attributes,
|
||||
bool $casesensitive = false): array {
|
||||
|
||||
return array_map(function($headerstring) use ($attributes, $casesensitive, $cookiestomatch) {
|
||||
if (!self::cookie_response_header_matches_names($headerstring, $cookiestomatch)) {
|
||||
return $headerstring;
|
||||
}
|
||||
foreach ($attributes as $attribute) {
|
||||
if (!self::cookie_response_header_contains_attribute($headerstring, $attribute, $casesensitive)) {
|
||||
$headerstring = self::cookie_response_header_append_attribute($headerstring, $attribute);
|
||||
}
|
||||
}
|
||||
return $headerstring;
|
||||
}, $headerstrings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether cookies can be used with the current user agent and, if so, via what method they are set.
|
||||
*
|
||||
* Currently, this tries 2 modes of setting a test cookie:
|
||||
* 1. Setting a SameSite=None, Secure cookie. This will work in any first party context, and in 3rd party contexts for
|
||||
* any browsers supporting automatic partitioning of 3rd party cookies (E.g. Firefox, Brave).
|
||||
* 2. If 1 fails, setting a cookie with the Chrome 'Partitioned' attribute included, opting that cookie into CHIPS. This will
|
||||
* work for Chrome.
|
||||
*
|
||||
* Upon completion of the cookie check, the check sets a SESSION flag indicating the method used to set the cookie, and upgrades
|
||||
* the session cookie ('MoodleSession') using the respective method. This ensure the session cookie will continue to be sent.
|
||||
*
|
||||
* Then, the following methods can be used by client code to query whether the UA supports cookies, and how:
|
||||
* @see self::cookies_supported() - whether it could be set at all.
|
||||
* @see self::get_cookies_supported_mode() - if a cookie could be set, what mode was used to set it.
|
||||
*
|
||||
* This permits client code to make sure it's setting its cookies appropriately (via the advertised method), and allows it to
|
||||
* present notices - such as in the case where a given UA is found to be lacking the requisite cookie support.
|
||||
* E.g.
|
||||
* cookie_helper::do_cookie_check($mypageurl);
|
||||
* if (!cookie_helper::cookies_supported()) {
|
||||
* // Print a notice stating that cookie support is required.
|
||||
* }
|
||||
* // Elsewhere in other client code...
|
||||
* if (cookie_helper::get_cookies_supported_mode() === cookie_helper::COOKIE_METHOD_EXPLICIT_PARTITIONING) {
|
||||
* // Set a cookie, making sure to use the helper to also opt-in to partitioning.
|
||||
* setcookie('myauthcookie', 'myauthcookievalue', ['samesite' => 'None', 'secure' => true]);
|
||||
* cookie_helper::add_partitioning_to_cookie('myauthcookie');
|
||||
* }
|
||||
*
|
||||
* @param \moodle_url $pageurl the URL of the page making the check, used to redirect back to after setting test cookies.
|
||||
* @return void
|
||||
*/
|
||||
public static function do_cookie_check(\moodle_url $pageurl): void {
|
||||
global $_COOKIE, $SESSION, $CFG;
|
||||
$cookiecheck1 = optional_param('cookiecheck1', null, PARAM_INT);
|
||||
$cookiecheck2 = optional_param('cookiecheck2', null, PARAM_INT);
|
||||
|
||||
if (empty($cookiecheck1)) {
|
||||
// Start the cookie check. Set two test cookies - one samesite none, and one partitioned - and redirect.
|
||||
// Set cookiecheck to show the check has started.
|
||||
self::set_test_cookie('cookiecheck1', self::COOKIE_METHOD_NO_PARTITIONING);
|
||||
self::set_test_cookie('cookiecheck2', self::COOKIE_METHOD_EXPLICIT_PARTITIONING, true);
|
||||
$pageurl->params([
|
||||
'cookiecheck1' => self::COOKIE_METHOD_NO_PARTITIONING,
|
||||
'cookiecheck2' => self::COOKIE_METHOD_EXPLICIT_PARTITIONING,
|
||||
]);
|
||||
|
||||
// LTI needs to guarantee the 'SameSite=None', 'Secure' (and sometimes 'Partitioned') attributes are set on the
|
||||
// MoodleSession cookie. This is done via manipulation of the outgoing headers after the cookie check redirect. To
|
||||
// guarantee these outgoing Set-Cookie headers will be created after the redirect, expire the current cookie.
|
||||
self::expire_moodlesession();
|
||||
|
||||
redirect($pageurl);
|
||||
} else {
|
||||
// Have already started a cookie check, so check the result.
|
||||
$cookie1received = isset($_COOKIE['cookiecheck1']) && $_COOKIE['cookiecheck1'] == $cookiecheck1;
|
||||
$cookie2received = isset($_COOKIE['cookiecheck2']) && $_COOKIE['cookiecheck2'] == $cookiecheck2;
|
||||
|
||||
if ($cookie1received || $cookie2received) {
|
||||
// The test cookie could be set and received.
|
||||
// Set a session flag storing the method used to set it, and make sure the session cookie uses this method.
|
||||
$cookiemethod = $cookie1received ? self::COOKIE_METHOD_NO_PARTITIONING : self::COOKIE_METHOD_EXPLICIT_PARTITIONING;
|
||||
$SESSION->auth_lti_cookie_method = $cookiemethod;
|
||||
if ($cookiemethod === self::COOKIE_METHOD_EXPLICIT_PARTITIONING) {
|
||||
// This assumes secure is set, since that's the only way a paritioned test cookie have been set.
|
||||
self::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, ['Partitioned', 'Secure']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a cookie check has been made, returns whether cookies could be set or not.
|
||||
*
|
||||
* @return bool whether cookies are supported or not.
|
||||
*/
|
||||
public static function cookies_supported(): bool {
|
||||
return self::get_cookies_supported_method() !== self::COOKIE_METHOD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a cookie check has been made, gets the method used to set a cookie, or self::COOKIE_METHOD_NOT_SUPPORTED if not supported.
|
||||
*
|
||||
* For cookie methods:
|
||||
* @see self::COOKIE_METHOD_NOT_SUPPORTED
|
||||
* @see self::COOKIE_METHOD_NO_PARTITIONING
|
||||
* @see self::COOKIE_METHOD_EXPLICIT_PARTITIONING
|
||||
*
|
||||
* @return int the constant representing the method by which the cookie was set, or not.
|
||||
*/
|
||||
public static function get_cookies_supported_method(): int {
|
||||
global $SESSION;
|
||||
return $SESSION->auth_lti_cookie_method ?? self::COOKIE_METHOD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the expiry of the MoodleSession cookie.
|
||||
*
|
||||
* This is useful to force a new Set-Cookie header on the next redirect.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function expire_moodlesession(): void {
|
||||
global $CFG;
|
||||
|
||||
$setcookieheader = array_filter(headers_list(), function($val) use ($CFG) {
|
||||
return self::cookie_response_header_matches_name($val, 'MoodleSession'.$CFG->sessioncookie);
|
||||
});
|
||||
if (!empty($setcookieheader)) {
|
||||
$expirestr = 'Expires='.gmdate(DATE_RFC7231, time() - 60);
|
||||
self::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, [$expirestr]);
|
||||
} else {
|
||||
setcookie('MoodleSession'.$CFG->sessioncookie, '', time() - 60);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a test cookie, using SameSite=None; Secure; attributes if possible, and with or without partitioning opt-in.
|
||||
*
|
||||
* @param string $name cookie name
|
||||
* @param string $value cookie value
|
||||
* @param bool $partitioned whether to try to add partitioning opt-in, which requires secure cookies (https sites).
|
||||
* @return void
|
||||
*/
|
||||
private static function set_test_cookie(string $name, string $value, bool $partitioned = false): void {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/sessionlib.php');
|
||||
|
||||
$atts = ['expires' => time() + 30];
|
||||
if (is_moodle_cookie_secure()) {
|
||||
$atts['samesite'] = 'none';
|
||||
$atts['secure'] = true;
|
||||
}
|
||||
setcookie($name, $value, $atts);
|
||||
|
||||
if (is_moodle_cookie_secure() && $partitioned) {
|
||||
self::add_attributes_to_cookie_response_header($name, ['Partitioned']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the header string is a 'Set-Cookie' header for the cookie identified by $cookiename.
|
||||
*
|
||||
* @param string $headerstring the header string to check.
|
||||
* @param string $cookiename the name of the cookie to match.
|
||||
* @return bool true if the header string is a Set-Cookie header for the named cookie, false otherwise.
|
||||
*/
|
||||
private static function cookie_response_header_matches_name(string $headerstring, string $cookiename): bool {
|
||||
// Generally match the format, but in a case-insensitive way so that 'set-cookie' and "SET-COOKIE" are both valid.
|
||||
return preg_match("/Set-Cookie: *$cookiename=/i", $headerstring)
|
||||
// Case-sensitive match on cookiename, which is case-sensitive.
|
||||
&& preg_match("/: *$cookiename=/", $headerstring);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the header string is a 'Set-Cookie' header for the cookies identified in the $cookienames array.
|
||||
*
|
||||
* @param string $headerstring the header string to check.
|
||||
* @param array $cookienames the array of cookie names to match.
|
||||
* @return bool true if the header string is a Set-Cookie header for one of the named cookies, false otherwise.
|
||||
*/
|
||||
private static function cookie_response_header_matches_names(string $headerstring, array $cookienames): bool {
|
||||
foreach ($cookienames as $cookiename) {
|
||||
if (self::cookie_response_header_matches_name($headerstring, $cookiename)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the header string contains the given attribute.
|
||||
*
|
||||
* @param string $headerstring the header string to check.
|
||||
* @param string $attribute the attribute to check for.
|
||||
* @param bool $casesensitive whether to perform a case-sensitive check.
|
||||
* @return bool true if the header contains the attribute, false otherwise.
|
||||
*/
|
||||
private static function cookie_response_header_contains_attribute(string $headerstring, string $attribute,
|
||||
bool $casesensitive): bool {
|
||||
|
||||
if ($casesensitive) {
|
||||
return str_contains($headerstring, $attribute);
|
||||
}
|
||||
return str_contains(strtolower($headerstring), strtolower($attribute));
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the given attribute to the header string.
|
||||
*
|
||||
* @param string $headerstring the header string to append to.
|
||||
* @param string $attribute the attribute to append.
|
||||
* @return string the updated header string.
|
||||
*/
|
||||
private static function cookie_response_header_append_attribute(string $headerstring, string $attribute): string {
|
||||
$headerstring = rtrim($headerstring, ';'); // Sometimes included.
|
||||
return "$headerstring; $attribute;";
|
||||
}
|
||||
}
|
33
auth/lti/db/events.php
Normal file
33
auth/lti/db/events.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* LTI Auth plugin event handler definition.
|
||||
*
|
||||
* @package auth_lti
|
||||
* @category event
|
||||
* @copyright 2024 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$observers = [
|
||||
[
|
||||
'eventname' => '\core\event\user_loggedin',
|
||||
'callback' => '\auth_lti\local\ltiadvantage\event\event_handler::handle_user_loggedin',
|
||||
],
|
||||
];
|
215
auth/lti/tests/local/ltiadvantage/utility/cookie_helper_test.php
Normal file
215
auth/lti/tests/local/ltiadvantage/utility/cookie_helper_test.php
Normal file
@ -0,0 +1,215 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace auth_lti\local\ltiadvantage\utility;
|
||||
|
||||
/**
|
||||
* Tests for the cookie_helper utility class.
|
||||
*
|
||||
* @package auth_lti
|
||||
* @copyright 2024 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \auth_lti\local\ltiadvantage\utility\cookie_helper
|
||||
*/
|
||||
class cookie_helper_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Testing cookie_response_headers_add_attributes().
|
||||
*
|
||||
* @dataProvider cookie_response_headers_provider
|
||||
*
|
||||
* @param array $headers the headers to search
|
||||
* @param array $cookienames the cookienames to match
|
||||
* @param array $attributes the attributes to add
|
||||
* @param bool $casesensitive whether to do a case-sensitive lookup for the attribute
|
||||
* @param array $expectedheaders the expected, updated headers
|
||||
* @return void
|
||||
*/
|
||||
public function test_cookie_response_headers_add_attributes(array $headers, array $cookienames, array $attributes,
|
||||
bool $casesensitive, array $expectedheaders): void {
|
||||
|
||||
$updated = cookie_helper::cookie_response_headers_add_attributes($headers, $cookienames, $attributes, $casesensitive);
|
||||
$this->assertEquals($expectedheaders, $updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testing cookie_response_headers_add_attributes().
|
||||
*
|
||||
* @return array the inputs and expected outputs.
|
||||
*/
|
||||
public static function cookie_response_headers_provider(): array {
|
||||
return [
|
||||
'Only one matching cookie header, without any of the attributes' => [
|
||||
'headers' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; HttpOnly;',
|
||||
],
|
||||
'cookienames' => [
|
||||
'testcookie',
|
||||
],
|
||||
'attributes' => [
|
||||
'Partitioned',
|
||||
'SameSite=None',
|
||||
'Secure',
|
||||
],
|
||||
'casesensitive' => false,
|
||||
'output' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; HttpOnly; Partitioned; SameSite=None; Secure;',
|
||||
],
|
||||
],
|
||||
'Several matching cookie headers, without attributes' => [
|
||||
'headers' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; HttpOnly;',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; HttpOnly;',
|
||||
],
|
||||
'cookienames' => [
|
||||
'testcookie',
|
||||
'mytestcookie',
|
||||
],
|
||||
'attributes' => [
|
||||
'Partitioned',
|
||||
'SameSite=None',
|
||||
'Secure',
|
||||
],
|
||||
'casesensitive' => false,
|
||||
'output' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; HttpOnly; Partitioned; SameSite=None; Secure;',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; HttpOnly; Partitioned; SameSite=None; Secure;',
|
||||
],
|
||||
],
|
||||
'Several matching cookie headers, several non-matching, all missing all attributes' => [
|
||||
'headers' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; HttpOnly;',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; HttpOnly;',
|
||||
'Set-Cookie: anothertestcookie=value; path=/test/; HttpOnly;',
|
||||
],
|
||||
'cookienames' => [
|
||||
'testcookie',
|
||||
'mytestcookie',
|
||||
'blah',
|
||||
'etc',
|
||||
],
|
||||
'attributes' => [
|
||||
'Partitioned',
|
||||
'SameSite=None',
|
||||
'Secure',
|
||||
],
|
||||
'casesensitive' => false,
|
||||
'output' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; HttpOnly; Partitioned; SameSite=None; Secure;',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; HttpOnly; Partitioned; SameSite=None; Secure;',
|
||||
'Set-Cookie: anothertestcookie=value; path=/test/; HttpOnly;',
|
||||
],
|
||||
],
|
||||
'Matching cookie headers, some with existing attributes' => [
|
||||
'headers' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; secure; HttpOnly; Partitioned; SameSite=None',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; secure; HttpOnly; SameSite=None',
|
||||
],
|
||||
'cookienames' => [
|
||||
'testcookie',
|
||||
'mytestcookie',
|
||||
'etc',
|
||||
],
|
||||
'attributes' => [
|
||||
'Partitioned',
|
||||
'SameSite=None',
|
||||
'Secure',
|
||||
],
|
||||
'casesensitive' => false,
|
||||
'output' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; secure; HttpOnly; Partitioned; SameSite=None',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; secure; HttpOnly; SameSite=None; Partitioned;',
|
||||
],
|
||||
],
|
||||
'Matching headers, some with existing attributes, case sensitive' => [
|
||||
'headers' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; secure; HttpOnly; SameSite=None; partitioned',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; secure; HttpOnly; SameSite=None',
|
||||
],
|
||||
'cookienames' => [
|
||||
'testcookie',
|
||||
'mytestcookie',
|
||||
'etc',
|
||||
],
|
||||
'attributes' => [
|
||||
'Partitioned',
|
||||
'SameSite=None',
|
||||
'Secure',
|
||||
],
|
||||
'casesensitive' => true,
|
||||
'output' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; secure; HttpOnly; SameSite=None; partitioned; Partitioned; Secure;',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; secure; HttpOnly; SameSite=None; Partitioned; Secure;',
|
||||
],
|
||||
],
|
||||
'Empty list of cookie names to match, so unmodified inputs' => [
|
||||
'headers' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; secure; HttpOnly; SameSite=None; partitioned',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; secure; HttpOnly; SameSite=None',
|
||||
],
|
||||
'cookienames' => [],
|
||||
'attributes' => [
|
||||
'Partitioned',
|
||||
'SameSite=None',
|
||||
'Secure',
|
||||
],
|
||||
'casesensitive' => false,
|
||||
'output' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; secure; HttpOnly; SameSite=None; partitioned',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; secure; HttpOnly; SameSite=None',
|
||||
],
|
||||
],
|
||||
'Empty list of attributes to set, so unmodified inputs' => [
|
||||
'headers' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; secure; HttpOnly; SameSite=None; partitioned',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; secure; HttpOnly; SameSite=None',
|
||||
],
|
||||
'cookienames' => [
|
||||
'testcookie',
|
||||
'mycookie',
|
||||
],
|
||||
'attributes' => [],
|
||||
'casesensitive' => false,
|
||||
'output' => [
|
||||
'Set-Cookie: testcookie=value; path=/test/; secure; HttpOnly; SameSite=None; partitioned',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; secure; HttpOnly; SameSite=None',
|
||||
],
|
||||
],
|
||||
'Other HTTP headers, some matching Set-Cookie, some not' => [
|
||||
'headers' => [
|
||||
'Authorization: blah',
|
||||
'Set-Cookie: testcookie=value; path=/test/; secure; HttpOnly; SameSite=None; Partitioned',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; secure; HttpOnly; SameSite=None',
|
||||
],
|
||||
'cookienames' => [
|
||||
'testcookie',
|
||||
'mytestcookie',
|
||||
],
|
||||
'attributes' => [
|
||||
'Partitioned',
|
||||
'SameSite=None',
|
||||
'Secure',
|
||||
],
|
||||
'casesensitive' => false,
|
||||
'output' => [
|
||||
'Authorization: blah',
|
||||
'Set-Cookie: testcookie=value; path=/test/; secure; HttpOnly; SameSite=None; Partitioned',
|
||||
'Set-Cookie: mytestcookie=value; path=/test/; secure; HttpOnly; SameSite=None; Partitioned;',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2023100900; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2024020700; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2023100400; // Requires this Moodle version.
|
||||
$plugin->component = 'auth_lti'; // Full name of the plugin (used for diagnostics).
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
namespace enrol_lti\local\ltiadvantage\lib;
|
||||
|
||||
use auth_lti\local\ltiadvantage\utility\cookie_helper;
|
||||
use Packback\Lti1p3\Interfaces\ICookie;
|
||||
|
||||
/**
|
||||
@ -55,6 +56,9 @@ class lti_cookie implements ICookie {
|
||||
|
||||
setcookie($name, $value, array_merge($cookieoptions, $samesiteoptions, $options));
|
||||
|
||||
// Necessary, since partitioned can't be set via setcookie yet.
|
||||
cookie_helper::add_attributes_to_cookie_response_header($name, ['Partitioned']);
|
||||
|
||||
// Set a second fallback cookie in the event that "SameSite" is not supported.
|
||||
setcookie('LEGACY_'.$name, $value, array_merge($cookieoptions, $options));
|
||||
}
|
||||
|
@ -263,4 +263,19 @@ class renderer extends plugin_renderer_base {
|
||||
return parent::render_from_template('enrol_lti/local/ltiadvantage/registration_view',
|
||||
$tcontext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a warning, indicating to the user that cookies are require but couldn't be set.
|
||||
*
|
||||
* @return string the html.
|
||||
*/
|
||||
public function render_cookies_required_notice(): string {
|
||||
$notification = new notification(get_string('cookiesarerequiredinfo', 'enrol_lti'), notification::NOTIFY_WARNING, false);
|
||||
$tcontext = [
|
||||
'heading' => get_string('cookiesarerequired', 'enrol_lti'),
|
||||
'notification' => $notification->export_for_template($this),
|
||||
];
|
||||
|
||||
return parent::render_from_template('enrol_lti/local/ltiadvantage/cookies_required_notice', $tcontext);
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,10 @@ $string['addtocourse'] = 'Add to course';
|
||||
$string['addtogradebook'] = 'Add to gradebook';
|
||||
$string['allowframeembedding'] = 'Note: It is recommended that the site administration setting \'Allow frame embedding\' is enabled, so that tools are displayed within a frame rather than in a new window.';
|
||||
$string['authltimustbeenabled'] = 'Note: This plugin requires the LTI authentication plugin to be enabled too.';
|
||||
$string['cookiesarerequired'] = 'Cookies are blocked by your browser';
|
||||
$string['cookiesarerequiredinfo'] = 'This tool can\'t be launched because your browser seems to be blocking third-party cookies.
|
||||
<br><br>
|
||||
To use this tool, try changing your browser cookie settings or using a different browser.';
|
||||
$string['copiedtoclipboard'] = '{$a} copied to clipboard';
|
||||
$string['copytoclipboard'] = 'Copy to clipboard';
|
||||
$string['couldnotestablishproxy'] = 'Could not establish proxy with consumer.';
|
||||
|
@ -26,6 +26,7 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use auth_lti\local\ltiadvantage\utility\cookie_helper;
|
||||
use enrol_lti\local\ltiadvantage\lib\lti_cookie;
|
||||
use enrol_lti\local\ltiadvantage\lib\issuer_database;
|
||||
use enrol_lti\local\ltiadvantage\lib\launch_cache_session;
|
||||
@ -76,6 +77,30 @@ if (empty($_REQUEST['client_id']) && !empty($_REQUEST['id'])) {
|
||||
$_REQUEST['client_id'] = $_REQUEST['id'];
|
||||
}
|
||||
|
||||
// Before beginning the OIDC authentication, ensure the MoodleSession cookie can be used. Browser-specific steps may need to be
|
||||
// taken to set cookies in 3rd party contexts. Skip the check if the user is already auth'd. This means that either cookies aren't
|
||||
// an issue in the current browser/launch context.
|
||||
if (!isloggedin()) {
|
||||
cookie_helper::do_cookie_check(new moodle_url('/enrol/lti/login.php', [
|
||||
'iss' => $iss,
|
||||
'login_hint' => $loginhint,
|
||||
'target_link_uri' => $targetlinkuri,
|
||||
'lti_message_hint' => $ltimessagehint,
|
||||
'client_id' => $_REQUEST['client_id'],
|
||||
]));
|
||||
if (!cookie_helper::cookies_supported()) {
|
||||
global $OUTPUT, $PAGE;
|
||||
$PAGE->set_context(context_system::instance());
|
||||
$PAGE->set_url(new moodle_url('/enrol/lti/login.php'));
|
||||
$PAGE->set_pagelayout('popup');
|
||||
echo $OUTPUT->header();
|
||||
$renderer = $PAGE->get_renderer('enrol_lti');
|
||||
echo $renderer->render_cookies_required_notice();
|
||||
echo $OUTPUT->footer();
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
// Now, do the OIDC login.
|
||||
$redirecturl = LtiOidcLogin::new(
|
||||
new issuer_database(new application_registration_repository(), new deployment_repository()),
|
||||
|
@ -0,0 +1,50 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template enrol_lti/local/ltiadvantage/cookies_required_notice
|
||||
|
||||
Displays a notice, reporting that cookies are required but couldn't be set.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* heading
|
||||
* notification
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"heading": "Cookies are required",
|
||||
"notification": {
|
||||
"message": "You appear to be using an unsupported browser...",
|
||||
"extraclasses": "",
|
||||
"announce": true,
|
||||
"closebutton": false,
|
||||
"issuccess": false,
|
||||
"isinfo": false,
|
||||
"iswarning": true,
|
||||
"iserror": false
|
||||
}
|
||||
}
|
||||
}}
|
||||
<h3>{{heading}}</h3>
|
||||
{{#notification}}
|
||||
{{> core/notification}}
|
||||
{{/notification}}
|
Loading…
x
Reference in New Issue
Block a user