Passed
Push — develop ( 009926...78c094 )
by Ollie
05:20 queued 11s
created

JWTGuard::validate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
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
     * Create a new authentication guard.
47
     *
48
     * @param \Illuminate\Contracts\Auth\UserProvider $userProvider
49
     * @param string                                  $name
50
     * @param array                                   $config
51
     */
52
    public function __construct(UserProvider $userProvider, string $name, array $config = [])
53
    {
54
        $this->setProvider($userProvider);
55
        $this->setConfig($config);
56
        $this->name = $name;
57
    }
58
59
    /**
60
     * Get the currently authenticated user.
61
     *
62
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
63
     */
64
    public function user(): ?Authenticatable
65
    {
66
        if ($this->user === null) {
67
            $token = $this->getTokenFromCookie();
68
69
            if ($token === null) {
70
                $token = $this->getTokenFromRequest();
71
            }
72
73
            if ($token !== null) {
74
                $this->user = $this->getProvider()->retrieveById($token->getClaim('sub'));
75
76
                if ($this->user !== null) {
77
                    $this->fireAuthenticatedEvent($this->user);
78
                }
79
            }
80
        }
81
82
        return $this->user;
83
    }
84
85
    /**
86
     * Validate a user's credentials.
87
     *
88
     * @param array $credentials
89
     *
90
     * @return bool
91
     */
92
    public function validate(array $credentials = []): bool
93
    {
94
        return $this->getProvider()->retrieveByCredentials($credentials) !== null;
95
    }
96
97
    /**
98
     * Set the current user.
99
     *
100
     * @param \Illuminate\Contracts\Auth\Authenticatable $user
101
     *
102
     * @return $this|void
103
     */
104
    public function setUser(Authenticatable $user)
105
    {
106
        $this->user = $user;
107
108
        $this->fireAuthenticatedEvent($user);
109
    }
110
111
    /**
112
     * Get the currently authenticated token.
113
     *
114
     * @return \Lcobucci\JWT\Token|null
115
     */
116
    public function token(): ?Token
117
    {
118
        return $this->token;
119
    }
120
121
    /**
122
     * Attempt to log a user in.
123
     *
124
     * @param array $credentials
125
     * @param bool  $cookie
126
     *
127
     * @return \Lcobucci\JWT\Token|null
128
     * @throws \Exception
129
     */
130
    public function attempt(array $credentials = [], bool $cookie = false): ?Token
131
    {
132
        $this->fireAttemptEvent($credentials, false);
133
134
        $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
135
136
        if ($this->hasValidCredentials($user, $credentials)) {
137
            return $this->login($user, $cookie);
138
        }
139
140
        $this->fireFailedEvent($user, $credentials);
141
142
        return null;
143
    }
144
145
    /**
146
     * Log a user into the application.
147
     *
148
     * @param \Illuminate\Contracts\Auth\Authenticatable $user
149
     * @param bool                                       $cookie
150
     *
151
     * @return \Lcobucci\JWT\Token
152
     * @throws \Exception
153
     */
154
    public function login(Authenticatable $user, bool $cookie = false): Token
155
    {
156
        $time    = Carbon::now();
157
        $expiry  = CarbonInterval::fromString($this->config['ttl']);
158
        $builder = (new Builder)
159
            ->issuedBy(config('app.url'))
160
            ->permittedFor(config('app.url'))
161
            ->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

161
            ->identifiedBy(/** @scrutinizer ignore-type */ Uuid::uuid4())
Loading history...
162
            ->issuedAt($time->timestamp)
163
            ->expiresAt($time->copy()->add($expiry)->timestamp)
164
            ->relatedTo($user->getAuthIdentifier());
165
166
        if ($this->shouldSignToken()) {
167
            $token = $builder->getToken(new $this->config['signer'], new Key($this->config['key']));
168
        } else {
169
            $token = $builder->getToken();
170
        }
171
172
        $this->fireLoginEvent($user, false);
173
174
        $this->setUser($user);
175
        $this->setToken($token);
176
177
        if ($cookie) {
178
            $this->createJwtCookie($token, $expiry);
179
        }
180
181
        return $token;
182
    }
183
184
    public function getCookieName(): string
185
    {
186
        return 'login_' . $this->name . '_' . sha1(static::class);
187
    }
188
189
    /**
190
     * Get the cookie creator instance used by the guard.
191
     *
192
     * @return \Illuminate\Contracts\Cookie\QueueingFactory
193
     *
194
     * @throws \RuntimeException
195
     */
196
    public function getCookieJar(): CookieJar
197
    {
198
        if (! isset($this->cookie)) {
199
            throw new RuntimeException('Cookie jar has not been set.');
200
        }
201
202
        return $this->cookie;
203
    }
204
205
    /**
206
     * Set the cookie creator instance used by the guard.
207
     *
208
     * @param \Illuminate\Contracts\Cookie\QueueingFactory $cookie
209
     *
210
     * @return \Sprocketbox\JWT\JWTGuard
211
     */
212
    public function setCookieJar(CookieJar $cookie): self
213
    {
214
        $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...
215
216
        return $this;
217
    }
218
219
    /**
220
     * Get the token from the current request.
221
     *
222
     * @return \Lcobucci\JWT\Token|null
223
     */
224
    private function getTokenFromRequest(): ?Token
225
    {
226
        return $this->parseToken($this->getRequest()->bearerToken());
227
    }
228
229
    /**
230
     * Get the token from a cookie.
231
     *
232
     * @return \Lcobucci\JWT\Token|null
233
     */
234
    private function getTokenFromCookie(): ?Token
235
    {
236
        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

236
        return $this->parseToken(/** @scrutinizer ignore-type */ $this->getRequest()->cookie($this->getCookieName()));
Loading history...
237
    }
238
239
    /**
240
     * Validate a token retrieved from a request.
241
     *
242
     * @param \Lcobucci\JWT\Token $token
243
     *
244
     * @return bool
245
     */
246
    private function validateToken(Token $token): bool
247
    {
248
        $validator = new ValidationData;
249
        $validator->setAudience(config('app.url'));
250
        $validator->setIssuer(config('app.url'));
251
252
        if (! $token->validate($validator)) {
253
            return false;
254
        }
255
256
        if ($this->shouldSignToken()) {
257
            try {
258
                return $token->verify(new Sha256(), (new Key($this->config['key']))->getContent());
259
            } catch (BadMethodCallException $exception) {
260
                report($exception);
261
            }
262
        }
263
264
        return false;
265
    }
266
267
    /**
268
     * Set the currently authenticated token.
269
     *
270
     * @param \Lcobucci\JWT\Token $token
271
     *
272
     * @return $this
273
     */
274
    private function setToken(Token $token): self
275
    {
276
        $this->token = $token;
277
278
        return $this;
279
    }
280
281
    /**
282
     * Get whether the token should be signed or not.
283
     *
284
     * @return bool
285
     */
286
    private function shouldSignToken(): bool
287
    {
288
        return ! empty($this->config['signer']) && ! empty($this->config['key']);
289
    }
290
291
    /**
292
     * Set this guards configuration.
293
     *
294
     * @param array $config
295
     *
296
     * @return $this
297
     */
298
    private function setConfig(array $config): self
299
    {
300
        $this->config = array_merge([
301
            'signer' => Sha256::class,
302
            'key'    => null,
303
            'ttl'    => 'P1M',
304
        ], $config);
305
306
        return $this;
307
    }
308
309
    /**
310
     * Parse a JWT string
311
     *
312
     * @param string|null $jwt
313
     *
314
     * @return \Lcobucci\JWT\Token|null
315
     */
316
    private function parseToken(?string $jwt): ?Token
317
    {
318
        if (empty($jwt)) {
319
            return null;
320
        }
321
322
        $token = (new Parser)->parse($jwt);
323
324
        if (! $this->validateToken($token)) {
325
            return null;
326
        }
327
328
        return $token;
329
    }
330
331
    /**
332
     * Create the HTTP only JWT cookie
333
     *
334
     * @param \Lcobucci\JWT\Token    $token
335
     * @param \Carbon\CarbonInterval $expiry
336
     */
337
    private function createJwtCookie(Token $token, CarbonInterval $expiry): void
338
    {
339
        $this->getCookieJar()->queue(
340
            $this->getCookieJar()->make($this->getCookieName(), (string) $token, $expiry->minutes)
341
        );
342
    }
343
}