Completed
Branch develop (8ea255)
by Nate
05:15
created

SelfConsumable::parse()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 0
cts 17
cp 0
rs 8.2222
c 0
b 0
f 0
cc 7
eloc 13
nc 4
nop 3
crap 56
1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 * @license    https://flipboxfactory.com/software/jwt/license
6
 * @link       https://www.flipboxfactory.com/jwt/organization/
7
 */
8
9
namespace flipbox\craft\jwt\services;
10
11
use Craft;
12
use craft\elements\User;
13
use flipbox\craft\jwt\Jwt;
14
use Lcobucci\JWT\Token;
15
use yii\base\Component;
16
use yii\web\IdentityInterface;
17
18
/**
19
 * @author Flipbox Factory <[email protected]>
20
 * @since 1.0.0
21
 */
22
class SelfConsumable extends Component
23
{
24
    /**
25
     * The CSRF claim identifier
26
     */
27
    const CLAIM_CSRF = 'csrf';
28
29
    /**
30
     * The Audience claim identifier
31
     */
32
    const CLAIM_AUDIENCE = 'aud';
33
34
    /**
35
     * The Issuer claim identifier
36
     */
37
    const CLAIM_ISSUER = 'iss';
38
39
    /**
40
     * The Identity claim identifier
41
     */
42
    const CLAIM_IDENTITY = 'jti';
43
44
    /**
45
     * Issue an authorization JWT token on behalf of a user.
46
     *
47
     * @param string $user
48
     * @param string|null $audience
49
     * @param int|null $expiration
50
     * @return Token|null
51
     * @throws \craft\errors\SiteNotFoundException
52
     * @throws \yii\base\InvalidConfigException
53
     */
54
    public function issue(
55
        $user = 'CURRENT_USER',
56
        string $audience = null,
57
        int $expiration = null
58
    ) {
59
        if (null === ($identity = $this->resolveUser($user))) {
60
            return null;
61
        }
62
63
        return Jwt::getInstance()->getBuilder()
64
            ->setIssuer(Jwt::getInstance()->getSettings()->getIssuer())
65
            ->setAudience($this->resolveAudience($audience))
66
            ->setId($identity->getId(), true)
67
            ->setIssuedAt(time())
68
            ->setNotBefore(time())
69
            ->setExpiration($this->resolveTokenExpiration($expiration))
70
            ->set(self::CLAIM_CSRF, Craft::$app->getRequest()->getCsrfToken())
71
            ->sign(Jwt::getInstance()->getSettings()->getSigner(), $this->getSignatureKey($identity))
72
            ->getToken();
73
    }
74
75
    /**
76
     * This
77
     * @param string $token
78
     * @return null|IdentityInterface
79
     * @throws \craft\errors\SiteNotFoundException
80
     */
81
    public function claim(string $token)
82
    {
83
        if (null === ($token = $this->parse($token))) {
84
            return null;
85
        }
86
87
        return $this->tokenIdentity($token);
88
    }
89
90
    /**
91
     * @param $token
92
     * @param bool $validate
93
     * @param bool $verify
94
     * @return Token|null
95
     * @throws \craft\errors\SiteNotFoundException
96
     */
97
    public function parse(string $token, bool $validate = true, bool $verify = true)
98
    {
99
        try {
100
            $token = Jwt::getInstance()->getParser()->parse((string)$token);
101
        } catch (\RuntimeException $e) {
102
            Jwt::warning("Invalid JWT provided: " . $e->getMessage());
103
            return null;
104
        } catch (\InvalidArgumentException $e) {
105
            Jwt::warning("Invalid JWT provided: " . $e->getMessage());
106
            return null;
107
        }
108
109
        if (($validate && !Jwt::getInstance()->validateToken($token)) ||
110
            ($verify && !$this->verifyToken($token))) {
111
            return null;
112
        }
113
114
        return $token;
115
    }
116
117
    /**
118
     * @param Token $token
119
     * @return bool
120
     * @throws \craft\errors\SiteNotFoundException
121
     */
122
    public function verifyToken(Token $token): bool
123
    {
124
        if (null === ($identity = $this->resolveUser($token->getClaim(self::CLAIM_IDENTITY)))) {
125
            return false;
126
        }
127
128
        return $this->verifyTokenCsrfClaim($token) &&
129
            $this->verifyIssuer($token) &&
130
            $this->verifyAudience($token) &&
131
            $this->verifyTokenSignature($token, $identity);
132
    }
133
134
    /**
135
     * @param Token $token
136
     * @return null|IdentityInterface
137
     */
138
    private function tokenIdentity(Token $token)
139
    {
140
        return $this->resolveUser($token->getClaim(self::CLAIM_IDENTITY));
141
    }
142
143
    /**
144
     * @param Token $token
145
     * @return bool
146
     * @throws \craft\errors\SiteNotFoundException
147
     */
148
    private function verifyAudience(Token $token): bool
149
    {
150
        $audience = $token->getClaim(self::CLAIM_AUDIENCE);
151
        if (false === ($audience ===
152
            Jwt::getInstance()->getSettings()->getSelfConsumableAudience())
153
        ) {
154
            Jwt::error(sprintf(
155
                "Unable to verify audience: %s",
156
                $audience
157
            ));
158
159
            return false;
160
        }
161
        return true;
162
    }
163
164
    /**
165
     * Verify that the issuer is one we can accept from
166
     *
167
     * @param Token $token
168
     * @return bool
169
     * @throws \craft\errors\SiteNotFoundException
170
     */
171
    private function verifyIssuer(Token $token): bool
172
    {
173
        $issuer = $token->getClaim(self::CLAIM_ISSUER);
174
        if (false === in_array(
175
            $issuer,
176
            Jwt::getInstance()->getSettings()->getSelfConsumableIssuers()
177
        )) {
178
            Jwt::error(sprintf(
179
                "Unable to verify issuer: %s",
180
                $issuer
181
            ));
182
183
            return false;
184
        }
185
        return true;
186
    }
187
188
    /**
189
     * @param Token $token
190
     * @return bool
191
     */
192
    private function verifyTokenCsrfClaim(Token $token): bool
193
    {
194
        $csrf = $token->getClaim(self::CLAIM_CSRF);
195
        if (false === Craft::$app->getRequest()->validateCsrfToken($csrf)) {
196
            Jwt::error(sprintf(
197
                "Unable to verify CSRF Token: %s",
198
                $csrf
199
            ));
200
201
            return false;
202
        }
203
        return true;
204
    }
205
206
    /**
207
     * @param Token $token
208
     * @param IdentityInterface $identity
209
     * @return bool
210
     */
211
    private function verifyTokenSignature(Token $token, IdentityInterface $identity): bool
212
    {
213
        try {
214
            if (false === $token->verify(
215
                Jwt::getInstance()->getSettings()->resolveSigner($token->getHeader('alg')),
216
                $this->getSignatureKey($identity)
217
            )) {
218
                Jwt::error("Unable to verify token signature");
219
                return false;
220
            }
221
            return true;
222
        } catch (\Exception $e) {
223
            Jwt::error(sprintf(
224
                "Exception caught while trying to verify token signature: %s",
225
                $e->getMessage()
226
            ));
227
        }
228
        return false;
229
    }
230
231
    /**
232
     * @param IdentityInterface $identity
233
     * @return string
234
     */
235
    private function getSignatureKey(IdentityInterface $identity)
236
    {
237
        $id = $identity instanceof User ? $identity->uid : $identity->getId();
238
        return Jwt::getInstance()->getSettings()->getKey() . '.' . $id;
239
    }
240
241
    /**
242
     * @param string|null $audience
243
     * @return string
244
     * @throws \craft\errors\SiteNotFoundException
245
     */
246
    private function resolveAudience(string $audience = null): string
247
    {
248
        if ($audience === null) {
249
            $audience = Jwt::getInstance()->getSettings()->getSelfConsumableAudience();
250
        }
251
252
        return (string)$audience;
253
    }
254
255
    /**
256
     * @param int|null $expiration
257
     * @return int
258
     */
259
    private function resolveTokenExpiration(int $expiration = null): int
260
    {
261
        if ($expiration === null) {
262
            $expiration = Jwt::getInstance()->getSettings()->tokenExpiration;
263
        }
264
265
        return time() + (int)$expiration;
266
    }
267
268
    /**
269
     * @param $user
270
     * @return IdentityInterface|null
271
     */
272
    private function resolveUser($user)
273
    {
274
        if ($user instanceof IdentityInterface) {
275
            return $user;
276
        }
277
278
        if ($user === 'CURRENT_USER') {
279
            return Craft::$app->getUser()->getIdentity();
280
        }
281
282
        if (is_numeric($user)) {
283
            return Craft::$app->getUsers()->getUserById($user);
284
        }
285
286
        if (is_string($user)) {
287
            return Craft::$app->getUsers()->getUserByUsernameOrEmail($user);
288
        }
289
290
        return null;
291
    }
292
}
293