Passed
Push — master ( 8c16bf...51caa9 )
by Arnold
02:35
created

Auth::forMultipleRequests()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Jasny\Auth;
6
7
use Jasny\Auth\AuthzInterface as Authz;
8
use Jasny\Auth\Confirmation\ConfirmationInterface as Confirmation;
9
use Jasny\Auth\Confirmation\NoConfirmation;
10
use Jasny\Auth\ContextInterface as Context;
11
use Jasny\Auth\Session\PhpSession;
12
use Jasny\Auth\Session\SessionInterface as Session;
13
use Jasny\Auth\StorageInterface as Storage;
14
use Jasny\Auth\UserInterface as User;
15
use Jasny\Immutable;
16
use Psr\EventDispatcher\EventDispatcherInterface as EventDispatcher;
17
18
/**
19
 * Authentication and authorization.
20
 */
21
class Auth implements Authz
22
{
23
    use Immutable\With;
24
25
    /**
26
     * Stateful authz service.
27
     * A new copy will be set if user is logged in or out, or if context changes.
28
     */
29
    protected Authz $authz;
30
31
    protected Session $session;
32
    protected Storage $storage;
33
    protected Confirmation $confirmation;
34
    protected EventDispatcher $dispatcher;
35
36
    /** Allow service to be re-initialized */
37
    protected bool $forMultipleRequests = false;
38
39
    /**
40
     * Auth constructor.
41
     */
42 32
    public function __construct(Authz $authz, Storage $storage, ?Confirmation $confirmation = null)
43
    {
44 32
        $this->authz = $authz;
45 32
        $this->storage = $storage;
46 32
        $this->confirmation = $confirmation ?? new NoConfirmation();
47
48
        // Set default services
49 32
        $this->dispatcher = self::dummyDispatcher();
50 32
    }
51
52
    /**
53
     * Get a copy of the service that allows reinitializing it.
54
     *
55
     * @return static
56
     */
57 1
    public function forMultipleRequests(): self
58
    {
59 1
        return $this->withProperty('forMultipleRequests', true);
60
    }
61
62
    /**
63
     * Get a copy with an event dispatcher.
64
     */
65 32
    public function withEventDispatcher(EventDispatcher $dispatcher): self
66
    {
67 32
        return $this->withProperty('dispatcher', $dispatcher);
68
    }
69
70
71
    /**
72
     * Initialize the service using session information.
73
     */
74 7
    public function initialize(?Session $session = null): void
75
    {
76 7
        if ($this->isInitialized()) {
77 2
            if (!$this->forMultipleRequests) {
78 1
                throw new \LogicException("Auth service is already initialized");
79
            }
80
81 1
            $this->authz = $this->authz()->forUser(null)->inContextOf(null);
82
        }
83
84 6
        $this->session = $session ?? new PhpSession();
85 6
        ['user' => $user, 'context' => $context] = $this->getInfoFromSession();
86
87 6
        $this->authz = $this->authz->forUser($user)->inContextOf($context);
88 6
    }
89
90
    /**
91
     * Get user and context from session, loading objects from storage.
92
     *
93
     * @return array{user:User|null,context:Context|null}
94
     */
95 6
    protected function getInfoFromSession()
96
    {
97 6
        ['user' => $uid, 'context' => $cid, 'checksum' => $checksum] = $this->session->getInfo();
98
99 6
        $user = $uid !== null
100 5
            ? ($uid instanceof User ? $uid : $this->storage->fetchUserById($uid))
101 6
            : null;
102
103 6
        if ($user !== null && $user->getAuthChecksum() !== $checksum) {
104 1
            return ['user' => null, 'context' => null];
105
        }
106
107 5
        $context = $cid !== null
108 2
            ? ($cid instanceof Context ? $cid : $this->storage->fetchContext($cid))
109 5
            : ($user !== null ? $this->storage->getContextForUser($user) : null);
110
111 5
        return ['user' => $user, 'context' => $context];
112
    }
113
114
    /**
115
     * Is the service is initialized?
116
     */
117 26
    public function isInitialized(): bool
118
    {
119 26
        return isset($this->session);
120
    }
121
122
    /**
123
     * Throw an exception if the service hasn't been initialized yet.
124
     *
125
     * @throws \LogicException
126
     */
127 19
    protected function assertInitialized(): void
128
    {
129 19
        if (!$this->isInitialized()) {
130 3
            throw new \LogicException("Auth needs to be initialized before use");
131
        }
132 16
    }
133
134
135
    /**
136
     * Get all available authorization roles (for the current context).
137
     *
138
     * @return string[]
139
     */
140 1
    final public function getAvailableRoles(): array
141
    {
142 1
        return $this->authz->getAvailableRoles();
143
    }
144
145
146
    /**
147
     * Check if the current user is logged in.
148
     */
149 1
    final public function isLoggedIn(): bool
150
    {
151 1
        $this->assertInitialized();
152
153 1
        return $this->authz->isLoggedIn();
154
    }
155
156
    /**
157
     * Check if the current user is logged in and has specified role.
158
     *
159
     * <code>
160
     *   if (!$auth->is('manager')) {
161
     *     http_response_code(403); // Forbidden
162
     *     echo "You are not allowed to view this page";
163
     *     exit();
164
     *   }
165
     * </code>
166
     */
167 2
    final public function is(string $role): bool
168
    {
169 2
        $this->assertInitialized();
170
171 1
        return $this->authz->is($role);
172
    }
173
174
    /**
175
     * Get current authenticated user.
176
     *
177
     * @throws AuthException if no user is logged in.
178
     */
179 2
    final public function user(): User
180
    {
181 2
        $this->assertInitialized();
182
183 1
        return $this->authz->user();
184
    }
185
186
    /**
187
     * Get the current context.
188
     */
189 2
    final public function context(): ?Context
190
    {
191 2
        $this->assertInitialized();
192
193 1
        return $this->authz->context();
194
    }
195
196
197
    /**
198
     * Set the current user.
199
     *
200
     * @throws LoginException
201
     */
202 4
    public function loginAs(User $user): void
203
    {
204 4
        $this->assertInitialized();
205
206 4
        if ($this->authz->isLoggedIn()) {
207 1
            throw new \LogicException("Already logged in");
208
        }
209
210 3
        $this->loginUser($user);
211 2
    }
212
213
    /**
214
     * Login with username and password.
215
     *
216
     * @throws LoginException
217
     */
218 4
    public function login(string $username, string $password): void
219
    {
220 4
        $this->assertInitialized();
221
222 4
        if ($this->authz->isLoggedIn()) {
223 1
            throw new \LogicException("Already logged in");
224
        }
225
226 3
        $user = $this->storage->fetchUserByUsername($username);
227
228 3
        if ($user === null || !$user->verifyPassword($password)) {
229 2
            throw new LoginException('Invalid credentials', LoginException::INVALID_CREDENTIALS);
230
        }
231
232 1
        $this->loginUser($user);
233 1
    }
234
235
    /**
236
     * Set the current user and dispatch login event.
237
     *
238
     * @throws LoginException
239
     */
240 4
    private function loginUser(User $user): void
241
    {
242 4
        $event = new Event\Login($this, $user);
243 4
        $this->dispatcher->dispatch($event);
244
245 4
        if ($event->isCancelled()) {
246 1
            throw new LoginException($event->getCancellationReason(), LoginException::CANCELLED);
247
        }
248
249
        // Beware; the `authz` property may have been changed via the login event.
250 3
        $this->authz = $this->authz->forUser($user);
251
252 3
        if ($this->authz->context() === null) {
253 3
            $context = $this->storage->getContextForUser($user);
254 3
            $this->authz = $this->authz->inContextOf($context);
255
        }
256
257 3
        $this->updateSession();
258 3
    }
259
260
    /**
261
     * Logout current user.
262
     */
263 2
    public function logout(): void
264
    {
265 2
        $this->assertInitialized();
266
267 2
        if (!$this->authz()->isLoggedIn()) {
268 1
            return;
269
        }
270
271 1
        $user = $this->authz->user();
272
273 1
        $this->authz = $this->authz->forUser(null)->inContextOf(null);
274 1
        $this->updateSession();
275
276 1
        $this->dispatcher->dispatch(new Event\Logout($this, $user));
277 1
    }
278
279
    /**
280
     * Set the current context.
281
     */
282 2
    public function setContext(?Context $context): void
283
    {
284 2
        $this->assertInitialized();
285
286 2
        $this->authz = $this->authz->inContextOf($context);
287 2
        $this->updateSession();
288 2
    }
289
290
    /**
291
     * Recalculate authz roles for current user and context.
292
     * Store the current auth information in the session.
293
     *
294
     * @return $this
295
     */
296 2
    public function recalc(): self
297
    {
298 2
        $this->authz = $this->authz->recalc();
299 2
        $this->updateSession();
300
301 2
        return $this;
302
    }
303
304
    /**
305
     * Store the current auth information in the session.
306
     */
307 8
    protected function updateSession(): void
308
    {
309 8
        if (!$this->authz->isLoggedIn()) {
310 2
            $this->session->clear();
311 2
            return;
312
        }
313
314 6
        $user = $this->authz->user();
315 6
        $context = $this->authz->context();
316
317 6
        $uid = $user->getAuthId();
318 6
        $cid = $context !== null ? $context->getAuthId() : null;
319 6
        $checksum = $user->getAuthChecksum();
320
321 6
        $this->session->persist($uid, $cid, $checksum);
322 6
    }
323
324
325
    /**
326
     * Return read-only service for authorization of the current user and context.
327
     */
328 12
    public function authz(): Authz
329
    {
330 12
        return $this->authz;
331
    }
332
333
    /**
334
     * Return read-only service for authorization of the specified user.
335
     */
336 1
    public function forUser(?User $user): Authz
337
    {
338 1
        return $this->authz->forUser($user);
339
    }
340
341
    /**
342
     * Get an authz service for the given context.
343
     */
344 1
    public function inContextOf(?Context $context): Authz
345
    {
346 1
        return $this->authz->inContextOf($context);
347
    }
348
349
350
    /**
351
     * Get service to create or validate confirmation token.
352
     */
353 1
    public function confirm(string $subject): Confirmation
354
    {
355 1
        return $this->confirmation->withStorage($this->storage)->withSubject($subject);
356
    }
357
358
359
    /**
360
     * Create an event dispatcher as null object.
361
     * @codeCoverageIgnore
362
     */
363
    private static function dummyDispatcher(): EventDispatcher
364
    {
365
        return new class () implements EventDispatcher {
366
            /** @inheritDoc */
367
            public function dispatch(object $event): object
368
            {
369
                return $event;
370
            }
371
        };
372
    }
373
}
374