Completed
Push — master ( 0f5e6b...d874fd )
by Conrad
01:54
created

tests/OAuthServerTest.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

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