Passed
Push — master ( 7e4f27...ed015a )
by Conrad
01:50
created

OAuthServerTest::testAuthenticationException()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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