diff options
Diffstat (limited to 'lib/classes')
| -rw-r--r-- | lib/classes/LtiLink.php | 16 | ||||
| -rw-r--r-- | lib/classes/OAuth1.php | 167 | ||||
| -rw-r--r-- | lib/classes/auth_plugins/StudipAuthLTI.class.php | 21 |
3 files changed, 183 insertions, 21 deletions
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) { |
