Passed
Push — master ( 4b9eff...777e56 )
by Ollie
08:08 queued 03:47
created

JWTGuard::parseToken()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 13
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 $tokenValidator;
54
55
    /**
56
     * Create a new authentication guard.
57
     *
58
     * @param \Illuminate\Contracts\Auth\UserProvider $userProvider
59
     * @param string                                  $name
60
     * @param array                                   $config
61
     */
62
    public function __construct(UserProvider $userProvider, string $name, array $config = [])
63
    {
64
        $this->setProvider($userProvider);
65
        $this->setConfig($config);
66
        $this->name = $name;
67
    }
68
69
    /**
70
     * Get the currently authenticated user.
71
     *
72
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
73
     */
74
    public function user(): ?Authenticatable
75
    {
76
        if ($this->user === null) {
77
            $token = $this->getTokenFromCookie();
78
79
            if ($token === null) {
80
                $token = $this->getTokenFromRequest();
81
            }
82
83
            if ($token !== null) {
84
                $this->user = $this->getProvider()->retrieveById($token->getClaim('sub'));
85
86
                if ($this->user !== null) {
87
                    $this->fireAuthenticatedEvent($this->user);
88
                }
89
            }
90
        }
91
92
        return $this->user;
93
    }
94
95
    /**
96
     * Validate a user's credentials.
97
     *
98
     * @param array $credentials
99
     *
100
     * @return bool
101
     */
102
    public function validate(array $credentials = []): bool
103
    {
104
        return $this->getProvider()->retrieveByCredentials($credentials) !== null;
105
    }
106
107
    /**
108
     * Set the current user.
109
     *
110
     * @param \Illuminate\Contracts\Auth\Authenticatable $user
111
     *
112
     * @return $this|void
113
     */
114
    public function setUser(Authenticatable $user)
115
    {
116
        $this->user = $user;
117
118
        $this->fireAuthenticatedEvent($user);
119
    }
120
121
    /**
122
     * Get the currently authenticated token.
123
     *
124
     * @return \Lcobucci\JWT\Token|null
125
     */
126
    public function token(): ?Token
127
    {
128
        return $this->token;
129
    }
130
131
    /**
132
     * Attempt to log a user in.
133
     *
134
     * @param array $credentials
135
     * @param bool  $cookie
136
     *
137
     * @return \Lcobucci\JWT\Token|null
138
     * @throws \Exception
139
     */
140
    public function attempt(array $credentials = [], bool $cookie = false): ?Token
141
    {
142
        $this->fireAttemptEvent($credentials, false);
143
144
        $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
145
146
        if ($this->hasValidCredentials($user, $credentials)) {
147
            return $this->login($user, $cookie);
148
        }
149
150
        $this->fireFailedEvent($user, $credentials);
151
152
        return null;
153
    }
154
155
    /**
156
     * Log a user into the application.
157
     *
158
     * @param \Illuminate\Contracts\Auth\Authenticatable $user
159
     * @param bool                                       $cookie
160
     *
161
     * @return \Lcobucci\JWT\Token
162
     * @throws \Exception
163
     */
164
    public function login(Authenticatable $user, bool $cookie = false): Token
165
    {
166
        $builder = $this->generateToken($user);
167
168
        if ($this->shouldSignToken()) {
169
            $token = $builder->getToken(new $this->config['signer'], new Key($this->config['key']));
170
        } else {
171
            $token = $builder->getToken();
172
        }
173
174
        $this->fireLoginEvent($user, false);
175
176
        $this->setUser($user);
177
        $this->setToken($token);
178
179
        if ($cookie) {
180
            $this->createJwtCookie($token, $token->getClaim('exp'));
181
        }
182
183
        return $token;
184
    }
185
186
    public function getCookieName(): string
187
    {
188
        return 'login_' . $this->name . '_' . sha1(static::class);
189
    }
190
191
    /**
192
     * Get the cookie creator instance used by the guard.
193
     *
194
     * @return \Illuminate\Contracts\Cookie\QueueingFactory
195
     *
196
     * @throws \RuntimeException
197
     */
198
    public function getCookieJar(): CookieJar
199
    {
200
        if (! isset($this->cookie)) {
201
            throw new RuntimeException('Cookie jar has not been set.');
202
        }
203
204
        return $this->cookie;
205
    }
206
207
    /**
208
     * Set the cookie creator instance used by the guard.
209
     *
210
     * @param \Illuminate\Contracts\Cookie\QueueingFactory $cookie
211
     *
212
     * @return \Sprocketbox\JWT\JWTGuard
213
     */
214
    public function setCookieJar(CookieJar $cookie): self
215
    {
216
        $this->cookie = $cookie;
0 ignored issues
show
Bug Best Practice introduced by
The property cookie does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
217
218
        return $this;
219
    }
220
221
    /**
222
     * @param callable|null $tokenGenerator
223
     *
224
     * @return \Sprocketbox\JWT\JWTGuard
225
     */
226
    public function setTokenGenerator(?callable $tokenGenerator): self
227
    {
228
        $this->tokenGenerator = $tokenGenerator;
229
230
        return $this;
231
    }
232
233
    /**
234
     * @param callable|null $tokenValidator
235
     *
236
     * @return \Sprocketbox\JWT\JWTGuard
237
     */
238
    public function setTokenValidator(?callable $tokenValidator): self
239
    {
240
        $this->tokenValidator = $tokenValidator;
241
242
        return $this;
243
    }
244
245
    /**
246
     * Get the token from the current request.
247
     *
248
     * @return \Lcobucci\JWT\Token|null
249
     */
250
    private function getTokenFromRequest(): ?Token
251
    {
252
        return $this->parseToken($this->getRequest()->bearerToken());
253
    }
254
255
    /**
256
     * Get the token from a cookie.
257
     *
258
     * @return \Lcobucci\JWT\Token|null
259
     */
260
    private function getTokenFromCookie(): ?Token
261
    {
262
        return $this->parseToken($this->getRequest()->cookie($this->getCookieName()));
1 ignored issue
show
Bug introduced by
It seems like $this->getRequest()->coo...$this->getCookieName()) can also be of type array; however, parameter $jwt of Sprocketbox\JWT\JWTGuard::parseToken() does only seem to accept null|string, 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

262
        return $this->parseToken(/** @scrutinizer ignore-type */ $this->getRequest()->cookie($this->getCookieName()));
Loading history...
263
    }
264
265
    /**
266
     * Validate a token retrieved from a request.
267
     *
268
     * @param \Lcobucci\JWT\Token $token
269
     *
270
     * @return bool
271
     */
272
    private function validateToken(Token $token): bool
273
    {
274
        if ($this->tokenValidator !== null) {
275
            if (call_user_func($this->tokenValidator, $token, $this) === false) {
276
                return false;
277
            }
278
        } else {
279
            $validator = new ValidationData;
280
            $validator->setAudience(config('app.url'));
281
            $validator->setIssuer(config('app.url'));
282
283
            if (! $token->validate($validator)) {
284
                return false;
285
            }
286
287
            if ($token->isExpired()) {
288
                return false;
289
            }
290
        }
291
292
        if ($this->shouldSignToken()) {
293
            try {
294
                return $token->verify(new Sha256(), (new Key($this->config['key']))->getContent());
295
            } catch (BadMethodCallException $exception) {
296
                report($exception);
297
            }
298
        }
299
300
        return false;
301
    }
302
303
    /**
304
     * Set the currently authenticated token.
305
     *
306
     * @param \Lcobucci\JWT\Token $token
307
     *
308
     * @return $this
309
     */
310
    private function setToken(Token $token): self
311
    {
312
        $this->token = $token;
313
314
        return $this;
315
    }
316
317
    /**
318
     * Get whether the token should be signed or not.
319
     *
320
     * @return bool
321
     */
322
    private function shouldSignToken(): bool
323
    {
324
        return ! empty($this->config['signer']) && ! empty($this->config['key']);
325
    }
326
327
    /**
328
     * Set this guards configuration.
329
     *
330
     * @param array $config
331
     *
332
     * @return $this
333
     */
334
    private function setConfig(array $config): self
335
    {
336
        $this->config = array_merge([
337
            'signer' => Sha256::class,
338
            'key'    => null,
339
            'ttl'    => 'P1M',
340
        ], $config);
341
342
        return $this;
343
    }
344
345
    /**
346
     * Parse a JWT string
347
     *
348
     * @param string|null $jwt
349
     *
350
     * @return \Lcobucci\JWT\Token|null
351
     */
352
    private function parseToken(?string $jwt): ?Token
353
    {
354
        if (empty($jwt)) {
355
            return null;
356
        }
357
358
        $token = (new Parser)->parse($jwt);
359
360
        if (! $this->validateToken($token)) {
361
            return null;
362
        }
363
364
        return $token;
365
    }
366
367
    /**
368
     * Create the HTTP only JWT cookie
369
     *
370
     * @param \Lcobucci\JWT\Token $token
371
     * @param int                 $expiry
372
     */
373
    private function createJwtCookie(Token $token, int $expiry): void
374
    {
375
        $this->getCookieJar()->queue(
376
            $this->getCookieJar()->make($this->getCookieName(), (string) $token, ($expiry - time()) / 60)
377
        );
378
    }
379
380
    private function generateToken(Authenticatable $user): Builder
381
    {
382
        if ($this->tokenGenerator !== null) {
383
            return call_user_func($this->tokenGenerator, $user, $this);
384
        }
385
386
        $time   = Carbon::now();
387
        $expiry = CarbonInterval::fromString($this->config['ttl']);
388
389
        return (new Builder)
390
            ->issuedBy(config('app.url'))
391
            ->permittedFor(config('app.url'))
392
            ->identifiedBy(Uuid::uuid4())
0 ignored issues
show
Bug introduced by
Ramsey\Uuid\Uuid::uuid4() of type Ramsey\Uuid\UuidInterface is incompatible with the type string expected by parameter $id of Lcobucci\JWT\Builder::identifiedBy(). ( Ignorable by Annotation )

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

392
            ->identifiedBy(/** @scrutinizer ignore-type */ Uuid::uuid4())
Loading history...
393
            ->issuedAt($time->timestamp)
394
            ->expiresAt($time->copy()->add($expiry)->timestamp)
395
            ->relatedTo($user->getAuthIdentifier());
396
    }
397
}