Completed
Push — master ( c3ed74...023bd4 )
by Simon
01:30
created

src/Authentication/JWTAuthenticator.php (8 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Deprecated Code introduced by
The method Lcobucci\JWT\Token::verify() has been deprecated with message: This method will be removed on v4, new validation API should be used

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
164
        $valid = $parsedToken->validate($validator);
0 ignored issues
show
Deprecated Code introduced by
The method Lcobucci\JWT\Token::validate() has been deprecated with message: This method will be removed on v4, new validation API should be used

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
165
166
        // If the token is not verified, just give up
167
        if (!$verified || !$valid) {
168
            $result->addError('Invalid token');
169
        }
170
        // An expired token can be renewed
171
        if (
172
            $verified &&
173
            $parsedToken->isExpired()
174
        ) {
175
            $result->addError('Token is expired, please renew your token with a refreshToken query');
176
        }
177
        // Not entirely fine, do we allow anonymous users?
178
        // Then, if the token is valid, return an anonymous user
179
        if (
180
            $result->isValid() &&
181
            $parsedToken->getClaim('uid') === 0 &&
182
            static::config()->get('anonymous_allowed')
183
        ) {
184
            $member = Member::create(['ID' => 0, 'FirstName' => 'Anonymous']);
185
        }
186
187
        return $result->isValid() ? $member : null;
188
    }
189
190
    /**
191
     * @param HTTPRequest $request
192
     * @param Token $parsedToken
193
     * @return array[ValidationData, Member]
0 ignored issues
show
The doc-type array[ValidationData, could not be parsed: Expected "]" at position 2, but found "ValidationData". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
194
     * @throws \OutOfBoundsException
195
     */
196
    private function getValidator($request, $parsedToken)
197
    {
198
        $audience = $request->getHeader('Origin');
199
200
        $member = null;
201
        $id = null;
202
        $validator = new ValidationData();
0 ignored issues
show
Deprecated Code introduced by
The class Lcobucci\JWT\ValidationData has been deprecated with message: This class will be removed on v4, new validation API should be used

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
203
        $validator->setIssuer(Director::absoluteBaseURL());
204
        $validator->setAudience($audience);
205
206
        if ($parsedToken->getClaim('uid') === 0 && static::config()->get('anonymous_allowed')) {
0 ignored issues
show
Deprecated Code introduced by
The method Lcobucci\JWT\Token::getClaim() has been deprecated with message: This method will be removed on v4

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
207
            $id = $request->getSession()->get('jwt_uid');
208
        } elseif ($parsedToken->getClaim('uid') > 0) {
0 ignored issues
show
Deprecated Code introduced by
The method Lcobucci\JWT\Token::getClaim() has been deprecated with message: This method will be removed on v4

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
209
            $member = Member::get()->byID($parsedToken->getClaim('uid'));
0 ignored issues
show
Deprecated Code introduced by
The method Lcobucci\JWT\Token::getClaim() has been deprecated with message: This method will be removed on v4

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
210
            $id = $member->JWTUniqueID;
211
        }
212
213
        $validator->setId($id);
214
215
        return [$validator, $member];
216
    }
217
}
218