aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+studip@gmail.com>2024-05-27 05:51:08 +0000
committerDavid Siegfried <david.siegfried@uni-vechta.de>2024-05-27 05:51:08 +0000
commitaddf6e2dd8296b9c05cd648641158788175b7b88 (patch)
treefcc6aad711be9d2d2f8fea7258c78d96bcefc92b
parente48767859758c9e12cf464313ca104482d5bec4d (diff)
add Studip\OAuth1 class to sign or verify PSR7 requests, fixes #4203
Closes #4203 Merge request studip/studip!3030
-rw-r--r--app/controllers/course/lti.php31
-rw-r--r--lib/classes/LtiLink.php16
-rw-r--r--lib/classes/OAuth1.php167
-rw-r--r--lib/classes/auth_plugins/StudipAuthLTI.class.php21
-rw-r--r--tests/unit/lib/classes/OAuth1Test.php118
5 files changed, 312 insertions, 41 deletions
diff --git a/app/controllers/course/lti.php b/app/controllers/course/lti.php
index e0ca2cf..4db7776 100644
--- a/app/controllers/course/lti.php
+++ b/app/controllers/course/lti.php
@@ -1,4 +1,7 @@
<?php
+
+use Studip\OAuth2\NegotiatesWithPsr7;
+
/**
* course/lti.php - LTI consumer API for Stud.IP
*
@@ -13,6 +16,8 @@
class Course_LtiController extends StudipController
{
+ use NegotiatesWithPsr7;
+
/**
* Callback function being called before an action is executed.
*/
@@ -268,22 +273,15 @@ class Course_LtiController extends StudipController
*/
public function save_link_action($tool_id)
{
- require_once 'vendor/oauth-php/library/OAuthRequestVerifier.php';
-
$tool = LtiTool::find($tool_id);
$lti_msg = Request::get('lti_msg');
$lti_errormsg = Request::get('lti_errormsg');
$content_items = Request::get('content_items');
$content_items = json_decode($content_items, true);
- OAuthStore::instance('PDO', [
- 'dsn' => 'mysql:host=' . $GLOBALS['DB_STUDIP_HOST'] . ';dbname=' . $GLOBALS['DB_STUDIP_DATABASE'],
- 'username' => $GLOBALS['DB_STUDIP_USER'],
- 'password' => $GLOBALS['DB_STUDIP_PASSWORD']
- ]);
-
- $oarv = new OAuthRequestVerifier();
- $oarv->verifySignature($tool->consumer_secret, false, false);
+ if (!Studip\OAuth1::verifyRequest($this->getPsrRequest(), $tool->consumer_secret, '')) {
+ throw new Exception('Could not verify request.');
+ }
if (is_array($content_items) && count($content_items['@graph'])) {
// we only support selecting a single content item
@@ -452,18 +450,11 @@ class Course_LtiController extends StudipController
*/
public function outcome_action($id)
{
- require_once 'vendor/oauth-php/library/OAuthRequestVerifier.php';
-
$lti_data = LtiData::find($id);
- OAuthStore::instance('PDO', [
- 'dsn' => 'mysql:host=' . $GLOBALS['DB_STUDIP_HOST'] . ';dbname=' . $GLOBALS['DB_STUDIP_DATABASE'],
- 'username' => $GLOBALS['DB_STUDIP_USER'],
- 'password' => $GLOBALS['DB_STUDIP_PASSWORD']
- ]);
-
- $oarv = new OAuthRequestVerifier();
- $oarv->verifySignature($lti_data->getConsumerSecret(), false, false);
+ if (!Studip\OAuth1::verifyRequest($this->getPsrRequest(), $lti_data->getConsumerSecret(), '')) {
+ throw new Exception('Could not verify request.');
+ }
// fetch and parse POST data
$message = file_get_contents('php://input');
diff --git a/lib/classes/LtiLink.php b/lib/classes/LtiLink.php
index 801b6c0..1423cef 100644
--- a/lib/classes/LtiLink.php
+++ b/lib/classes/LtiLink.php
@@ -310,12 +310,14 @@ class LtiLink
// posted form data will always use CR LF
$launch_params = preg_replace("/\r?\n/", "\r\n", $launch_params);
- // In OAuth, request parameters must be sorted by name
- ksort($launch_params);
- $launch_params = http_build_query($launch_params, '', '&', PHP_QUERY_RFC3986);
- $base_string = 'POST&' . rawurlencode($launch_url) . '&' . rawurlencode($launch_params);
- $secret = rawurlencode($this->consumer_secret) . '&';
-
- return base64_encode(hash_hmac($this->oauth_signature_method, $base_string, $secret, true));
+ return Studip\OAuth1::signRequest(
+ (new Slim\Psr7\Factory\ServerRequestFactory())->createServerRequest(
+ 'POST',
+ $launch_url
+ )->withQueryParams($launch_params),
+ $this->consumer_secret,
+ '',
+ $this->oauth_signature_method
+ );
}
}
diff --git a/lib/classes/OAuth1.php b/lib/classes/OAuth1.php
new file mode 100644
index 0000000..1695f9f
--- /dev/null
+++ b/lib/classes/OAuth1.php
@@ -0,0 +1,167 @@
+<?php
+namespace Studip;
+
+use Psr\Http\Message\ServerRequestInterface as Request;
+use RuntimeException;
+
+/**
+ * Basic oauth1 request handling for Stud.IP using PSR-7 http messages.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @since Stud.IP 6.0
+ */
+final class OAuth1
+{
+ /**
+ * Signs a given request.
+ *
+ * @throws RuntimeException if a request for any other oauth version then
+ * 1.0 shall be signed
+ */
+ public static function signRequest(
+ Request $request,
+ string $consumerSecret,
+ string $tokenSecret,
+ string $method
+ ): string {
+ if (
+ isset($request->getQueryParams()['oauth_version'])
+ && $request->getQueryParams()['oauth_version'] !== '1.0'
+ ) {
+ throw new RuntimeException(self::class . ' only supports OAuth 1.0 requests');
+ }
+
+ return self::hash(
+ $method,
+ self::getSignatureBaseString($request),
+ self::urlencode($consumerSecret) . '&' . self::urlencode($tokenSecret)
+ );
+ }
+
+ /**
+ * Verifies an oauth request.
+ *
+ * @throws RuntimeException if any necessary oauth parameter is missing
+ */
+ public static function verifyRequest(
+ Request $request,
+ string $consumerSecret,
+ string $tokenSecret
+ ): bool {
+ $parameters = self::extractParameters($request);
+
+ $required = [
+ 'oauth_consumer_key',
+ 'oauth_nonce',
+ 'oauth_signature',
+ 'oauth_signature_method',
+ 'oauth_timestamp',
+ ];
+
+ $missing = array_diff($required, array_keys($parameters));
+ if (count($missing) > 0) {
+ throw new RuntimeException('Missing oauth parameters ' . implode(', ', $missing));
+ }
+
+ $signatureToVerify = $parameters['oauth_signature'];
+ unset($parameters['oauth_signature']);
+
+ $signature = self::signRequest(
+ $request->withQueryParams($parameters),
+ $consumerSecret,
+ $tokenSecret,
+ $parameters['oauth_signature_method']
+ );
+
+ return $signature === $signatureToVerify;
+ }
+
+ /**
+ * Extracts the oauth parameters either from the Authorization header or
+ * from the query string.
+ */
+ public static function extractParameters(Request $request): array
+ {
+ $parameters = $request->getQueryParams();
+
+ $header = $request->getHeaderLine('Authorization');
+ if ($header && str_starts_with($header, 'OAuth ')) {
+ $temp = substr($header, 6);
+ $chunks = explode(',', $temp);
+
+ foreach ($chunks as $chunk) {
+ [$key, $value] = explode('=', $chunk, 2);
+ $value = trim($value, '"');
+ $parameters[$key] = self::urldecode($value);
+ }
+ }
+
+ return $parameters;
+ }
+
+ /**
+ * Creates the base string for the signature. It consists of:
+ *
+ * - The uppercase request method
+ * - The request URL
+ * - the sorted and urlencoded parameters of the request
+ *
+ * The urlencoded parts are concatenated together into a single string
+ * separated by the '&' character.
+ *
+ *
+ */
+ public static function getSignatureBaseString(Request $request): string
+ {
+ $parameters = $request->getQueryParams();
+ ksort($parameters);
+
+ return implode('&', array_map(
+ self::urlencode(...),
+ [
+ strtoupper($request->getMethod()),
+ (string) $request->getUri()->withQuery(''),
+ http_build_query($parameters, '', '&', PHP_QUERY_RFC3986),
+ ]
+ ));
+ }
+
+ /**
+ * Hashes a given text with a given key by the given method.
+ *
+ * @throws RuntimeException if the given hash method is not supported
+ */
+ public static function hash(string $method, string $text, string $key): string
+ {
+ $method = strtolower($method);
+ return match ($method) {
+ 'hmac-sha1', 'sha1' => base64_encode(hash_hmac('sha1', $text, $key, true)),
+ 'hmac-sha256', 'sha256' => base64_encode(hash_hmac('sha256', $text, $key, true)),
+ 'hmac-sha512', 'sha512' => base64_encode(hash_hmac('sha512', $text, $key, true)),
+
+ 'plaintext' => $key,
+
+ default => throw new RuntimeException('Unsupported sigature method "' . $method . '"'),
+ };
+ }
+
+ /**
+ * Urlencodes a given input
+ */
+ public static function urldecode(string $input): string
+ {
+ return rawurldecode($input);
+ }
+
+ /**
+ * Urldecodes a given input
+ */
+ public static function urlencode(string $input): string
+ {
+ $encoded = rawurlencode($input);
+ return str_starts_with($encoded, '/%7E')
+ ? str_replace('/%7E', '/~', $encoded)
+ : $encoded;
+ }
+}
diff --git a/lib/classes/auth_plugins/StudipAuthLTI.class.php b/lib/classes/auth_plugins/StudipAuthLTI.class.php
index e8c316f..07ab8c3 100644
--- a/lib/classes/auth_plugins/StudipAuthLTI.class.php
+++ b/lib/classes/auth_plugins/StudipAuthLTI.class.php
@@ -9,8 +9,12 @@
* the License, or (at your option) any later version.
*/
+use Studip\OAuth2\NegotiatesWithPsr7;
+
class StudipAuthLTI extends StudipAuthSSO
{
+ use NegotiatesWithPsr7;
+
public $consumer_keys;
public $username;
public $domain;
@@ -62,24 +66,15 @@ class StudipAuthLTI extends StudipAuthSSO
*
* @return bool true if authentication succeeds
*
- * @throws OAuthException2 if the signature verification failed
- *
*/
public function isAuthenticated($username, $password)
{
- require_once 'vendor/oauth-php/library/OAuthRequestVerifier.php';
-
- OAuthStore::instance('PDO', [
- 'dsn' => 'mysql:host=' . $GLOBALS['DB_STUDIP_HOST'] . ';dbname=' . $GLOBALS['DB_STUDIP_DATABASE'],
- 'username' => $GLOBALS['DB_STUDIP_USER'],
- 'password' => $GLOBALS['DB_STUDIP_PASSWORD']
- ]);
-
$consumer_key = Request::get('oauth_consumer_key');
$consumer_secret = $this->consumer_keys[$consumer_key]['consumer_secret'];
- $oarv = new OAuthRequestVerifier();
- $oarv->verifySignature($consumer_secret, false, false);
+ if (!Studip\OAuth1::verifyRequest($this->getPsrRequest(), $consumer_secret, '')) {
+ return false;
+ }
return parent::isAuthenticated($username, $password);
}
@@ -93,8 +88,6 @@ class StudipAuthLTI extends StudipAuthSSO
* @param string $password the password (ignored)
*
* @return mixed if authentication succeeds: the Stud.IP user, else false
- *
- * @throws OAuthException2 if the signature verification failed
*/
public function authenticateUser($username, $password)
{
diff --git a/tests/unit/lib/classes/OAuth1Test.php b/tests/unit/lib/classes/OAuth1Test.php
new file mode 100644
index 0000000..84d4fb2
--- /dev/null
+++ b/tests/unit/lib/classes/OAuth1Test.php
@@ -0,0 +1,118 @@
+<?php
+
+use Psr\Http\Message\ServerRequestInterface;
+use Studip\OAuth1;
+
+/**
+ * All values are from the OAuth 1.0 Authentication Sandbox (using the example
+ * used in the OAuth Specification).
+ *
+ * @see http://lti.tools/oauth/
+ */
+final class OAuth1Test extends \Codeception\Test\Unit
+{
+ /**
+ * @covers OAuth1::getSignatureBaseString
+ */
+ public function testCreationOfBaseString(): void
+ {
+ $this->assertEquals(
+ 'GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal',
+ OAuth1::getSignatureBaseString($this->getDefaultTestRequest())
+ );
+ }
+
+ /**
+ * @covers OAuth1::signRequest
+ */
+ public function testSigningARequest(): void
+ {
+ $this->assertEquals(
+ 'tR3+Ty81lMeYAr/Fid0kMTYa/WM=',
+ OAuth1::signRequest(
+ $this->getDefaultTestRequest(),
+ 'kd94hf93k423kf44',
+ 'pfkkdhi9sl3r4s00',
+ 'HMAC-SHA1'
+ )
+ );
+ }
+
+ /**
+ * @covers OAuth1::verifyRequest
+ */
+ public function testVerifyingARequest(): void
+ {
+ $this->assertTrue(
+ OAuth1::verifyRequest(
+ $this->getDefaultTestRequest(['oauth_signature' => 'tR3+Ty81lMeYAr/Fid0kMTYa/WM=']),
+ 'kd94hf93k423kf44',
+ 'pfkkdhi9sl3r4s00'
+ )
+ );
+ }
+
+ /**
+ * @covers OAuth1::verifyRequest
+ * @covers OAuth1::extractParameters
+ */
+ public function testVerifyingARequestFromAuthorizationHeader(): void
+ {
+ $parameters = [
+ ...$this->getDefaultParameters(),
+ 'oauth_signature' => 'tR3+Ty81lMeYAr/Fid0kMTYa/WM='
+ ];
+
+
+ $request = $this->getTestRequest()->withHeader(
+ 'Authorization',
+ 'OAuth ' . implode(',', array_map(
+ fn($key, $value) => sprintf('%s="%s"', $key, $value),
+ array_keys($parameters),
+ array_values($parameters)
+ ))
+ );
+
+ $this->assertTrue(
+ OAuth1::verifyRequest(
+ $request,
+ 'kd94hf93k423kf44',
+ 'pfkkdhi9sl3r4s00'
+ )
+ );
+ }
+
+ private function getTestRequest(): ServerRequestInterface
+ {
+ $factory = new Slim\Psr7\Factory\ServerRequestFactory();
+ return $factory->createServerRequest(
+ 'GET',
+ 'http://photos.example.net/photos'
+ )->withQueryParams([
+ 'size' => 'original',
+ 'file' => 'vacation.jpg',
+ ]);
+ }
+
+ private function getDefaultTestRequest(array $parameters = []): ServerRequestInterface
+ {
+ $request = $this->getTestRequest();
+ return $request->withQueryParams([
+ ...$request->getQueryParams(),
+ ...$this->getDefaultParameters(),
+ ...$parameters,
+ ]);
+ }
+
+ private function getDefaultParameters(): array
+ {
+ return [
+ 'oauth_consumer_key' => 'dpf43f3p2l4k3l03',
+ 'oauth_token' => 'nnch734d00sl2jdk',
+ 'oauth_nonce' => 'kllo9940pd9333jh',
+ 'oauth_timestamp' => '1191242096',
+ 'oauth_signature_method' => 'HMAC-SHA1',
+ 'oauth_version' => '1.0',
+ ];
+ }
+}