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); |
|
|
|
|
164
|
|
|
$valid = $parsedToken->validate($validator); |
|
|
|
|
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] |
|
|
|
|
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(); |
|
|
|
|
203
|
|
|
$validator->setIssuer(Director::absoluteBaseURL()); |
|
|
|
|
204
|
|
|
$validator->setAudience($audience); |
205
|
|
|
|
206
|
|
|
if ($parsedToken->getClaim('uid') === 0 && static::config()->get('anonymous_allowed')) { |
|
|
|
|
207
|
|
|
$id = $request->getSession()->get('jwt_uid'); |
208
|
|
|
} elseif ($parsedToken->getClaim('uid') > 0) { |
|
|
|
|
209
|
|
|
$member = Member::get()->byID($parsedToken->getClaim('uid')); |
|
|
|
|
210
|
|
|
$id = $member->JWTUniqueID; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
$validator->setId($id); |
214
|
|
|
|
215
|
|
|
return [$validator, $member]; |
216
|
|
|
} |
217
|
|
|
} |
218
|
|
|
|
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.