Completed
Push — master ( 3a6bc2...bdf328 )
by Conrad
01:56
created

OAuthServerTest::testGraphQLClient()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 18
rs 9.4285
cc 1
eloc 10
nc 1
nop 0
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\Response;
18
use GuzzleHttp\Psr7\ServerRequest;
19
use League\OAuth2\Server\AuthorizationServer;
20
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
21
use League\OAuth2\Server\Grant\PasswordGrant;
22
use Robbie\Psr7\HttpRequestAdapter;
23
use SilverStripe\Control\HTTPApplication;
24
use SilverStripe\Control\HTTPRequest;
25
use SilverStripe\Control\HTTPResponse;
26
use SilverStripe\Core\Config\Config;
27
use SilverStripe\Core\Environment;
28
use SilverStripe\Core\Injector\Injector;
29
use SilverStripe\Core\Kernel;
30
use SilverStripe\Core\Tests\Startup\ErrorControlChainMiddlewareTest\BlankKernel;
31
use SilverStripe\Dev\SapphireTest;
32
use SilverStripe\Security\Group;
33
use SilverStripe\Security\Member;
34
use SilverStripe\Security\Security;
35
use function file_get_contents;
36
use function file_put_contents;
37
use function sys_get_temp_dir;
38
39
class OAuthServerTest extends SapphireTest
40
{
41
    protected static $fixture_file = 'OAuthFixture.yml';
42
43
    protected static $privateKeyFile = 'private.key';
44
45
    protected static $publicKeyFile = 'public.key';
46
47
    /**
48
     * Setup test environment.
49
     */
50
    public function setUp()
51
    {
52
        // add GroupExtension for scopes
53
        Config::forClass(Group::class)->merge('extensions', [GroupExtension::class]);
54
55
        parent::setUp();
56
57
        // copy private key so we can set correct permissions, file gets removed when tests finish
58
        $path = $this->getPrivateKeyPath();
59
        file_put_contents($path, file_get_contents(__DIR__ . '/' . self::$privateKeyFile));
60
        chmod($path, 0660);
61
        Environment::setEnv('OAUTH_PRIVATE_KEY_PATH', $path);
62
63
        // copy public key
64
        $path = $this->getPublicKeyPath();
65
        file_put_contents($path, file_get_contents(__DIR__ . '/' . self::$publicKeyFile));
66
        chmod($path, 0660);
67
        Environment::setEnv('OAUTH_PUBLIC_KEY_PATH', $path);
68
69
        Security::force_database_is_ready(true);
70
    }
71
72
    /**
73
     * Test a client grant.
74
     */
75
    public function testClientGrant()
76
    {
77
        $response = $this->generateClientAccessToken();
78
        $data = json_decode((string)$response->getBody(), true);
79
80
        $this->assertArrayHasKey('token_type', $data, 'Response should have a token_type');
81
        $this->assertArrayHasKey('expires_in', $data, 'Response should have expire time for token');
82
        $this->assertArrayHasKey('access_token', $data, 'Response should have a token');
83
        $this->assertEquals('Bearer', $data['token_type'], 'Token type should be Bearer');
84
    }
85
86
    public function testPasswordGrant()
87
    {
88
        $userRepository = new UserRepository();
89
        $refreshRepository = new RefreshTokenRepository();
90
91
        $server = $this->getAuthorisationServer();
92
        $server->enableGrantType(
93
            new PasswordGrant($userRepository, $refreshRepository),
94
            new \DateInterval('PT1H')
95
        );
96
97
        $client = $this->objFromFixture(Client::class, 'webapp');
98
        $member = $this->objFromFixture(Member::class, 'member1');
99
100
        $request = (new ServerRequest(
101
            'POST',
102
            '',
103
            ['Content-Type' => 'application/json']
104
        ))->withParsedBody([
105
            'grant_type' => 'password',
106
            'client_id' => $client->Identifier,
107
            'client_secret' => $client->Secret,
108
            'scope' => 'members',
109
            'username' => $member->Email,
110
            'password' => 'password1'
111
        ]);
112
113
        $response = new Response();
114
        $response = $server->respondToAccessTokenRequest($request, $response);
115
116
        $data = json_decode((string)$response->getBody(), true);
117
118
        $this->assertNotEmpty($data, 'Should have received response data');
119
        $this->assertArrayHasKey('token_type', $data, 'Response should have a token_type');
120
        $this->assertArrayHasKey('expires_in', $data, 'Response should have expire time for token');
121
        $this->assertArrayHasKey('access_token', $data, 'Response should have a token');
122
        $this->assertEquals('Bearer', $data['token_type'], 'Token type should be Bearer');
123
    }
124
125
    public function testScopes()
126
    {
127
        $member = $this->objFromFixture(Member::class, 'member1');
128
129
        $entity = new UserEntity($member);
0 ignored issues
show
Documentation introduced by
$member is of type object<SilverStripe\ORM\DataObject>|null, but the function expects a object<SilverStripe\Security\Member>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
130
131
        $this->assertTrue($entity->hasScope('scope1'), 'Member should have scope1');
132
        $this->assertFalse($entity->hasScope('scope2'), 'Member should not have scope2');
133
    }
134
135
    public function testMiddleware()
136
    {
137
        $response = $this->generateClientAccessToken();
138
        $data = json_decode((string)$response->getBody(), true);
139
        $token = $data['access_token'];
140
141
        $server = $this->getResourceServer();
142
143
        // set the resource server on authenticator service
144
        Injector::inst()->get(Authenticator::class)->setServer($server);
145
146
        $request = new HTTPRequest('GET', '/');
147
        $request->addHeader('authorization', 'Bearer ' . $token);
148
        // fake server port
149
        $_SERVER['SERVER_PORT'] = 443;
150
151
        // Mock app
152
        $app = new HTTPApplication(new BlankKernel(BASE_PATH));
153
        $app->getKernel()->setEnvironment(Kernel::LIVE);
154
155
        $result = (new AuthenticationMiddleware($app))->process($request, function () {
0 ignored issues
show
Unused Code introduced by
The call to AuthenticationMiddleware::__construct() has too many arguments starting with $app.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
156
            return null;
157
        });
158
159
        $this->assertNull($result, 'Resource Server shouldn\'t modify the response');
160
    }
161
162
    public function testAuthoriseController()
163
    {
164
        $controller = new AuthoriseController(new DefaultGenerator());
165
166
        $client = $this->objFromFixture(Client::class, 'webapp');
167
        $request = $this->getClientRequest($client);
0 ignored issues
show
Documentation introduced by
$client is of type object<SilverStripe\ORM\DataObject>|null, but the function expects a object<AdvancedLearning\...h2Server\Models\Client>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
168
169
        /**
170
         * @var HTTPResponse $response
171
         */
172
        $response = $controller->setRequest(
173
            (new HttpRequestAdapter())
174
                ->fromPsr7($request)
175
                // controller expects a string
176
                ->setBody(json_encode($request->getParsedBody()))
177
        )
178
            ->index();
179
180
        $this->assertInstanceOf(HTTPResponse::class, $response, 'Should receive a response object');
181
        $this->assertEquals(200, $response->getStatusCode(), 'Should receive a 200 response code');
182
183
        // check for access token
184
        $data = json_decode($response->getBody(), true);
185
        $this->assertArrayHasKey('token_type', $data, 'Response should have a token_type');
186
        $this->assertArrayHasKey('expires_in', $data, 'Response should have expire time for token');
187
        $this->assertArrayHasKey('access_token', $data, 'Response should have a token');
188
        $this->assertEquals('Bearer', $data['token_type'], 'Token type should be Bearer');
189
    }
190
191
    public function testUserEntity()
192
    {
193
        $member = $this->objFromFixture(Member::class, 'member1');
194
        $entity = new UserEntity($member);
0 ignored issues
show
Documentation introduced by
$member is of type object<SilverStripe\ORM\DataObject>|null, but the function expects a object<SilverStripe\Security\Member>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
195
196
        $this->assertEquals($member->ID, $entity->getMember()->ID, 'User entity member should have been set');
197
    }
198
199
    public function testGraphQLClient()
200
    {
201
        // generate token
202
        $response = $this->generateClientAccessToken();
203
        $data = json_decode((string)$response->getBody(), true);
204
        $token = $data['access_token'];
205
206
        // create request
207
        $request = new HTTPRequest('GET', '/');
208
        $request->addHeader('authorization', 'Bearer ' . $token);
209
        // fake server port
210
        $_SERVER['SERVER_PORT'] = 443;
211
212
        $member = (new \AdvancedLearning\Oauth2Server\GraphQL\Authenticator())->authenticate($request);
213
214
        $this->assertEquals('My Web App', $member->FirstName, 'Member FirstName should be same as client name');
215
        $this->assertEquals(0, $member->ID, 'Member should not have and ID');
216
    }
217
218
    public function testGraphQLMember()
219
    {
220
        $userRepository = new UserRepository();
221
        $refreshRepository = new RefreshTokenRepository();
222
223
        $server = $this->getAuthorisationServer();
224
        $server->enableGrantType(
225
            new PasswordGrant($userRepository, $refreshRepository),
226
            new \DateInterval('PT1H')
227
        );
228
229
        $client = $this->objFromFixture(Client::class, 'webapp');
230
        $member = $this->objFromFixture(Member::class, 'member1');
231
232
        $request = (new ServerRequest(
233
            'POST',
234
            '',
235
            ['Content-Type' => 'application/json']
236
        ))->withParsedBody([
237
            'grant_type' => 'password',
238
            'client_id' => $client->Identifier,
239
            'client_secret' => $client->Secret,
240
            'scope' => 'members',
241
            'username' => $member->Email,
242
            'password' => 'password1'
243
        ]);
244
245
        $response = new Response();
246
        $response = $server->respondToAccessTokenRequest($request, $response);
247
248
        $data = json_decode((string)$response->getBody(), true);
249
        $token = $data['access_token'];
250
251
        // create request
252
        $request = new HTTPRequest('GET', '/');
253
        $request->addHeader('authorization', 'Bearer ' . $token);
254
        // fake server port
255
        $_SERVER['SERVER_PORT'] = 443;
256
257
        $authMember = (new \AdvancedLearning\Oauth2Server\GraphQL\Authenticator())->authenticate($request);
258
259
        $this->assertEquals($member->ID, $authMember->ID, 'Member should exist in DB');
260
    }
261
262
    /**
263
     * Setup the Authorization Server.
264
     *
265
     * @return AuthorizationServer
266
     */
267
    protected function getAuthorisationServer()
268
    {
269
        // Init our repositories
270
        $clientRepository = new ClientRepository(); // instance of ClientRepositoryInterface
271
        $scopeRepository = new ScopeRepository(); // instance of ScopeRepositoryInterface
272
        $accessTokenRepository = new AccessTokenRepository(); // instance of AccessTokenRepositoryInterface
273
274
        // Path to public and private keys
275
        $privateKey = $this->getPrivateKeyPath();
276
        $encryptionKey = 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen';
277
278
        // Setup the authorization server
279
        $server = new AuthorizationServer(
280
            $clientRepository,
281
            $accessTokenRepository,
282
            $scopeRepository,
283
            $privateKey,
284
            $encryptionKey
285
        );
286
287
        return $server;
288
    }
289
290
    /**
291
     * Get the resource server.
292
     *
293
     * @return \League\OAuth2\Server\ResourceServer
294
     */
295
    protected function getResourceServer()
296
    {
297
        // Init our repositories
298
        $accessTokenRepository = new AccessTokenRepository(); // instance of AccessTokenRepositoryInterface
299
300
        // Path to authorization server's public key
301
        $publicKeyPath = $this->getPublicKeyPath();
302
303
        // Setup the authorization server
304
        $server = new \League\OAuth2\Server\ResourceServer(
305
            $accessTokenRepository,
306
            $publicKeyPath
307
        );
308
309
        return $server;
310
    }
311
312
    /**
313
     * Get the full path the private key.
314
     *
315
     * @return string
316
     */
317
    protected function getPrivateKeyPath()
318
    {
319
        return sys_get_temp_dir() . '/' . self::$privateKeyFile;
320
    }
321
322
    /**
323
     * Get the full path the public key.
324
     *
325
     * @return string
326
     */
327
    protected function getPublicKeyPath()
328
    {
329
        return sys_get_temp_dir() . '/' . self::$publicKeyFile;
330
    }
331
332
    /**
333
     * Cleanup test environment.
334
     */
335
    protected function tearDown()
336
    {
337
        parent::tearDown();
338
        // remove private key after tests have finished
339
        unlink($this->getPrivateKeyPath());
340
        // remove public key after tests have finished
341
        unlink($this->getPublicKeyPath());
342
    }
343
344
    /**
345
     * Generates a response with an access token using the client grant.
346
     *
347
     * @return \Psr\Http\Message\ResponseInterface
348
     */
349
    protected function generateClientAccessToken()
350
    {
351
        $server = $this->getAuthorisationServer();
352
        // Enable the client credentials grant on the server
353
        $server->enableGrantType(
354
            new ClientCredentialsGrant(),
355
            new \DateInterval('PT1H') // access tokens will expire after 1 hour
356
        );
357
358
        $client = $this->objFromFixture(Client::class, 'webapp');
359
360
        $request = $this->getClientRequest($client);
0 ignored issues
show
Documentation introduced by
$client is of type object<SilverStripe\ORM\DataObject>|null, but the function expects a object<AdvancedLearning\...h2Server\Models\Client>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
361
362
        $response = new Response();
363
        return $server->respondToAccessTokenRequest($request, $response);
364
    }
365
366
    /**
367
     * Get PSR7 request object to be used for a client grant.
368
     *
369
     * @param Client $client
370
     *
371
     * @return ServerRequest
372
     */
373
    protected function getClientRequest(Client $client)
374
    {
375
        // setup server vars
376
        $_SERVER['SERVER_PORT'] = 80;
377
        $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
378
379
        return (new ServerRequest(
380
            'POST',
381
            '',
382
            ['Content-Type' => 'application/json']
383
        ))->withParsedBody([
384
            'grant_type' => 'client_credentials',
385
            'client_id' => $client->Identifier,
386
            'client_secret' => $client->Secret,
387
            'scope' => 'members'
388
        ]);
389
    }
390
391
}
392