diff --git a/src/Api/ApiKey.php b/src/Api/ApiKey.php
index e8ebb48ec..1adfb9b83 100644
--- a/src/Api/ApiKey.php
+++ b/src/Api/ApiKey.php
@@ -11,7 +11,9 @@
 
 namespace Flarum\Api;
 
+use Carbon\Carbon;
 use Flarum\Database\AbstractModel;
+use Flarum\User\User;
 
 /**
  * @property int $id
@@ -19,11 +21,14 @@ use Flarum\Database\AbstractModel;
  * @property string|null $allowed_ips
  * @property string|null $scopes
  * @property int|null $user_id
+ * @property \Flarum\User\User|null $user
  * @property \Carbon\Carbon $created_at
  * @property \Carbon\Carbon|null $last_activity_at
  */
 class ApiKey extends AbstractModel
 {
+    protected $dates = ['last_activity_at'];
+
     /**
      * Generate an API key.
      *
@@ -37,4 +42,16 @@ class ApiKey extends AbstractModel
 
         return $key;
     }
+
+    public function touch()
+    {
+        $this->last_activity_at = Carbon::now();
+
+        return $this->save();
+    }
+
+    public function user()
+    {
+        return $this->belongsTo(User::class);
+    }
 }
diff --git a/src/Http/Middleware/AuthenticateWithHeader.php b/src/Http/Middleware/AuthenticateWithHeader.php
index 658d064ab..787470c90 100644
--- a/src/Http/Middleware/AuthenticateWithHeader.php
+++ b/src/Http/Middleware/AuthenticateWithHeader.php
@@ -32,13 +32,14 @@ class AuthenticateWithHeader implements Middleware
         if (isset($parts[0]) && starts_with($parts[0], self::TOKEN_PREFIX)) {
             $id = substr($parts[0], strlen(self::TOKEN_PREFIX));
 
-            if (isset($parts[1])) {
-                if ($key = ApiKey::find($id)) {
-                    $actor = $this->getUser($parts[1]);
+            if ($key = ApiKey::where('key', $id)->first()) {
+                $key->touch();
 
-                    $request = $request->withAttribute('apiKey', $key);
-                    $request = $request->withAttribute('bypassFloodgate', true);
-                }
+                $userId = $parts[1] ?? '';
+                $actor = $key->user ?? $this->getUser($userId);
+
+                $request = $request->withAttribute('apiKey', $key);
+                $request = $request->withAttribute('bypassFloodgate', true);
             } elseif ($token = AccessToken::find($id)) {
                 $token->touch();
 
diff --git a/tests/Api/Auth/AuthenticateWithApiKeyTest.php b/tests/Api/Auth/AuthenticateWithApiKeyTest.php
new file mode 100644
index 000000000..0d2f179a5
--- /dev/null
+++ b/tests/Api/Auth/AuthenticateWithApiKeyTest.php
@@ -0,0 +1,150 @@
+<?php
+
+/*
+ * This file is part of Flarum.
+ *
+ * (c) Toby Zerner <toby.zerner@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Flarum\Tests\Api\Auth;
+
+use Carbon\Carbon;
+use Flarum\Api\ApiKey;
+use Flarum\Api\Controller\CreateGroupController;
+use Flarum\Tests\Test\Concerns\RetrievesAuthorizedUsers;
+use Flarum\Tests\Test\TestCase;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Zend\Diactoros\Response;
+use Zend\Diactoros\ServerRequestFactory;
+use Zend\Stratigility\MiddlewarePipe;
+
+class AuthenticateWithApiKeyTest extends TestCase
+{
+    use RetrievesAuthorizedUsers;
+
+    protected function key(int $user_id = null): ApiKey
+    {
+        return ApiKey::unguarded(function () use ($user_id) {
+            return ApiKey::query()->firstOrCreate([
+                'key'        => str_random(),
+                'user_id'    => $user_id,
+                'created_at' => Carbon::now()
+            ]);
+        });
+    }
+
+    /**
+     * @test
+     * @expectedException \Flarum\User\Exception\PermissionDeniedException
+     */
+    public function cannot_authorize_without_key()
+    {
+        $this->call(
+            CreateGroupController::class
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function master_token_can_authenticate_as_anyone()
+    {
+        $key = $this->key();
+
+        $request = ServerRequestFactory::fromGlobals()
+            ->withAddedHeader('Authorization', "Token {$key->key}; userId=1");
+
+        $pipe = $this->injectAuthorizationPipeline();
+
+        $response = $pipe->handle($request);
+
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals(1, $response->getHeader('X-Authenticated-As')[0]);
+
+        $key = $key->refresh();
+
+        $this->assertNotNull($key->last_activity_at);
+
+        $key->delete();
+    }
+
+    /**
+     * @test
+     */
+    public function personal_api_token_cannot_authenticate_as_anyone()
+    {
+        $user = $this->getNormalUser();
+
+        $key = $this->key($user->id);
+
+        $request = ServerRequestFactory::fromGlobals()
+            ->withAddedHeader('Authorization', "Token {$key->key}; userId=1");
+
+        $pipe = $this->injectAuthorizationPipeline();
+
+        $response = $pipe->handle($request);
+
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals($user->id, $response->getHeader('X-Authenticated-As')[0]);
+
+        $key = $key->refresh();
+
+        $this->assertNotNull($key->last_activity_at);
+
+        $key->delete();
+    }
+
+    /**
+     * @test
+     */
+    public function personal_api_token_authenticates_user()
+    {
+        $user = $this->getNormalUser();
+
+        $key = $this->key($user->id);
+
+        $request = ServerRequestFactory::fromGlobals()
+            ->withAddedHeader('Authorization', "Token {$key->key}");
+
+        $pipe = $this->injectAuthorizationPipeline();
+
+        $response = $pipe->handle($request);
+
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals($user->id, $response->getHeader('X-Authenticated-As')[0]);
+
+        $key = $key->refresh();
+
+        $this->assertNotNull($key->last_activity_at);
+
+        $key->delete();
+    }
+
+    protected function injectAuthorizationPipeline(): MiddlewarePipe
+    {
+        app()->resolving('flarum.api.middleware', function ($pipeline) {
+            $pipeline->pipe(new class implements MiddlewareInterface {
+                public function process(
+                    ServerRequestInterface $request,
+                    RequestHandlerInterface $handler
+                ): ResponseInterface {
+                    if ($actor = $request->getAttribute('actor')) {
+                        return new Response\EmptyResponse(200, [
+                            'X-Authenticated-As' => $actor->id
+                        ]);
+                    }
+                }
+            });
+        });
+
+        $pipe = app('flarum.api.middleware');
+
+        return $pipe;
+    }
+}
diff --git a/tests/tmp/storage/sessions/.gitkeep b/tests/tmp/storage/sessions/.gitkeep
new file mode 100644
index 000000000..e69de29bb