Skip to content

Commit 32c0c3d

Browse files
authored
Merge pull request #18178 from craftcms/feature/session-authorizations
[6.x] Session authorizations + elevated sessions
2 parents bb67804 + d011664 commit 32c0c3d

36 files changed

+344
-234
lines changed

CHANGELOG-WIP.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ Craft's Mutex classes have been deprecated. [Laravel's atomic locking](https://l
142142
- Deprecated `\craft\records\RecoveryCodes`. `\CraftCms\Cms\Auth\Models\RecoveryCodes` should be used instead.
143143
- Deprecated `\craft\records\SsoIdentity`. `\CraftCms\Cms\Auth\Models\SsoIdentity` should be used instead.
144144
- Deprecated `\craft\records\WebAuthn`. `\CraftCms\Cms\Auth\Models\WebAuthn` should be used instead.
145+
- Deprecated `craft\behaviors\SessionBehavior::authorize`. `CraftCms\Cms\Auth\SessionAuth::authorize` should be used instead.
146+
- Deprecated `craft\behaviors\SessionBehavior::deauthorize`. `CraftCms\Cms\Auth\SessionAuth::deauthorize` should be used instead.
147+
- Deprecated `craft\behaviors\SessionBehavior::checkAuthorization`. `CraftCms\Cms\Auth\SessionAuth::checkAuthorization` should be used instead.
148+
- Deprecated `GeneralConfig::elevatedSessionDuration()`. The `auth.password_timeout` config value should be used instead. To disable password confirmation (elevated sessions), you now set this value to `-1` instead of `0`.
149+
- Elevated sessions now work through [Laravel's password confirmation](https://laravel.com/docs/12.x/authentication#password-confirmation) system.
145150

146151
## Drafts
147152

routes/actions.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
use CraftCms\Cms\Http\Middleware\RequireAdmin;
5454
use CraftCms\Cms\Http\Middleware\RequireAdminChanges;
5555
use CraftCms\Cms\Http\Middleware\RequireEdition;
56-
use CraftCms\Cms\Http\Middleware\RequireElevatedSession;
5756
use CraftCms\Cms\Http\Middleware\RequireToken;
5857
use CraftCms\Cms\Support\Str;
5958
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
@@ -273,7 +272,7 @@
273272
Route::post('app/cache-updates', [UpdatesController::class, 'cache']);
274273

275274
// Users
276-
Route::middleware([RequireElevatedSession::class])->group(function () {
275+
Route::middleware('password.confirm')->group(function () {
277276
Route::post('users/impersonate', [ImpersonationController::class, 'impersonate']);
278277
Route::post('users/get-impersonation-url', [ImpersonationController::class, 'getUrl']);
279278
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CraftCms\Cms\Auth\Concerns;
6+
7+
use Illuminate\Support\Facades\Auth;
8+
use Illuminate\Support\Facades\Date;
9+
use Illuminate\Support\Facades\Session;
10+
11+
use function CraftCms\Cms\t;
12+
13+
trait ConfirmsPasswords
14+
{
15+
protected function confirmPassword(): void
16+
{
17+
Session::passwordConfirmed();
18+
}
19+
20+
protected function requireConfirmedPassword(?string $message = null): void
21+
{
22+
abort_unless(
23+
$this->isPasswordConfirmed(),
24+
403,
25+
$message ?? t('This action may only be performed with an elevated session.')
26+
);
27+
}
28+
29+
protected function isPasswordConfirmed(): bool
30+
{
31+
$maximumSecondsSinceConfirmation = config('auth.password_timeout', 900);
32+
33+
if ($maximumSecondsSinceConfirmation === -1) {
34+
return true;
35+
}
36+
37+
return $this->confirmedPasswordTimeout() !== 0;
38+
}
39+
40+
protected function confirmedPasswordTimeout(): int|false
41+
{
42+
if (Auth::check()) {
43+
$maximumSecondsSinceConfirmation = config('auth.password_timeout', 300);
44+
$confirmedAt = Session::get('auth.password_confirmed_at');
45+
46+
if ($confirmedAt !== null) {
47+
$diff = Date::now()->unix() - $confirmedAt;
48+
$remainingTime = $maximumSecondsSinceConfirmation - $diff;
49+
50+
if ($remainingTime >= 0) {
51+
return $remainingTime;
52+
}
53+
}
54+
}
55+
56+
// If it has been disabled, return false.
57+
if (config('auth.password_timeout') === -1) {
58+
return false;
59+
}
60+
61+
return 0;
62+
}
63+
}
Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@
22

33
declare(strict_types=1);
44

5-
namespace CraftCms\Cms\Http;
5+
namespace CraftCms\Cms\Auth\Concerns;
66

77
use Craft;
88
use craft\elements\Entry;
9+
use CraftCms\Cms\Auth\SessionAuth;
910
use CraftCms\Cms\Site\Data\Site;
1011
use CraftCms\Cms\Support\Facades\Sites;
11-
use CraftCms\Yii2Adapter\IdentityWrapper;
1212
use Illuminate\Support\Facades\Auth;
1313

14-
use function CraftCms\Cms\t;
15-
1614
trait EnforcesPermissions
1715
{
1816
protected function enforeSitePermission(Site $site): void
@@ -42,7 +40,7 @@ protected function enforceEditEntryPermissions(Entry $entry, bool $duplicate = f
4240

4341
protected function requireSessionAuthorization(string $permission): void
4442
{
45-
if (! Craft::$app->getSession()->checkAuthorization($permission)) {
43+
if (! SessionAuth::checkAuthorization($permission)) {
4644
abort(403, 'User is not authorized to perform this action.');
4745
}
4846
}
@@ -58,19 +56,6 @@ protected function requirePermission(string $permission): void
5856
}
5957
}
6058

61-
protected function requireElevatedSession(): void
62-
{
63-
Craft::$app->getUser()->setIdentity(
64-
new IdentityWrapper(Auth::user()),
65-
);
66-
67-
abort_unless(
68-
Craft::$app->getUser()->getHasElevatedSession(),
69-
403,
70-
t('This action may only be performed with an elevated session.'),
71-
);
72-
}
73-
7459
protected function requireAdmin(): void
7560
{
7661
abort_unless(Auth::user()->isAdmin(), 403, 'User is not permitted to perform this action.');

src/Auth/SessionAuth.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CraftCms\Cms\Auth;
6+
7+
use CraftCms\Cms\Cms;
8+
use Illuminate\Support\Facades\Cache;
9+
use Illuminate\Support\Facades\Session;
10+
11+
final class SessionAuth
12+
{
13+
private const string AUTH_LOCK_NAME = 'authAccess';
14+
15+
/**
16+
* @var string|null The session variable name used to store the authorization keys for the current session.
17+
*
18+
* @see authorize()
19+
* @see deauthorize()
20+
* @see checkAuthorization()
21+
*/
22+
public static ?string $authAccessParam = '__auth_access';
23+
24+
/**
25+
* Authorizes the user to perform an action for the duration of the session.
26+
*/
27+
public static function authorize(string $action): void
28+
{
29+
Cache::lock(self::AUTH_LOCK_NAME)->block(5, function () use ($action) {
30+
$access = Session::get(self::authAccessParam(), []);
31+
32+
if (in_array($action, $access, true)) {
33+
return;
34+
}
35+
36+
$access[] = $action;
37+
38+
Session::put(self::authAccessParam(), $access);
39+
});
40+
}
41+
42+
/**
43+
* Deauthorizes the user from performing an action.
44+
*/
45+
public static function deauthorize(string $action): void
46+
{
47+
Cache::lock(self::AUTH_LOCK_NAME)->block(5, function () use ($action) {
48+
$access = Session::get(self::authAccessParam(), []);
49+
$index = array_search($action, $access, true);
50+
51+
if ($index === false) {
52+
return;
53+
}
54+
55+
array_splice($access, $index, 1);
56+
57+
Session::put(self::authAccessParam(), $access);
58+
});
59+
}
60+
61+
/**
62+
* Returns whether the user is authorized to perform an action.
63+
*/
64+
public static function checkAuthorization(string $action): bool
65+
{
66+
$access = Session::get(self::authAccessParam(), []);
67+
68+
return in_array($action, $access, true);
69+
}
70+
71+
private static function authAccessParam(): string
72+
{
73+
$prefix = md5('CraftSession'.Cms::envId());
74+
75+
return $prefix.self::$authAccessParam;
76+
}
77+
}

src/Cms.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,9 @@ public static function systemName(): string
4141

4242
return $name ?: config('app.name', 'Craft');
4343
}
44+
45+
public static function envId(): string
46+
{
47+
return sprintf('%s--%s', self::systemName(), app()->environment());
48+
}
4449
}

0 commit comments

Comments
 (0)