Passed
Push — master ( 169102...d8a6c3 )
by Ollie
10:08 queued 04:10
created

JWTGuard::getTokenSigningDetails()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 9
rs 10
1
<?php
2
3
namespace Sprocketbox\JWT;
4
5
use BadMethodCallException;
6
use Carbon\CarbonInterval;
7
use Illuminate\Contracts\Auth\Authenticatable;
8
use Illuminate\Contracts\Auth\Guard;
9
use Illuminate\Contracts\Auth\UserProvider;
10
use Illuminate\Contracts\Cookie\QueueingFactory as CookieJar;
11
use Illuminate\Support\Carbon;
12
use Lcobucci\JWT\Builder;
13
use Lcobucci\JWT\Parser;
14
use Lcobucci\JWT\Signer\Hmac\Sha256;
15
use Lcobucci\JWT\Signer\Key;
16
use Lcobucci\JWT\Token;
17
use Lcobucci\JWT\ValidationData;
18
use Ramsey\Uuid\Uuid;
19
use RuntimeException;
20
use Sprocketbox\JWT\Concerns\DefaultCompatibility;
21
22
/**
23
 * Class JwtGuard
24
 *
25
 * @package Sprocketbox\JWT
26
 */
27
class JWTGuard implements Guard
28
{
29
    use DefaultCompatibility;
30
31
    /**
32
     * Configuration options for this guard.
33
     *
34
     * @var array
35
     */
36
    private $config;
37
38
    /**
39
     * The currently authenticated token.
40
     *
41
     * @var \Lcobucci\JWT\Token|null
42
     */
43
    private $token;
44
45
    /**
46
     * @var callable|null
47
     */
48
    private $tokenGenerator;
49
50
    /**
51
     * @var callable|null
52
     */
53
    private $tokenSigning;
54
55
    /**
56
     * @var callable|null
57
     */
58
    private $tokenValidator;
59
60
    /**
61
     * @var \Illuminate\Contracts\Cookie\QueueingFactory
62
     */
63
    private $cookie;
64
65
    /**
66
     * Create a new authentication guard.
67
     *
68
     * @param \Illuminate\Contracts\Auth\UserProvider $userProvider
69
     * @param string                                  $name
70
     * @param array                                   $config
71
     */
72
    public function __construct(UserProvider $userProvider, string $name, array $config = [])
73
    {
74
        $this->setProvider($userProvider);
75
        $this->setConfig($config);
76
        $this->name = $name;
77
    }
78
79
    /**
80
     * Get the currently authenticated user.
81
     *
82
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
83
     */
84
    public function user(): ?Authenticatable
85
    {
86
        if ($this->user === null) {
87
            $token = $this->getTokenFromCookie();
88
89
            if ($token === null) {
90
                $token = $this->getTokenFromRequest();
91
            }
92
93
            if ($token !== null) {
94
                $this->user = $this->getProvider()->retrieveById($token->getClaim('sub'));
95
96
                if ($this->user !== null) {
97
                    $this->fireAuthenticatedEvent($this->user);
98
                }
99
            }
100
        }
101
102
        return $this->user;
103
    }
104
105
    /**
106
     * Validate a user's credentials.
107
     *
108
     * @param array $credentials
109
     *
110
     * @return bool
111
     */
112
    public function validate(array $credentials = []): bool
113
    {
114
        return $this->getProvider()->retrieveByCredentials($credentials) !== null;
115
    }
116
117
    /**
118
     * Set the current user.
119
     *
120
     * @param \Illuminate\Contracts\Auth\Authenticatable $user
121
     *
122
     * @return $this|void
123
     */
124
    public function setUser(Authenticatable $user)
125
    {
126
        $this->user = $user;
127
128
        $this->fireAuthenticatedEvent($user);
129
    }
130
131
    /**
132
     * Get the currently authenticated token.
133
     *
134
     * @return \Lcobucci\JWT\Token|null
135
     */
136
    public function token(): ?Token
137
    {
138
        return $this->token;
139
    }
140
141
    /**
142
     * Attempt to log a user in.
143
     *
144
     * @param array $credentials
145
     * @param bool  $cookie
146
     *
147
     * @return \Lcobucci\JWT\Token|null
148
     * @throws \Exception
149
     */
150
    public function attempt(array $credentials = [], bool $cookie = false): ?Token
151
    {
152
        $this->fireAttemptEvent($credentials, false);
153
154
        $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
155
156
        if ($this->hasValidCredentials($user, $credentials)) {
157
            return $this->login($user, $cookie);
158
        }
159
160
        $this->fireFailedEvent($user, $credentials);
161
162
        return null;
163
    }
164
165
    /**
166
     * Log a user into the application.
167
     *
168
     * @param \Illuminate\Contracts\Auth\Authenticatable $user
169
     * @param bool                                       $cookie
170
     *
171
     * @return \Lcobucci\JWT\Token
172
     * @throws \Exception
173
     */
174
    public function login(Authenticatable $user, bool $cookie = false): Token
175
    {
176
        $builder = $this->generateToken($user);
177
178
        if ($this->shouldSignToken()) {
179
            $details = $this->getTokenSigningDetails();
180
            $token   = $builder->getToken(...$details);
0 ignored issues
show
Bug introduced by
It seems like $details can also be of type Lcobucci\JWT\Signer\Key; however, parameter $signer of Lcobucci\JWT\Builder::getToken() does only seem to accept Lcobucci\JWT\Signer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

180
            $token   = $builder->getToken(/** @scrutinizer ignore-type */ ...$details);
Loading history...
181
        } else {
182
            $token = $builder->getToken();
183
        }
184
185
        $this->fireLoginEvent($user, false);
186
187
        $this->setUser($user);
188
        $this->setToken($token);
189
190
        if ($cookie) {
191
            $this->createJwtCookie($token, $token->getClaim('exp'));
192
        }
193
194
        return $token;
195
    }
196
197
    public function getCookieName(): string
198
    {
199
        return 'login_' . $this->name . '_' . sha1(static::class);
200
    }
201
202
    /**
203
     * Get the cookie creator instance used by the guard.
204
     *
205
     * @return \Illuminate\Contracts\Cookie\QueueingFactory
206
     *
207
     * @throws \RuntimeException
208
     */
209
    public function getCookieJar(): CookieJar
210
    {
211
        if (! isset($this->cookie)) {
212
            throw new RuntimeException('Cookie jar has not been set.');
213
        }
214
215
        return $this->cookie;
216
    }
217
218
    /**
219
     * Set the cookie creator instance used by the guard.
220
     *
221
     * @param \Illuminate\Contracts\Cookie\QueueingFactory $cookie
222
     *
223
     * @return \Sprocketbox\JWT\JWTGuard
224
     */
225
    public function setCookieJar(CookieJar $cookie): self
226
    {
227
        $this->cookie = $cookie;
228
229
        return $this;
230
    }
231
232
    /**
233
     * @param callable|null $tokenGenerator
234
     *
235
     * @return \Sprocketbox\JWT\JWTGuard
236
     */
237
    public function setTokenGenerator(?callable $tokenGenerator): self
238
    {
239
        $this->tokenGenerator = $tokenGenerator;
240
241
        return $this;
242
    }
243
244
    /**
245
     * @param callable|null $tokenSigning
246
     *
247
     * @return JWTGuard
248
     */
249
    public function setTokenSigner(?callable $tokenSigning): JWTGuard
250
    {
251
        $this->tokenSigning = $tokenSigning;
252
253
        return $this;
254
    }
255
256
    /**
257
     * @param callable|null $tokenValidator
258
     *
259
     * @return \Sprocketbox\JWT\JWTGuard
260
     */
261
    public function setTokenValidator(?callable $tokenValidator): self
262
    {
263
        $this->tokenValidator = $tokenValidator;
264
265
        return $this;
266
    }
267
268
    /**
269
     * Get the token from the current request.
270
     *
271
     * @return \Lcobucci\JWT\Token|null
272
     */
273
    private function getTokenFromRequest(): ?Token
274
    {
275
        return $this->parseToken($this->getRequest()->bearerToken());
276
    }
277
278
    /**
279
     * Get the token from a cookie.
280
     *
281
     * @return \Lcobucci\JWT\Token|null
282
     */
283
    private function getTokenFromCookie(): ?Token
284
    {
285
        return $this->parseToken($this->getRequest()->cookie($this->getCookieName()));
286
    }
287
288
    /**
289
     * Validate a token retrieved from a request.
290
     *
291
     * @param \Lcobucci\JWT\Token $token
292
     *
293
     * @return bool
294
     */
295
    private function validateToken(Token $token): bool
296
    {
297
        if ($this->tokenValidator !== null) {
298
            if (call_user_func($this->tokenValidator, $token, $this) === false) {
299
                return false;
300
            }
301
        } else {
302
            $validator = new ValidationData;
303
            $validator->setAudience(config('app.url'));
304
            $validator->setIssuer(config('app.url'));
305
306
            if (! $token->validate($validator)) {
307
                return false;
308
            }
309
310
            if ($token->isExpired()) {
311
                return false;
312
            }
313
        }
314
315
        if ($this->shouldSignToken()) {
316
            try {
317
                $details = $this->getTokenSigningDetails();
318
                return $token->verify($details[0], ($details[1])->getContent());
319
            } catch (BadMethodCallException $exception) {
320
                report($exception);
321
            }
322
        }
323
324
        return false;
325
    }
326
327
    /**
328
     * Set the currently authenticated token.
329
     *
330
     * @param \Lcobucci\JWT\Token $token
331
     *
332
     * @return $this
333
     */
334
    private function setToken(Token $token): self
335
    {
336
        $this->token = $token;
337
338
        return $this;
339
    }
340
341
    /**
342
     * Get whether the token should be signed or not.
343
     *
344
     * @return bool
345
     */
346
    private function shouldSignToken(): bool
347
    {
348
        return ! empty($this->config['signer']) && ! empty($this->config['key']);
349
    }
350
351
    /**
352
     * Set this guards configuration.
353
     *
354
     * @param array $config
355
     *
356
     * @return $this
357
     */
358
    private function setConfig(array $config): self
359
    {
360
        $this->config = array_merge([
361
            'signer' => Sha256::class,
362
            'key'    => null,
363
            'ttl'    => 'P1M',
364
        ], $config);
365
366
        return $this;
367
    }
368
369
    /**
370
     * Parse a JWT string
371
     *
372
     * @param string|null $jwt
373
     *
374
     * @return \Lcobucci\JWT\Token|null
375
     */
376
    private function parseToken(?string $jwt): ?Token
377
    {
378
        if (empty($jwt)) {
379
            return null;
380
        }
381
382
        $token = (new Parser)->parse($jwt);
383
384
        if (! $this->validateToken($token)) {
385
            return null;
386
        }
387
388
        return $token;
389
    }
390
391
    /**
392
     * Create the HTTP only JWT cookie
393
     *
394
     * @param \Lcobucci\JWT\Token $token
395
     * @param int                 $expiry
396
     */
397
    private function createJwtCookie(Token $token, int $expiry): void
398
    {
399
        $this->getCookieJar()->queue(
400
            $this->getCookieJar()->make($this->getCookieName(), (string) $token, ($expiry - time()) / 60)
401
        );
402
    }
403
404
    /**
405
     * @param \Illuminate\Contracts\Auth\Authenticatable $user
406
     *
407
     * @return \Lcobucci\JWT\Builder
408
     * @throws \Exception
409
     */
410
    private function generateToken(Authenticatable $user): Builder
411
    {
412
        if ($this->tokenGenerator !== null) {
413
            return call_user_func($this->tokenGenerator, $user, $this);
414
        }
415
416
        $time   = Carbon::now();
417
        $expiry = new CarbonInterval($this->config['ttl']);
418
419
        return (new Builder)
420
            ->issuedBy(config('app.url'))
421
            ->permittedFor(config('app.url'))
422
            ->identifiedBy(Uuid::uuid4()->toString())
423
            ->issuedAt($time->timestamp)
424
            ->expiresAt($time->copy()->add($expiry)->timestamp)
425
            ->relatedTo($user->getAuthIdentifier());
426
    }
427
428
    private function getTokenSigningDetails(): array
429
    {
430
        if ($this->tokenSigning !== null) {
431
            return call_user_func($this->tokenSigning, $this);
432
        }
433
434
        return [
435
            new $this->config['signer'],
436
            new Key($this->config['key']),
437
        ];
438
    }
439
}