diff --git a/app/controllers/course/lti.php b/app/controllers/course/lti.php
index e0ca2cf29129f9e18933be76ae134fb1dadc17f4..4db777661b5a3792cecd3d794658a272e884a2dc 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 801b6c0035cdb4d23d0d92d26286ea49d461919a..1423cefaa307a2df97b9c9b6eca9feb41e8f4f81 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 0000000000000000000000000000000000000000..1695f9f0d200dbef746550852ad50de8c4a8c030
--- /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 e8c316fe4996a5d4f08df679017d0f177077e432..07ab8c378c98af523821b386afb3020f4f4e759d 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 0000000000000000000000000000000000000000..84d4fb204426f711ffa6f28857f4f8026dbcbf50
--- /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',
+        ];
+    }
+}