Skip to content
Snippets Groups Projects
Commit addf6e2d authored by Jan-Hendrik Willms's avatar Jan-Hendrik Willms Committed by David Siegfried
Browse files

add Studip\OAuth1 class to sign or verify PSR7 requests, fixes #4203

Closes #4203

Merge request studip/studip!3030
parent e4876785
No related branches found
No related tags found
No related merge requests found
<?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');
......
......@@ -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
);
}
}
<?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;
}
}
......@@ -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)
{
......
<?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',
];
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment