Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.63% covered (warning)
87.63%
85 / 97
62.50% covered (warning)
62.50%
10 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Auth
87.63% covered (warning)
87.63%
85 / 97
62.50% covered (warning)
62.50%
10 / 16
45.34
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 logout
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
11.53
 authenticate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 authenticateByOneTimeToken
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getOneTimeToken
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 invalidateOneTimeToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 user
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 userFromToken
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 permissions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getAuthToken
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 remember
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 login
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 startSession
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 rememberUser
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 forgetRemembered
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 getSessionTokenHash
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray;
6
7use Psr\Http\Message\ServerRequestInterface as Request;
8use RuntimeException;
9
10class Auth
11{
12    public function __construct(
13        protected Request $request,
14        protected Users $users,
15        protected Config $config,
16        protected ?Session $session = null,
17    ) {}
18
19    public function logout(): void
20    {
21        if (!$this->session) {
22            return;
23        }
24
25        $session = $this->session;
26        $hash = $this->getSessionTokenHash();
27
28        if ($hash) {
29            $this->users->forget($hash);
30            $session->forgetRemembered();
31        }
32
33        if ($session->active()) {
34            $session->destroy();
35        }
36    }
37
38    public function authenticate(
39        string $login,
40        #[\SensitiveParameter]
41        string $password,
42        bool $remember,
43        bool $initSession,
44    ): User|false {
45        $user = $this->users->byLogin($login);
46
47        if (!$user) {
48            return false;
49        }
50
51        if (password_verify($password, $user->password)) {
52            if ($initSession) {
53                $this->login($user->id, $remember);
54            }
55
56            return $user;
57        }
58
59        return false;
60    }
61
62    public function authenticateByOneTimeToken(
63        #[\SensitiveParameter]
64        string $token,
65        bool $initSession,
66    ): User|false {
67        $user = $this->users->byOneTimeToken($token);
68
69        if (!$user) {
70            return false;
71        }
72
73        if ($initSession) {
74            $this->login($user->id, false);
75        }
76
77        return $user;
78    }
79
80    public function getOneTimeToken(
81        #[\SensitiveParameter]
82        string $token,
83    ): string|false {
84        $user = $this->users->byAuthToken($token);
85
86        if (!$user) {
87            return false;
88        }
89
90        return $this->users->createOneTimeToken($user->id);
91    }
92
93    public function invalidateOneTimeToken(
94        #[\SensitiveParameter]
95        string $token,
96    ): void {
97        $this->users->removeOneTimeToken($token);
98    }
99
100    public function user(): ?User
101    {
102        if (!$this->session) {
103            return $this->userFromToken();
104        }
105
106        // Verify if user is logged in via cookie session
107        $userId = $this->session->authenticatedUserId();
108
109        if ($userId) {
110            return $this->users->byId($userId);
111        }
112
113        $hash = $this->getSessionTokenHash();
114
115        if ($hash) {
116            $user = $this->users->bySession($hash);
117
118            if ($user && !(strtotime($user->expires) < time())) {
119                $this->startSession($user->id);
120                $this->rememberUser($user->id);
121
122                return $user;
123            }
124
125            $this->users->forget($hash);
126            $this->session->forgetRemembered();
127        }
128
129        // Fall back to token auth if session auth failed
130        return $this->userFromToken();
131    }
132
133    protected function userFromToken(): ?User
134    {
135        $authToken = $this->getAuthToken();
136
137        if ($authToken) {
138            return $this->users->byAuthToken($authToken);
139        }
140
141        return null;
142    }
143
144    public function permissions(): array
145    {
146        $user = $this->user();
147
148        if ($user === null) {
149            return [];
150        }
151
152        return $user->permissions();
153    }
154
155    public function getAuthToken(): string
156    {
157        $authToken = '';
158        $bearer = $this->request->getHeaderLine('Authentication');
159
160        if (preg_match('/Bearer\s(\S+)/', $bearer, $matches)) {
161            $authToken = $matches[1];
162        }
163
164        return $authToken;
165    }
166
167    protected function remember(int $userId): RememberDetails
168    {
169        $token = new Token($this->config->app->secret);
170        $expires = time() + $this->config->auth->rememberLifetime;
171
172        $remembered = $this->users->remember(
173            $token->hash(),
174            $userId,
175            date(DATE_ATOM, $expires),
176        );
177
178        if ($remembered) {
179            return new RememberDetails($token, $expires);
180        }
181
182        throw new RuntimeException('Could not remember user');
183    }
184
185    protected function login(int $userId, bool $remember): void
186    {
187        $this->startSession($userId);
188
189        if ($remember) {
190            $this->rememberUser($userId);
191        } else {
192            $this->forgetRemembered();
193        }
194    }
195
196    private function startSession(int $userId): void
197    {
198        $session = $this->session;
199
200        if (!$session) {
201            throw new RuntimeException('Cannot initialize auth session without session service');
202        }
203
204        if (!$session->active()) {
205            $session->start();
206        }
207
208        // Regenerate the session id before setting the user id
209        // to mitigate session fixation attack.
210        $session->regenerate();
211        $session->setUser($userId);
212    }
213
214    private function rememberUser(int $userId): void
215    {
216        if (!$this->session) {
217            throw new RuntimeException('Cannot remember user without session service');
218        }
219
220        $details = $this->remember($userId);
221
222        $this->session->remember(
223            $details->token,
224            $details->expires,
225        );
226    }
227
228    private function forgetRemembered(): void
229    {
230        if (!$this->session) {
231            return;
232        }
233
234        $hash = $this->getSessionTokenHash();
235
236        if ($hash !== null) {
237            $this->users->forget($hash);
238            $this->session->forgetRemembered();
239        }
240    }
241
242    protected function getSessionTokenHash(): ?string
243    {
244        if (!$this->session) {
245            return null;
246        }
247
248        $token = $this->session->getAuthToken();
249
250        if ($token) {
251            return new Token($this->config->app->secret, $token)->hash();
252        }
253
254        return null;
255    }
256}