Passed
Push — master ( af16b4...bf34ca )
by Arnold
03:18
created

Auth::isLoggedIn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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