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

tests/OAuthServerTest.php (1 issue)

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