Passed
Push — master ( 0e941b...0f3de7 )
by Conrad
08:36
created

OAuthServerTest   C

Complexity

Total Complexity 16

Size/Duplication

Total Lines 359
Duplicated Lines 2.51 %

Coupling/Cohesion

Components 1
Dependencies 34

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 16
lcom 1
cbo 34
dl 9
loc 359
rs 5
c 6
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A setUp() 0 21 1
A testClientGrant() 0 10 1
B testPasswordGrant() 0 38 1
A testScopes() 9 9 1
B testMiddleware() 0 26 1
B testAuthoriseController() 0 28 1
A testUserEntity() 0 7 1
A testGraphQLClient() 0 18 1
A testGraphQLMember() 0 49 1
A getAuthorisationServer() 0 22 1
A getResourceServer() 0 16 1
A getPrivateKeyPath() 0 4 1
A getPublicKeyPath() 0 4 1
A tearDown() 0 8 1
A generateClientAccessToken() 0 16 1
A getClientRequest() 0 17 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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
Duplication introduced by Conrad Dobbs
This method seems to be duplicated in your project.

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.

Loading history...
129
    {
130
        $member = $this->objFromFixture(Member::class, 'member1');
131
132
        $entity = new UserEntity($member);
0 ignored issues
show
Documentation introduced by Conrad Dobbs
$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...
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 () {
0 ignored issues
show
Unused Code introduced by Conrad
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...
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);
0 ignored issues
show
Documentation introduced by Conrad Dobbs
$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...
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);
0 ignored issues
show
Documentation introduced by Conrad Dobbs
$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...
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);
0 ignored issues
show
Documentation introduced by Conrad Dobbs
$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...
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