Completed
Pull Request — master (#11)
by Arnold
03:10
created

Auth.php$0 ➔ dummyDispatcher()   A

Complexity

Conditions 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
ccs 0
cts 0
cp 0
rs 10
cc 1
crap 2

1 Method

Rating   Name   Duplication   Size   Complexity  
A Auth.php$0 ➔ dispatch() 0 3 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 28
    public function __construct(Authz $authz, Storage $storage, ?Confirmation $confirmation = null)
43
    {
44 28
        $this->authz = $authz;
45 28
        $this->storage = $storage;
46 28
        $this->confirmation = $confirmation ?? new NoConfirmation();
47
48
        // Set default services
49 28
        $this->session = new PhpSession();
50 28
        $this->dispatcher = self::dummyDispatcher();
51 28
    }
52
53
    /**
54
     * Get a copy with a different session manager.
55
     */
56 28
    public function withSession(Session $session): self
57
    {
58 28
        return $this->withProperty('session', $session);
59
    }
60
61
    /**
62
     * Get a copy with an event dispatcher.
63
     */
64 28
    public function withEventDispatcher(EventDispatcher $dispatcher): self
65
    {
66 28
        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 17
    protected function assertInitialized(): void
107
    {
108 17
        if (!$this->initialized) {
109 3
            throw new \LogicException("Auth needs to be initialized before use");
110
        }
111 14
    }
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
     * Check if the current user is logged in and has specified role.
126
     *
127
     * <code>
128
     *   if (!$auth->is('manager')) {
129
     *     http_response_code(403); // Forbidden
130
     *     echo "You are not allowed to view this page";
131
     *     exit();
132
     *   }
133
     * </code>
134
     */
135 2
    final public function is(string $role): bool
136
    {
137 2
        $this->assertInitialized();
138
139 1
        return $this->authz->is($role);
140
    }
141
142
    /**
143
     * Get current authenticated user.
144
     *
145
     * @return User|null
146
     */
147 2
    final public function user(): ?User
148
    {
149 2
        $this->assertInitialized();
150
151 1
        return $this->authz->user();
152
    }
153
154
    /**
155
     * Get the current context.
156
     */
157 2
    final public function context(): ?Context
158
    {
159 2
        $this->assertInitialized();
160
161 1
        return $this->authz->context();
162
    }
163
164
165
    /**
166
     * Set the current user.
167
     *
168
     * @throws LoginException
169
     */
170 3
    public function loginAs(User $user): void
171
    {
172 3
        $this->assertInitialized();
173
174 3
        if ($this->authz->user() !== null) {
175 1
            throw new \LogicException("Already logged in");
176
        }
177
178 2
        $this->loginUser($user);
179 1
    }
180
181
    /**
182
     * Login with username and password.
183
     *
184
     * @throws LoginException
185
     */
186 4
    public function login(string $username, string $password): void
187
    {
188 4
        $this->assertInitialized();
189
190 4
        if ($this->authz->user() !== null) {
191 1
            throw new \LogicException("Already logged in");
192
        }
193
194 3
        $user = $this->storage->fetchUserByUsername($username);
195
196 3
        if ($user === null || !$user->verifyPassword($password)) {
197 2
            throw new LoginException('Invalid credentials', LoginException::INVALID_CREDENTIALS);
198
        }
199
200 1
        $this->loginUser($user);
201 1
    }
202
203
    /**
204
     * Set the current user and dispatch login event.
205
     *
206
     * @throws LoginException
207
     */
208 3
    private function loginUser(User $user): void
209
    {
210 3
        $event = new Event\Login($this, $user);
211 3
        $this->dispatcher->dispatch($event);
212
213 3
        if ($event->isCancelled()) {
214 1
            throw new LoginException($event->getCancellationReason(), LoginException::CANCELLED);
215
        }
216
217 2
        $this->authz = $this->authz->forUser($user);
218
219 2
        $this->updateSession();
220 2
    }
221
222
    /**
223
     * Logout current user.
224
     */
225 2
    public function logout(): void
226
    {
227 2
        $this->assertInitialized();
228
229 2
        $user = $this->authz->user();
230
231 2
        if ($user === null) {
232 1
            return; // already logged out
233
        }
234
235 1
        $this->authz = $this->authz->forUser(null)->inContextOf(null);
236 1
        $this->updateSession();
237
238 1
        $this->dispatcher->dispatch(new Event\Logout($this, $user));
239 1
    }
240
241
    /**
242
     * Set the current context.
243
     */
244 2
    public function setContext(?Context $context): void
245
    {
246 2
        $this->assertInitialized();
247
248 2
        $this->authz = $this->authz->inContextOf($context);
249 2
        $this->updateSession();
250 2
    }
251
252
    /**
253
     * Recalculate authz roles for current user and context.
254
     * Store the current auth information in the session.
255
     *
256
     * @return $this
257
     */
258 2
    public function recalc(): self
259
    {
260 2
        $this->authz = $this->authz->recalc();
261 2
        $this->updateSession();
262
263 2
        return $this;
264
    }
265
266
    /**
267
     * Store the current auth information in the session.
268
     */
269 7
    protected function updateSession(): void
270
    {
271 7
        $user = $this->authz->user();
272 7
        $context = $this->authz->context();
273
274 7
        if ($user === null) {
275 2
            $this->session->clear();
276 2
            return;
277
        }
278
279 5
        $uid = $user->getAuthId();
280 5
        $cid = $context !== null ? $context->getAuthId() : null;
281 5
        $checksum = $user->getAuthChecksum();
282
283 5
        $this->session->persist($uid, $cid, $checksum);
284 5
    }
285
286
287
    /**
288
     * Return read-only service for authorization of the current user and context.
289
     */
290 9
    public function authz(): Authz
291
    {
292 9
        return $this->authz;
293
    }
294
295
    /**
296
     * Return read-only service for authorization of the specified user.
297
     */
298 1
    public function forUser(?User $user): Authz
299
    {
300 1
        return $this->authz->forUser($user);
301
    }
302
303
    /**
304
     * Get an authz service for the given context.
305
     */
306 1
    public function inContextOf(?Context $context): Authz
307
    {
308 1
        return $this->authz->inContextOf($context);
309
    }
310
311
312
    /**
313
     * Get service to create or validate confirmation token.
314
     */
315 1
    public function confirm(string $subject): Confirmation
316
    {
317 1
        return $this->confirmation->withStorage($this->storage)->withSubject($subject);
318
    }
319
320
321
    /**
322
     * Create an event dispatcher as null object.
323
     * @codeCoverageIgnore
324
     */
325
    private static function dummyDispatcher(): EventDispatcher
326
    {
327
        return new class () implements EventDispatcher {
328
            /** @inheritDoc */
329
            public function dispatch(object $event): object
330
            {
331
                return $event;
332
            }
333
        };
334
    }
335
}
336