Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
YiiSessionCsrfTokenManager
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
6 / 6
18
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getToken
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 validateToken
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
8
 regenerateToken
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 generateAndStoreToken
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 ensureSessionActive
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Security\Csrf;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use Yiisoft\Session\SessionInterface;
12
13/**
14 * Yii Session-Backed CSRF Token Manager.
15 *
16 * Implements CsrfTokenManagerInterface storing cryptographically strong random tokens
17 * inside Yii3 Session storage with constant-time verification.
18 *
19 * @package App\Core\Security\Csrf
20 */
21final class YiiSessionCsrfTokenManager implements CsrfTokenManagerInterface
22{
23    /** @var string Session storage key for active CSRF token. */
24    public const string SESSION_CSRF_KEY = '_ammonly_csrf_token';
25
26    /**
27     * YiiSessionCsrfTokenManager constructor.
28     *
29     * @param SessionInterface $session Active Yii3 session instance.
30     */
31    public function __construct(
32        private readonly SessionInterface $session
33    ) {
34    }
35
36    /**
37     * {@inheritdoc}
38     */
39    public function getToken(): string
40    {
41        $this->ensureSessionActive();
42
43        if ($this->session->has(self::SESSION_CSRF_KEY)) {
44            $token = (string) $this->session->get(self::SESSION_CSRF_KEY);
45            if ($token !== '') {
46                return $token;
47            }
48        }
49
50        if (!empty($_SESSION[self::SESSION_CSRF_KEY]) && is_string($_SESSION[self::SESSION_CSRF_KEY])) {
51            $token = (string) $_SESSION[self::SESSION_CSRF_KEY];
52            $this->session->set(self::SESSION_CSRF_KEY, $token);
53            return $token;
54        }
55
56        return $this->generateAndStoreToken();
57    }
58
59    /**
60     * {@inheritdoc}
61     */
62    public function validateToken(?string $token): bool
63    {
64        if ($token === null || $token === '') {
65            return false;
66        }
67
68        $activeToken = $this->getToken();
69        $fallbackToken = (!empty($_SESSION[self::SESSION_CSRF_KEY]) && is_string($_SESSION[self::SESSION_CSRF_KEY]))
70            ? (string) $_SESSION[self::SESSION_CSRF_KEY]
71            : null;
72
73        return ($activeToken !== '' && hash_equals($activeToken, $token))
74            || ($fallbackToken !== null && hash_equals($fallbackToken, $token));
75    }
76
77    /**
78     * {@inheritdoc}
79     */
80    public function regenerateToken(): string
81    {
82        $this->ensureSessionActive();
83        return $this->generateAndStoreToken();
84    }
85
86    /**
87     * Generates a random 32-byte hex token and persists it to active session.
88     *
89     * @return string Generated CSRF token.
90     */
91    private function generateAndStoreToken(): string
92    {
93        $newToken = bin2hex(random_bytes(32));
94        $this->session->set(self::SESSION_CSRF_KEY, $newToken);
95        return $newToken;
96    }
97
98    /**
99     * Ensures session storage is active.
100     */
101    private function ensureSessionActive(): void
102    {
103        if (!$this->session->isActive()) {
104            $this->session->open();
105        }
106    }
107}