Failed Conditions
Push — master ( 2436f4...03b798 )
by Arnold
02:53
created

Auth::initialize()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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