v0Authenticator   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 384
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 20
eloc 98
dl 0
loc 384
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A authenticate() 0 15 2
A needsRefresh() 0 10 3
A getAuthFailureCodes() 0 22 1
A setParameters() 0 7 1
A getDefaultOptions() 0 12 1
B throwForStatus() 0 46 6
A createBodyArray() 0 9 1
A addAuthentication() 0 4 1
A validateParameters() 0 9 1
A extractAuthToken() 0 15 3
1
<?php
2
/**
3
 * V0 Authenticator
4
 */
5
namespace Twigger\UnionCloud\API\Auth;
6
7
use Carbon\Carbon;
8
use GuzzleHttp\Client;
9
use GuzzleHttp\Exception\RequestException;
10
use Psr\Http\Message\ResponseInterface;
11
use Twigger\UnionCloud\API\Exception\Authentication\AuthenticationIncorrectParameters;
12
use Twigger\UnionCloud\API\Exception\Authentication\AuthenticationParameterMissing;
13
use Twigger\UnionCloud\API\Exception\Authentication\UnionCloudResponseAuthenticationException;
14
use Twigger\UnionCloud\API\Exception\InsufficientPermissionException;
15
use Twigger\UnionCloud\API\Exception\BaseUnionCloudException;
16
use Twigger\UnionCloud\API\Traits\Authenticates;
17
18
/**
19
 * The authenticator for version 0 of the API.
20
 *
21
 * Version 0 uses the API Endpoint '/api/authenticate'
22
 * to collect an Authentication token. This is then
23
 * added into the header of the request in 'auth_token'
24
 *
25
 * Class v0Authenticator
26
 *
27
 * @package Twigger\UnionCloud\API\Authenticators
28
 */
29
class v0Authenticator implements IAuthenticator
30
{
31
    use Authenticates;
32
33
    /**
34
     * User Email
35
     *
36
     * @var string
37
     */
38
    private $email;
39
40
    /**
41
     * User Password
42
     *
43
     * @var string
44
     */
45
    private $password;
46
47
    /**
48
     * User App ID
49
     *
50
     * @var string
51
     */
52
    private $appID;
53
54
    /**
55
     * User App Password
56
     *
57
     * @var string
58
     */
59
    private $appPassword;
60
61
    /**
62
     * User Auth Token
63
     *
64
     * @var string
65
     */
66
    private $authToken;
67
68
    /**
69
     * User Union ID
70
     *
71
     * @var string
72
     */
73
    private $union_id;
74
75
    /**
76
     * Expiry of the Auth Token
77
     *
78
     * @var Carbon
79
     */
80
    private $expires;
81
82
    /*
83
    |--------------------------------------------------------------------------
84
    | Inherited from the Authenticator
85
    |--------------------------------------------------------------------------
86
    |
87
    | These functions are implementations of the Authenticator Interface
88
    |
89
    */
90
91
    /**
92
     * Validate the parameters used to authenticate
93
     *
94
     * This function should ensure the associative array
95
     * $parameters contains the parameters for a successful
96
     * authentication.
97
     *
98
     * If not all the parameters are present, return false
99
     *
100
     * @param array $parameters
101
     *
102
     * @see IAuthenticator::validateParameters()
103
     *
104
     * @return bool
105
     */
106
    public function validateParameters($parameters)
107
    {
108
        $requiredParameters = [
109
            'email',
110
            'password',
111
            'appID',
112
            'appPassword',
113
        ];
114
        return $this->authArrayKeysExist($requiredParameters, $parameters);
115
    }
116
117
    /**
118
     * Save the parameters into the Authenticator class
119
     *
120
     * This function takes an associative array of authentication
121
     * parameters required by the authenticator, and
122
     * saves them to be used later.
123
     *
124
     * For example, if the only parameter required by the
125
     * API was an API key, we could implement this function
126
     * in the following:
127
     *
128
     * private $apiKey;
129
     *
130
     * public function setParameters($parameters)
131
     * {
132
     *      $this->apiKey = $parameters['api_key'];
133
     * }
134
     *
135
     * You may assume all the parameters are present, since we
136
     * will call your 'validateParameters' function first.
137
     *
138
     * @param array $parameters
139
     *
140
     * @see IAuthenticator::setParameters()
141
     *
142
     * @return void
143
     */
144
    public function setParameters($parameters)
145
    {
146
147
        $this->email = $parameters['email'];
148
        $this->password = $parameters['password'];
149
        $this->appID = $parameters['appID'];
150
        $this->appPassword = $parameters['appPassword'];
151
    }
152
153
    /**
154
     * Authentication method
155
     *
156
     * This method should make any necessary API calls etc
157
     * required for authentication.
158
     *
159
     * @param string $baseURL Base URL for making API calls
160
     *
161
     * @see IAuthenticator::authenticate()
162
     *
163
     * @throws BaseUnionCloudException
164
     * @throws UnionCloudResponseAuthenticationException
165
     * @throws \GuzzleHttp\Exception\GuzzleException
166
     *
167
     * @return void
168
     */
169
    public function authenticate($baseURL)
170
    {
171
        $client = new Client(['base_uri' => $baseURL]);
172
        try {
173
            $response = $client->request(
174
                'POST',
175
                '/api/authenticate',
176
                $this->getDefaultOptions()
177
            );
178
        } catch (RequestException $e) {
179
            // Add on custom exceptions to the stack
180
            $this->throwForStatus($e);
181
        }
182
183
        $this->extractAuthToken($response);
184
    }
185
186
    /**
187
     * Transform the Guzzle HTTP request options to include the authentication
188
     *
189
     * This method receives an array in the form required by the
190
     * GuzzleHttp Client. You may manipulate this in any valid way
191
     * to add authentication.
192
     *
193
     * For example, adding an auth token to the headers may be done as so:
194
     *
195
     * public function addAuthentication($options)
196
     * {
197
     *      $options = $this->addHeader($options, $this->authToken);
198
     *      return $options;
199
     * }
200
     *
201
     * In this the Authenticates trait was used, which contains helpful
202
     * methods for writing Authenticators.
203
     *
204
     * @param $options
205
     *
206
     * @see IAuthenticator::addAuthentication()
207
     *
208
     * @return array $options (transformed)
209
     */
210
    public function addAuthentication($options)
211
    {
212
        $options = $this->addHeader($options, 'auth_token', $this->authToken);
213
        return $options;
214
    }
215
216
    /**
217
     * Determine if the authenticate function needs to be called.
218
     *
219
     * For example, you could check an API key is present and
220
     * the expiry is still in the future.
221
     *
222
     * @see IAuthenticator::needsRefresh()
223
     *
224
     * @return bool
225
     */
226
    public function needsRefresh()
227
    {
228
        if($this->expires === null)
229
        {
230
            return true;
231
        } elseif ($this->expires instanceof Carbon)
232
        {
233
            return ! $this->expires->isFuture();
234
        }
235
        return true;
236
    }
237
238
    /*
239
    |--------------------------------------------------------------------------
240
    | Helper Functions
241
    |--------------------------------------------------------------------------
242
    |
243
    | Functions to aid the execution of this class
244
    |
245
    */
246
247
248
249
    /**
250
     * Build up an array to pass in the body of the authentication request
251
     *
252
     * This is simply an associative array containing all the
253
     * parameters required by the '/api/authenticate' endpoint.
254
     *
255
     * @return array
256
     */
257
    private function createBodyArray()
258
    {
259
        return [
260
            'email' => $this->email,
261
            'password' => $this->password,
262
            'app_id' => $this->appID,
263
            'date_stamp' => Carbon::now()->timestamp,
264
            'hash' => hash('sha256',
265
                $this->email.$this->password.$this->appID.Carbon::now()->timestamp.$this->appPassword
266
            ),
267
        ];
268
    }
269
270
    /**
271
     * Get the options for the authentication request.
272
     *
273
     * This is an associative array that matches the format
274
     * specified by GuzzleHTTP
275
     *
276
     * @return array GuzzleHTTP option array
277
     */
278
    private function getDefaultOptions()
279
    {
280
        return [
281
            'form_params' => $this->createBodyArray(),
282
            'headers' => [
283
                "User-Agent" => "Twigger-UnionCloud-API-Wrapper",
284
                "Content-Type" => "multipart/form-data",
285
                "Accept-Version" => "v1",
286
            ],
287
            'http_errors' => true,
288
            'verify' => __DIR__.'/../../unioncloud.pem',
289
            'debug' => false
290
        ];
291
    }
292
293
    /**
294
     * Check the body of the response to add any additional information to the exception
295
     *
296
     * The checks performed are as follows:
297
     *      - The response body shouldn't be null
298
     *      - The request body should be a JSON response
299
     *      - The request body should contain an element called 'error'
300
     *          - If this is the case, we can use the error codes
301
     *            as documented in the UnionCloud API documentation
302
     *            to provide additional information
303
     *
304
     * @param RequestException $e
305
     *
306
     * @throws BaseUnionCloudException
307
     * @throws UnionCloudResponseAuthenticationException
308
     */
309
    private function throwForStatus(RequestException $e)
310
    {
311
312
        $response = $e->getResponse();
313
        if ($response === null)
314
        {
315
            throw new BaseUnionCloudException('No response received from UnionCloud', 500, $e);
316
        }
317
        $body = $response->getBody()->getContents();
318
        $code = $response->getStatusCode();
319
320
321
        // If there's a body, we may be able to process the error messages
322
        if (strlen($body) > 0)
323
        {
324
            // Get the message body
325
            try {
326
                $responseBody = json_decode($body, true);
327
            } catch (\Exception $decodeError) {
328
                throw new UnionCloudResponseAuthenticationException('Invalid response body returned in Authentication', 500, $e);
329
            }
330
331
            // Parse the message error. For Authentication, this is a message and a code
332
            try {
333
                $errorMessage = $responseBody['error']['message'];
334
                $errorCode = $responseBody['error']['code'];
335
            } catch (\Exception $e)
336
            {
337
                throw new UnionCloudResponseAuthenticationException('Incorrect response body returned in Authentication'.json_encode($responseBody), 500);
338
            }
339
340
            // If we have a description of the error, set that to the exception message.
341
            // If we don't have a description, throw the error as is
342
343
            $unionCloudErrorCodes = $this->getAuthFailureCodes();
344
345
            if (array_key_exists($errorCode, $unionCloudErrorCodes))
346
            {
347
                $throwable = $unionCloudErrorCodes[$errorCode]['throwable'];
348
                throw new $throwable($unionCloudErrorCodes[$errorCode]['message'], $code, $e, $errorCode);
349
            }
350
351
            throw new UnionCloudResponseAuthenticationException($errorMessage, $code, $e, $errorCode);
352
        }
353
354
        throw $e;
355
    }
356
357
    /**
358
     * Get human readable error messages corresponding to those on the
359
     * UnionCloud API Documentation
360
     *
361
     * @return array
362
     */
363
    private function getAuthFailureCodes()
364
    {
365
        return [
366
            '401' => [
367
                'message' => 'Authentication Failed: Invalid Email or Password',
368
                'throwable' => AuthenticationIncorrectParameters::class,
369
            ],
370
            '402' => [
371
                'message' => 'Authentication Failed: Invalid Hash, App ID or App Password',
372
                'throwable' => AuthenticationIncorrectParameters::class,
373
            ],
374
            '403' => [
375
                'message' => 'User doesn\'t have the correct permissions to use the API',
376
                'throwable' => InsufficientPermissionException::class,
377
            ],
378
            '404' => [
379
                'message' => 'Unknown error whilst authenticating',
380
                'throwable' => UnionCloudResponseAuthenticationException::class
381
            ],
382
            '405' => [
383
                'message' => 'Parameters missing from the authentication request',
384
                'throwable' => AuthenticationParameterMissing::class
385
            ],
386
        ];
387
    }
388
389
    /**
390
     * Get the auth token from the API response from UnionCloud
391
     *
392
     * This will also process the expiry and union ID, and save
393
     * them within the Authenticator for later use.
394
     *
395
     * @param ResponseInterface $response
396
     * @throws UnionCloudResponseAuthenticationException
397
     */
398
    private function extractAuthToken(ResponseInterface $response)
399
    {
400
        // Get the message body
401
        try {
402
            $responseBody = json_decode($response->getBody()->getContents(), true);
403
        } catch (\Exception $decodeError) {
404
            throw new UnionCloudResponseAuthenticationException('Invalid response body returned in Authentication', 500, $decodeError);
405
        }
406
407
        try {
408
            $this->authToken = $responseBody['response']['auth_token'];
409
            $this->expires = Carbon::now()->addSeconds($responseBody['response']['expires']);
410
            $this->union_id = $responseBody['response']['union_id'];
411
        } catch (\Exception $e) {
412
            throw new UnionCloudResponseAuthenticationException('Couldn\'t find required Auth Token parameters in the response', 500, $e);
413
        }
414
    }
415
416
}