Passed
Push — master ( 291050...8bc8e6 )
by Simon
01:52
created

JWTAuthenticator::validateToken()   D

Complexity

Conditions 9
Paths 16

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 18
nc 16
nop 3
dl 0
loc 34
rs 4.909
c 0
b 0
f 0
1
<?php
2
3
namespace Firesphere\GraphQLJWT\Authentication;
4
5
use BadMethodCallException;
6
use JWTException;
7
use Lcobucci\JWT\Builder;
8
use Lcobucci\JWT\Parser;
9
use Lcobucci\JWT\Signer\Hmac\Sha256;
10
use Lcobucci\JWT\Signer\Rsa\Sha256 as RsaSha256;
11
use Lcobucci\JWT\Signer\Key;
12
use Lcobucci\JWT\Token;
13
use Lcobucci\JWT\ValidationData;
14
use OutOfBoundsException;
15
use SilverStripe\Control\Director;
16
use SilverStripe\Control\HTTPRequest;
17
use SilverStripe\Core\Config\Configurable;
18
use SilverStripe\Core\Environment;
19
use SilverStripe\GraphQL\Controller;
20
use SilverStripe\ORM\ValidationException;
21
use SilverStripe\ORM\ValidationResult;
22
use SilverStripe\Security\Authenticator;
23
use SilverStripe\Security\Member;
24
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
25
26
class JWTAuthenticator extends MemberAuthenticator
27
{
28
    use Configurable;
29
30
    /**
31
     * @var Sha256|RsaSha256
32
     */
33
    private $signer;
34
35
    /**
36
     * @var string|Key;
37
     */
38
    private $privateKey;
39
40
    /**
41
     * @var string|Key;
42
     */
43
    private $publicKey;
44
45
    /**
46
     * JWTAuthenticator constructor.
47
     * @throws JWTException
48
     */
49
    public function __construct()
50
    {
51
        $key = Environment::getEnv('JWT_SIGNER_KEY');
52
        if (empty($key)) {
53
            throw new JWTException('No key defined!', 1);
54
        }
55
        $publicKeyLocation = Environment::getEnv('JWT_PUBLIC_KEY');
56
        if (file_exists($key) && !file_exists($publicKeyLocation)) {
57
            throw new JWTException('No public key found!', 1);
58
        }
59
    }
60
61
    /**
62
     * Setup the keys this has to be done on the spot for if the signer changes between validation cycles
63
     */
64
    private function setKeys()
65
    {
66
        $signerKey = Environment::getEnv('JWT_SIGNER_KEY');
67
        // If it's a private key, we also need a public key for validation!
68
        if (file_exists($signerKey)) {
69
            $this->signer = new RsaSha256();
70
            $password = Environment::getEnv('JWT_KEY_PASSWORD');
71
            $this->privateKey = new Key('file://' . $signerKey, $password ?: null);
72
            // We're having an RSA signed key instead of a string
73
            $this->publicKey = new Key('file://' . Environment::getEnv('JWT_PUBLIC_KEY'));
74
        } else {
75
            $this->signer = new Sha256();
76
            $this->privateKey = $signerKey;
77
            $this->publicKey = $signerKey;
78
        }
79
    }
80
81
    /**
82
     * JWT is stateless, therefore, we don't support anything but login
83
     *
84
     * @return int
85
     */
86
    public function supportedServices()
87
    {
88
        return Authenticator::LOGIN | Authenticator::CMS_LOGIN;
89
    }
90
91
    /**
92
     * @param array $data
93
     * @param HTTPRequest $request
94
     * @param ValidationResult|null $result
95
     * @return Member|null
96
     * @throws OutOfBoundsException
97
     * @throws BadMethodCallException
98
     */
99
    public function authenticate(array $data, HTTPRequest $request, ValidationResult &$result = null)
100
    {
101
        if (!$result) {
102
            $result = new ValidationResult();
103
        }
104
        $token = $data['token'];
105
106
        return $this->validateToken($token, $request, $result);
107
    }
108
109
    /**
110
     * @param Member $member
111
     * @return Token
112
     * @throws ValidationException
113
     * @throws BadMethodCallException
114
     */
115
    public function generateToken(Member $member)
116
    {
117
        $this->setKeys();
118
        $config = static::config();
119
        $uniqueID = uniqid(Environment::getEnv('JWT_PREFIX'), true);
120
121
        $request = Controller::curr()->getRequest();
122
        $audience = $request->getHeader('Origin');
123
124
        $builder = new Builder();
125
        $token = $builder
126
            // Configures the issuer (iss claim)
127
            ->setIssuer(Director::absoluteBaseURL())
128
            // Configures the audience (aud claim)
129
            ->setAudience($audience)
130
            // Configures the id (jti claim), replicating as a header item
131
            ->setId($uniqueID, true)
132
            // Configures the time that the token was issue (iat claim)
133
            ->setIssuedAt(time())
134
            // Configures the time that the token can be used (nbf claim)
135
            ->setNotBefore(time() + $config->get('nbf_time'))
136
            // Configures the expiration time of the token (nbf claim)
137
            ->setExpiration(time() + $config->get('nbf_expiration'))
138
            // Configures a new claim, called "uid"
139
            ->set('uid', $member->ID)
140
            // Sign the key with the Signer's key
141
            ->sign($this->signer, $this->privateKey);
142
143
        // Save the member if it's not anonymous
144
        if ($member->ID > 0) {
145
            $member->JWTUniqueID = $uniqueID;
146
            $member->write();
147
        }
148
149
        // Return the token
150
        return $token->getToken();
151
    }
152
153
    /**
154
     * @param string $token
155
     * @param HTTPRequest $request
156
     * @param ValidationResult $result
157
     * @return null|Member
158
     * @throws BadMethodCallException
159
     */
160
    private function validateToken($token, $request, &$result)
161
    {
162
        $this->setKeys();
163
        $parser = new Parser();
164
        $parsedToken = $parser->parse((string)$token);
165
166
        // Get a validator and the Member for this token
167
        list($validator, $member) = $this->getValidator($request, $parsedToken);
168
169
        $verified = $parsedToken->verify($this->signer, $this->publicKey);
0 ignored issues
show
Bug introduced by
It seems like $this->publicKey can also be of type Lcobucci\JWT\Signer\Key; however, parameter $key of Lcobucci\JWT\Token::verify() does only seem to accept 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

169
        $verified = $parsedToken->verify($this->signer, /** @scrutinizer ignore-type */ $this->publicKey);
Loading history...
170
        $valid = $parsedToken->validate($validator);
171
172
        // If the token is not verified, just give up
173
        if (!$verified || !$valid) {
174
            $result->addError('Invalid token');
175
        }
176
        // An expired token can be renewed
177
        if (
178
            $verified &&
179
            $parsedToken->isExpired()
180
        ) {
181
            $result->addError('Token is expired, please renew your token with a refreshToken query');
182
        }
183
        // Not entirely fine, do we allow anonymous users?
184
        // Then, if the token is valid, return an anonymous user
185
        if (
186
            $result->isValid() &&
187
            $parsedToken->getClaim('uid') === 0 &&
188
            static::config()->get('anonymous_allowed')
189
        ) {
190
            $member = Member::create(['ID' => 0, 'FirstName' => 'Anonymous']);
191
        }
192
193
        return $result->isValid() ? $member : null;
194
    }
195
196
    /**
197
     * @param HTTPRequest $request
198
     * @param Token $parsedToken
199
     * @return array Contains a ValidationData and Member object
200
     * @throws OutOfBoundsException
201
     */
202
    private function getValidator($request, $parsedToken)
203
    {
204
        $audience = $request->getHeader('Origin');
205
206
        $member = null;
207
        $id = null;
208
        $validator = new ValidationData();
209
        $validator->setIssuer(Director::absoluteBaseURL());
210
        $validator->setAudience($audience);
211
212
        if ($parsedToken->getClaim('uid') === 0 && static::config()->get('anonymous_allowed')) {
213
            $id = $request->getSession()->get('jwt_uid');
214
        } elseif ($parsedToken->getClaim('uid') > 0) {
215
            $member = Member::get()->byID($parsedToken->getClaim('uid'));
216
            $id = $member->JWTUniqueID;
217
        }
218
219
        $validator->setId($id);
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type array; however, parameter $id of Lcobucci\JWT\ValidationData::setId() does only seem to accept 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

219
        $validator->setId(/** @scrutinizer ignore-type */ $id);
Loading history...
220
221
        return [$validator, $member];
222
    }
223
}
224