mirror of
https://github.com/flarum/core.git
synced 2025-07-24 10:11:43 +02:00
Refactor Access Tokens (#2651)
- Make session token-based instead of user-based - Clear current session access tokens on logout - Introduce increment ID so we can show tokens to moderators in the future without exposing secrets - Switch to type classes to manage the different token types. New implementation fixes #2075 - Drop ability to customize lifetime per-token - Add developer access keys that don't expire. These must be created from the database for now - Add title in preparation for the developer token UI - Add IP and user agent logging - Delete all non-remember tokens in migration
This commit is contained in:
@@ -45,7 +45,7 @@ trait BuildsHttpRequests
|
||||
'user_id' => $userId,
|
||||
'created_at' => Carbon::now()->toDateTimeString(),
|
||||
'last_activity_at' => Carbon::now()->toDateTimeString(),
|
||||
'lifetime_seconds' => 3600
|
||||
'type' => 'session'
|
||||
]);
|
||||
|
||||
return $req->withAddedHeader('Authorization', "Token {$token}");
|
||||
|
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tests\integration\api\access_tokens;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Http\AccessToken;
|
||||
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
use Laminas\Diactoros\ServerRequest;
|
||||
|
||||
class AccessTokenLifecycleTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->prepareDatabase([
|
||||
'access_tokens' => [
|
||||
['token' => 'a', 'user_id' => 1, 'last_activity_at' => Carbon::parse('2021-01-01 02:00:00'), 'type' => 'session'],
|
||||
['token' => 'b', 'user_id' => 1, 'last_activity_at' => Carbon::parse('2021-01-01 02:00:00'), 'type' => 'session_remember'],
|
||||
['token' => 'c', 'user_id' => 1, 'last_activity_at' => Carbon::parse('2021-01-01 02:00:00'), 'type' => 'developer'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function tokens_expire()
|
||||
{
|
||||
$this->populateDatabase();
|
||||
|
||||
// 30 minutes after last activity
|
||||
$this->assertEquals([], AccessToken::whereExpired(Carbon::parse('2021-01-01 02:30:00'))->pluck('token')->all());
|
||||
|
||||
// 1h30 after last activity
|
||||
$this->assertEquals(['a'], AccessToken::whereExpired(Carbon::parse('2021-01-01 03:30:00'))->pluck('token')->all());
|
||||
|
||||
// 6 years after last activity
|
||||
$this->assertEquals(['a', 'b'], AccessToken::whereExpired(Carbon::parse('2027-01-01 01:00:00'))->pluck('token')->sort()->values()->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function tokens_valid()
|
||||
{
|
||||
$this->populateDatabase();
|
||||
|
||||
// 30 minutes after last activity
|
||||
$this->assertEquals(['a', 'b', 'c'], AccessToken::whereValid(Carbon::parse('2021-01-01 02:30:00'))->pluck('token')->sort()->values()->all());
|
||||
|
||||
// 1h30 after last activity
|
||||
$this->assertEquals(['b', 'c'], AccessToken::whereValid(Carbon::parse('2021-01-01 03:30:00'))->pluck('token')->sort()->values()->all());
|
||||
|
||||
// 6 years after last activity
|
||||
$this->assertEquals(['c'], AccessToken::whereValid(Carbon::parse('2027-01-01 01:00:00'))->pluck('token')->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function touch_updates_lifetime()
|
||||
{
|
||||
$this->populateDatabase();
|
||||
|
||||
// 45 minutes after last activity
|
||||
Carbon::setTestNow('2021-01-01 02:45:00');
|
||||
$token = AccessToken::findValid('a');
|
||||
$this->assertNotNull($token);
|
||||
$token->touch();
|
||||
Carbon::setTestNow();
|
||||
|
||||
// 1h30 after original last activity, 45 minutes after touch
|
||||
$this->assertTrue(AccessToken::whereValid(Carbon::parse('2021-01-01 03:30:00'))->whereToken('a')->exists());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function touch_without_request()
|
||||
{
|
||||
$this->populateDatabase();
|
||||
|
||||
/** @var AccessToken $token */
|
||||
$token = AccessToken::whereToken('a')->firstOrFail();
|
||||
$token->touch();
|
||||
|
||||
/** @var AccessToken $token */
|
||||
$token = AccessToken::whereToken('a')->firstOrFail();
|
||||
$this->assertNull($token->last_ip_address);
|
||||
$this->assertNull($token->last_user_agent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function touch_with_request()
|
||||
{
|
||||
$this->populateDatabase();
|
||||
|
||||
/** @var AccessToken $token */
|
||||
$token = AccessToken::whereToken('a')->firstOrFail();
|
||||
$token->touch((new ServerRequest([
|
||||
'HTTP_USER_AGENT' => 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36',
|
||||
]))->withAttribute('ipAddress', '8.8.8.8'));
|
||||
|
||||
/** @var AccessToken $token */
|
||||
$token = AccessToken::whereToken('a')->firstOrFail();
|
||||
$this->assertEquals('8.8.8.8', $token->last_ip_address);
|
||||
$this->assertEquals('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36', $token->last_user_agent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function long_user_agent_id_truncated()
|
||||
{
|
||||
$this->populateDatabase();
|
||||
|
||||
/** @var AccessToken $token */
|
||||
$token = AccessToken::whereToken('a')->firstOrFail();
|
||||
$token->touch(new ServerRequest([
|
||||
'HTTP_USER_AGENT' => str_repeat('a', 500),
|
||||
]));
|
||||
|
||||
/** @var AccessToken $token */
|
||||
$token = AccessToken::whereToken('a')->firstOrFail();
|
||||
$this->assertEquals(255, strlen($token->last_user_agent));
|
||||
}
|
||||
}
|
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tests\integration\api\access_tokens;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
|
||||
class RemembererTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->prepareDatabase([
|
||||
'access_tokens' => [
|
||||
['token' => 'a', 'user_id' => 1, 'last_activity_at' => Carbon::parse('2021-01-01 02:00:00'), 'type' => 'session'],
|
||||
['token' => 'b', 'user_id' => 1, 'last_activity_at' => Carbon::parse('2021-01-01 02:00:00'), 'type' => 'session_remember'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function non_remember_tokens_cannot_be_used()
|
||||
{
|
||||
$this->populateDatabase();
|
||||
|
||||
Carbon::setTestNow('2021-01-01 02:30:00');
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api')->withCookieParams([
|
||||
'flarum_remember' => 'a',
|
||||
])
|
||||
);
|
||||
|
||||
Carbon::setTestNow();
|
||||
|
||||
$data = json_decode($response->getBody(), true);
|
||||
$this->assertFalse($data['data']['attributes']['canViewUserList']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function expired_tokens_cannot_be_used()
|
||||
{
|
||||
$this->populateDatabase();
|
||||
|
||||
Carbon::setTestNow('2027-01-01 02:30:00');
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api')->withCookieParams([
|
||||
'flarum_remember' => 'b',
|
||||
])
|
||||
);
|
||||
|
||||
Carbon::setTestNow();
|
||||
|
||||
$data = json_decode($response->getBody(), true);
|
||||
$this->assertFalse($data['data']['attributes']['canViewUserList']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function valid_tokens_can_be_used()
|
||||
{
|
||||
$this->populateDatabase();
|
||||
|
||||
Carbon::setTestNow('2021-01-01 02:30:00');
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api')->withCookieParams([
|
||||
'flarum_remember' => 'b',
|
||||
])
|
||||
);
|
||||
|
||||
Carbon::setTestNow();
|
||||
|
||||
$data = json_decode($response->getBody(), true);
|
||||
$this->assertTrue($data['data']['attributes']['canViewUserList']);
|
||||
}
|
||||
}
|
@@ -60,7 +60,7 @@ class WithTokenTest extends TestCase
|
||||
|
||||
// ...and an access token belonging to this user.
|
||||
$token = $data['token'];
|
||||
$this->assertEquals(2, AccessToken::findOrFail($token)->user_id);
|
||||
$this->assertEquals(2, AccessToken::whereToken($token)->firstOrFail()->user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -193,7 +193,7 @@ class RequireCsrfTokenTest extends TestCase
|
||||
public function access_token_does_not_need_csrf_token()
|
||||
{
|
||||
$this->database()->table('access_tokens')->insert(
|
||||
['token' => 'myaccesstoken', 'user_id' => 1]
|
||||
['token' => 'myaccesstoken', 'user_id' => 1, 'type' => 'developer']
|
||||
);
|
||||
|
||||
$response = $this->send(
|
||||
|
Reference in New Issue
Block a user