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 AdvancedLearning\Oauth2Server\Tests; |
||
4 | |||
5 | use AdvancedLearning\Oauth2Server\AuthorizationServer\DefaultGenerator; |
||
6 | use AdvancedLearning\Oauth2Server\Controllers\AuthoriseController; |
||
7 | use AdvancedLearning\Oauth2Server\Entities\UserEntity; |
||
8 | use AdvancedLearning\Oauth2Server\Extensions\GroupExtension; |
||
9 | use AdvancedLearning\Oauth2Server\Middleware\AuthenticationMiddleware; |
||
10 | use AdvancedLearning\Oauth2Server\Models\Client; |
||
11 | use AdvancedLearning\Oauth2Server\Repositories\AccessTokenRepository; |
||
12 | use AdvancedLearning\Oauth2Server\Repositories\ClientRepository; |
||
13 | use AdvancedLearning\Oauth2Server\Repositories\RefreshTokenRepository; |
||
14 | use AdvancedLearning\Oauth2Server\Repositories\ScopeRepository; |
||
15 | use AdvancedLearning\Oauth2Server\Repositories\UserRepository; |
||
16 | use AdvancedLearning\Oauth2Server\Services\Authenticator; |
||
17 | use GuzzleHttp\Psr7\ServerRequest; |
||
18 | use League\OAuth2\Server\AuthorizationServer; |
||
19 | use League\OAuth2\Server\Grant\ClientCredentialsGrant; |
||
20 | use League\OAuth2\Server\Grant\PasswordGrant; |
||
21 | use Lcobucci\JWT\Claim\Factory as ClaimFactory; |
||
22 | use Lcobucci\JWT\Parser; |
||
23 | use Lcobucci\JWT\Parsing\Encoder; |
||
24 | use GuzzleHttp\Psr7\Response; |
||
25 | use Robbie\Psr7\HttpRequestAdapter; |
||
26 | use SilverStripe\Control\HTTPApplication; |
||
27 | use SilverStripe\Control\HTTPRequest; |
||
28 | use SilverStripe\Control\HTTPResponse; |
||
29 | use SilverStripe\Core\Config\Config; |
||
30 | use SilverStripe\Core\Environment; |
||
31 | use SilverStripe\Core\Injector\Injector; |
||
32 | use SilverStripe\Core\Kernel; |
||
33 | use SilverStripe\Core\Tests\Startup\ErrorControlChainMiddlewareTest\BlankKernel; |
||
34 | use SilverStripe\Dev\SapphireTest; |
||
35 | use SilverStripe\Security\Group; |
||
36 | use SilverStripe\Security\Member; |
||
37 | use SilverStripe\Security\Security; |
||
38 | use function file_get_contents; |
||
39 | use function file_put_contents; |
||
40 | use function sys_get_temp_dir; |
||
41 | |||
42 | class OAuthServerTest extends SapphireTest |
||
43 | { |
||
44 | protected static $fixture_file = 'OAuthFixture.yml'; |
||
45 | |||
46 | protected static $privateKeyFile = 'private.key'; |
||
47 | |||
48 | protected static $publicKeyFile = 'public.key'; |
||
49 | |||
50 | /** |
||
51 | * Setup test environment. |
||
52 | */ |
||
53 | public function setUp() |
||
54 | { |
||
55 | // add GroupExtension for scopes |
||
56 | Config::forClass(Group::class)->merge('extensions', [GroupExtension::class]); |
||
57 | |||
58 | parent::setUp(); |
||
59 | |||
60 | // copy private key so we can set correct permissions, file gets removed when tests finish |
||
61 | $path = $this->getPrivateKeyPath(); |
||
62 | file_put_contents($path, file_get_contents(__DIR__ . '/' . self::$privateKeyFile)); |
||
63 | chmod($path, 0660); |
||
64 | Environment::setEnv('OAUTH_PRIVATE_KEY_PATH', $path); |
||
65 | |||
66 | // copy public key |
||
67 | $path = $this->getPublicKeyPath(); |
||
68 | file_put_contents($path, file_get_contents(__DIR__ . '/' . self::$publicKeyFile)); |
||
69 | chmod($path, 0660); |
||
70 | Environment::setEnv('OAUTH_PUBLIC_KEY_PATH', $path); |
||
71 | |||
72 | Security::force_database_is_ready(true); |
||
73 | } |
||
74 | |||
75 | /** |
||
76 | * Test a client grant. |
||
77 | */ |
||
78 | public function testClientGrant() |
||
79 | { |
||
80 | $response = $this->generateClientAccessToken(); |
||
81 | $data = json_decode((string)$response->getBody(), true); |
||
82 | |||
83 | $this->assertArrayHasKey('token_type', $data, 'Response should have a token_type'); |
||
84 | $this->assertArrayHasKey('expires_in', $data, 'Response should have expire time for token'); |
||
85 | $this->assertArrayHasKey('access_token', $data, 'Response should have a token'); |
||
86 | $this->assertEquals('Bearer', $data['token_type'], 'Token type should be Bearer'); |
||
87 | } |
||
88 | |||
89 | public function testPasswordGrant() |
||
90 | { |
||
91 | $userRepository = new UserRepository(); |
||
92 | $refreshRepository = new RefreshTokenRepository(); |
||
93 | |||
94 | $server = $this->getAuthorisationServer(); |
||
95 | $server->enableGrantType( |
||
96 | new PasswordGrant($userRepository, $refreshRepository), |
||
97 | new \DateInterval('PT1H') |
||
98 | ); |
||
99 | |||
100 | $client = $this->objFromFixture(Client::class, 'webapp'); |
||
101 | $member = $this->objFromFixture(Member::class, 'member1'); |
||
102 | |||
103 | $request = (new ServerRequest( |
||
104 | 'POST', |
||
105 | '', |
||
106 | ['Content-Type' => 'application/json'] |
||
107 | ))->withParsedBody([ |
||
108 | 'grant_type' => 'password', |
||
109 | 'client_id' => $client->Identifier, |
||
110 | 'client_secret' => $client->Secret, |
||
111 | 'scope' => 'members', |
||
112 | 'username' => $member->Email, |
||
113 | 'password' => 'password1' |
||
114 | ]); |
||
115 | |||
116 | $response = new Response(); |
||
117 | $response = $server->respondToAccessTokenRequest($request, $response); |
||
118 | |||
119 | $data = json_decode((string)$response->getBody(), true); |
||
120 | |||
121 | $this->assertNotEmpty($data, 'Should have received response data'); |
||
122 | $this->assertArrayHasKey('token_type', $data, 'Response should have a token_type'); |
||
123 | $this->assertArrayHasKey('expires_in', $data, 'Response should have expire time for token'); |
||
124 | $this->assertArrayHasKey('access_token', $data, 'Response should have a token'); |
||
125 | $this->assertEquals('Bearer', $data['token_type'], 'Token type should be Bearer'); |
||
126 | } |
||
127 | |||
128 | View Code Duplication | public function testScopes() |
|
0 ignored issues
–
show
|
|||
129 | { |
||
130 | $member = $this->objFromFixture(Member::class, 'member1'); |
||
131 | |||
132 | $entity = new UserEntity($member); |
||
133 | |||
134 | $this->assertTrue($entity->hasScope('scope1'), 'Member should have scope1'); |
||
135 | $this->assertFalse($entity->hasScope('scope2'), 'Member should not have scope2'); |
||
136 | } |
||
137 | |||
138 | public function testMiddleware() |
||
139 | { |
||
140 | $response = $this->generateClientAccessToken(); |
||
141 | $data = json_decode((string)$response->getBody(), true); |
||
142 | $token = $data['access_token']; |
||
143 | |||
144 | $server = $this->getResourceServer(); |
||
145 | |||
146 | // set the resource server on authenticator service |
||
147 | Injector::inst()->get(Authenticator::class)->setServer($server); |
||
148 | |||
149 | $request = new HTTPRequest('GET', '/'); |
||
150 | $request->addHeader('authorization', 'Bearer ' . $token); |
||
151 | // fake server port |
||
152 | $_SERVER['SERVER_PORT'] = 443; |
||
153 | |||
154 | // Mock app |
||
155 | $app = new HTTPApplication(new BlankKernel(BASE_PATH)); |
||
156 | $app->getKernel()->setEnvironment(Kernel::LIVE); |
||
157 | |||
158 | $result = (new AuthenticationMiddleware($app))->process($request, function () { |
||
159 | return null; |
||
160 | }); |
||
161 | |||
162 | $this->assertNull($result, 'Resource Server shouldn\'t modify the response'); |
||
163 | } |
||
164 | |||
165 | public function testAuthoriseController() |
||
166 | { |
||
167 | $controller = new AuthoriseController(new DefaultGenerator()); |
||
168 | |||
169 | $client = $this->objFromFixture(Client::class, 'webapp'); |
||
170 | $request = $this->getClientRequest($client); |
||
171 | |||
172 | /** |
||
173 | * @var HTTPResponse $response |
||
174 | */ |
||
175 | $response = $controller->setRequest( |
||
176 | (new HttpRequestAdapter()) |
||
177 | ->fromPsr7($request) |
||
178 | // controller expects a string |
||
179 | ->setBody(json_encode($request->getParsedBody())) |
||
180 | ) |
||
181 | ->index(); |
||
182 | |||
183 | $this->assertInstanceOf(HTTPResponse::class, $response, 'Should receive a response object'); |
||
184 | $this->assertEquals(200, $response->getStatusCode(), 'Should receive a 200 response code'); |
||
185 | |||
186 | // check for access token |
||
187 | $data = json_decode($response->getBody(), true); |
||
188 | $this->assertArrayHasKey('token_type', $data, 'Response should have a token_type'); |
||
189 | $this->assertArrayHasKey('expires_in', $data, 'Response should have expire time for token'); |
||
190 | $this->assertArrayHasKey('access_token', $data, 'Response should have a token'); |
||
191 | $this->assertEquals('Bearer', $data['token_type'], 'Token type should be Bearer'); |
||
192 | } |
||
193 | |||
194 | public function testUserEntity() |
||
195 | { |
||
196 | $member = $this->objFromFixture(Member::class, 'member1'); |
||
197 | $entity = new UserEntity($member); |
||
198 | |||
199 | $this->assertEquals($member->ID, $entity->getMember()->ID, 'User entity member should have been set'); |
||
200 | } |
||
201 | |||
202 | public function testGraphQLClient() |
||
203 | { |
||
204 | // generate token |
||
205 | $response = $this->generateClientAccessToken(); |
||
206 | $data = json_decode((string)$response->getBody(), true); |
||
207 | $token = $data['access_token']; |
||
208 | |||
209 | // create request |
||
210 | $request = new HTTPRequest('GET', '/'); |
||
211 | $request->addHeader('authorization', 'Bearer ' . $token); |
||
212 | // fake server port |
||
213 | $_SERVER['SERVER_PORT'] = 443; |
||
214 | |||
215 | $member = (new \AdvancedLearning\Oauth2Server\GraphQL\Authenticator())->authenticate($request); |
||
216 | |||
217 | $this->assertEquals('My Web App', $member->FirstName, 'Member FirstName should be same as client name'); |
||
218 | $this->assertEquals(0, $member->ID, 'Member should not have and ID'); |
||
219 | } |
||
220 | |||
221 | public function testGraphQLMember() |
||
222 | { |
||
223 | $userRepository = new UserRepository(); |
||
224 | $refreshRepository = new RefreshTokenRepository(); |
||
225 | |||
226 | $server = $this->getAuthorisationServer(); |
||
227 | $server->enableGrantType( |
||
228 | new PasswordGrant($userRepository, $refreshRepository), |
||
229 | new \DateInterval('PT1H') |
||
230 | ); |
||
231 | |||
232 | $client = $this->objFromFixture(Client::class, 'webapp'); |
||
233 | $member = $this->objFromFixture(Member::class, 'member1'); |
||
234 | |||
235 | $request = (new ServerRequest( |
||
236 | 'POST', |
||
237 | '', |
||
238 | ['Content-Type' => 'application/json'] |
||
239 | ))->withParsedBody([ |
||
240 | 'grant_type' => 'password', |
||
241 | 'client_id' => $client->Identifier, |
||
242 | 'client_secret' => $client->Secret, |
||
243 | 'scope' => 'members', |
||
244 | 'username' => $member->Email, |
||
245 | 'password' => 'password1' |
||
246 | ]); |
||
247 | |||
248 | $response = new Response(); |
||
249 | $response = $server->respondToAccessTokenRequest($request, $response); |
||
250 | |||
251 | $data = json_decode((string)$response->getBody(), true); |
||
252 | $token = $data['access_token']; |
||
253 | |||
254 | // check for fn/ln |
||
255 | $decoded = (new Parser())->parse($token); |
||
256 | |||
257 | $this->assertEquals('My', $decoded->getClaim('fn'), 'First name should be correctly set'); |
||
258 | $this->assertEquals('Test', $decoded->getClaim('ln'), 'Last name should be correctly set'); |
||
259 | |||
260 | // create request |
||
261 | $request = new HTTPRequest('GET', '/'); |
||
262 | $request->addHeader('authorization', 'Bearer ' . $token); |
||
263 | // fake server port |
||
264 | $_SERVER['SERVER_PORT'] = 443; |
||
265 | |||
266 | $authMember = (new \AdvancedLearning\Oauth2Server\GraphQL\Authenticator())->authenticate($request); |
||
267 | |||
268 | $this->assertEquals($member->ID, $authMember->ID, 'Member should exist in DB'); |
||
269 | } |
||
270 | |||
271 | /** |
||
272 | * Setup the Authorization Server. |
||
273 | * |
||
274 | * @return AuthorizationServer |
||
275 | */ |
||
276 | protected function getAuthorisationServer() |
||
277 | { |
||
278 | // Init our repositories |
||
279 | $clientRepository = new ClientRepository(); // instance of ClientRepositoryInterface |
||
280 | $scopeRepository = new ScopeRepository(); // instance of ScopeRepositoryInterface |
||
281 | $accessTokenRepository = new AccessTokenRepository(); // instance of AccessTokenRepositoryInterface |
||
282 | |||
283 | // Path to public and private keys |
||
284 | $privateKey = $this->getPrivateKeyPath(); |
||
285 | $encryptionKey = 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen'; |
||
286 | |||
287 | // Setup the authorization server |
||
288 | $server = new AuthorizationServer( |
||
289 | $clientRepository, |
||
290 | $accessTokenRepository, |
||
291 | $scopeRepository, |
||
292 | $privateKey, |
||
293 | $encryptionKey |
||
294 | ); |
||
295 | |||
296 | return $server; |
||
297 | } |
||
298 | |||
299 | /** |
||
300 | * Get the resource server. |
||
301 | * |
||
302 | * @return \League\OAuth2\Server\ResourceServer |
||
303 | */ |
||
304 | protected function getResourceServer() |
||
305 | { |
||
306 | // Init our repositories |
||
307 | $accessTokenRepository = new AccessTokenRepository(); // instance of AccessTokenRepositoryInterface |
||
308 | |||
309 | // Path to authorization server's public key |
||
310 | $publicKeyPath = $this->getPublicKeyPath(); |
||
311 | |||
312 | // Setup the authorization server |
||
313 | $server = new \League\OAuth2\Server\ResourceServer( |
||
314 | $accessTokenRepository, |
||
315 | $publicKeyPath |
||
316 | ); |
||
317 | |||
318 | return $server; |
||
319 | } |
||
320 | |||
321 | /** |
||
322 | * Get the full path the private key. |
||
323 | * |
||
324 | * @return string |
||
325 | */ |
||
326 | protected function getPrivateKeyPath() |
||
327 | { |
||
328 | return sys_get_temp_dir() . '/' . self::$privateKeyFile; |
||
329 | } |
||
330 | |||
331 | /** |
||
332 | * Get the full path the public key. |
||
333 | * |
||
334 | * @return string |
||
335 | */ |
||
336 | protected function getPublicKeyPath() |
||
337 | { |
||
338 | return sys_get_temp_dir() . '/' . self::$publicKeyFile; |
||
339 | } |
||
340 | |||
341 | /** |
||
342 | * Cleanup test environment. |
||
343 | */ |
||
344 | protected function tearDown() |
||
345 | { |
||
346 | parent::tearDown(); |
||
347 | // remove private key after tests have finished |
||
348 | unlink($this->getPrivateKeyPath()); |
||
349 | // remove public key after tests have finished |
||
350 | unlink($this->getPublicKeyPath()); |
||
351 | } |
||
352 | |||
353 | /** |
||
354 | * Generates a response with an access token using the client grant. |
||
355 | * |
||
356 | * @return \Psr\Http\Message\ResponseInterface |
||
357 | */ |
||
358 | protected function generateClientAccessToken() |
||
359 | { |
||
360 | $server = $this->getAuthorisationServer(); |
||
361 | // Enable the client credentials grant on the server |
||
362 | $server->enableGrantType( |
||
363 | new ClientCredentialsGrant(), |
||
364 | new \DateInterval('PT1H') // access tokens will expire after 1 hour |
||
365 | ); |
||
366 | |||
367 | $client = $this->objFromFixture(Client::class, 'webapp'); |
||
368 | |||
369 | $request = $this->getClientRequest($client); |
||
370 | |||
371 | $response = new Response(); |
||
372 | return $server->respondToAccessTokenRequest($request, $response); |
||
373 | } |
||
374 | |||
375 | /** |
||
376 | * Get PSR7 request object to be used for a client grant. |
||
377 | * |
||
378 | * @param Client $client |
||
379 | * |
||
380 | * @return ServerRequest |
||
381 | */ |
||
382 | protected function getClientRequest(Client $client) |
||
383 | { |
||
384 | // setup server vars |
||
385 | $_SERVER['SERVER_PORT'] = 80; |
||
386 | $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; |
||
387 | |||
388 | return (new ServerRequest( |
||
389 | 'POST', |
||
390 | '', |
||
391 | ['Content-Type' => 'application/json'] |
||
392 | ))->withParsedBody([ |
||
393 | 'grant_type' => 'client_credentials', |
||
394 | 'client_id' => $client->Identifier, |
||
395 | 'client_secret' => $client->Secret, |
||
396 | 'scope' => 'members' |
||
397 | ]); |
||
398 | } |
||
399 | |||
400 | } |
||
401 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.