Completed
Push — master ( c47c6a...b116a1 )
by Arnold
03:00
created

Auth::getLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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