Completed
Push — master ( 3a6bc2...bdf328 )
by Conrad
01:56
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\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);
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 () {
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);
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
$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);
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