Completed
Push — master ( d7683b...50beb4 )
by Michał
02:17
created

Provider   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 186
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 15
lcom 1
cbo 6
dl 0
loc 186
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A authorize() 0 21 1
A exchange() 0 14 1
A identify() 0 6 1
A request() 0 10 1
A onRequestSuccess() 0 4 1
A onRequestError() 0 4 1
A getDefaultRequestOptions() 0 15 2
B createToken() 0 27 6
A createIdentity() 0 6 1
1
<?php namespace nyx\auth\id\protocols\oauth2;
2
3
// External dependencies
4
use Psr\Http\Message\ResponseInterface as Response;
5
use GuzzleHttp\Promise\PromiseInterface as Promise;
6
use nyx\utils;
7
8
// Internal dependencies
9
use nyx\auth\id\protocols\oauth2;
10
use nyx\auth;
11
12
/**
13
 * OAuth 2.0 Provider
14
 *
15
 * @package     Nyx\Auth
16
 * @version     0.1.0
17
 * @author      Michal Chojnacki <[email protected]>
18
 * @copyright   2012-2017 Nyx Dev Team
19
 * @link        https://github.com/unyx/nyx
20
 */
21
abstract class Provider extends auth\id\Provider implements interfaces\Provider
22
{
23
    /**
24
     * The character separating different scopes in the request.
25
     */
26
    const SCOPE_SEPARATOR = ',';
27
28
    /**
29
     * @var array   The default access scopes to be requested during the authorization step.
30
     */
31
    protected $defaultScopes = [];
32
33
    /**
34
     * {@inheritDoc}
35
     */
36
    public function authorize(callable $redirect, array $parameters = [])
37
    {
38
        $parameters += [
39
            'scope'         => implode(static::SCOPE_SEPARATOR, $this->defaultScopes),
40
            'response_type' => 'code'
41
        ];
42
43
        // The explicitly set Client Credentials will always overwrite the keys' values from the optional
44
        // $parameters if they are present. If you want to use other credentials, either use the getAuthorizeUrl()
45
        // method directly or instantiate a Provider with different consumer credentials.
46
        $parameters['client_id']    = $this->consumer->getId();
47
        $parameters['redirect_uri'] = $this->consumer->getRedirectUri();
48
49
        // Only doing an isset here - can't easily predict valid values nor force properly randomized values.
50
        // Invalid requests will be rejected by the endpoint, after all.
51
        $parameters['state'] = $parameters['state'] ?? utils\Random::string(16);
52
53
        // The state gets passed along explicitly as the second argument since it will *always* need to be persisted
54
        // in some way by the end-user until it can be discarded after a successful exchange.
55
        return $redirect($this->getAuthorizeUrl($parameters), $parameters['state'], $parameters);
56
    }
57
58
    /**
59
     * Exchanges the given authorization code grant for an Access Token.
60
     *
61
     * @param   string  $code       The authorization code to exchange.
62
     * @return  Promise             A Promise for an Access Token.
63
     */
64
    public function exchange(string $code) : Promise
65
    {
66
        return $this->request('POST', $this->getExchangeUrl(), null, [
67
            'form_params' => [
68
                'client_id'     => $this->consumer->getId(),
69
                'client_secret' => $this->consumer->getSecret(),
70
                'redirect_uri'  => $this->consumer->getRedirectUri(),
71
                'grant_type'    => 'authorization_code',
72
                'code'          => $code
73
            ]
74
        ])->then(function (array $data) {
75
            return $this->createToken($data);
76
        });
77
    }
78
79
    /**
80
     * {@inheritDoc}
81
     */
82
    public function identify(oauth2\Token $token) : Promise
83
    {
84
        return $this->request('GET', $this->getIdentifyUrl(), $token)->then(function (array $data) use ($token) {
85
            return $this->createIdentity($token, $data);
86
        });
87
    }
88
89
    /**
90
     * {@inheritDoc}
91
     *
92
     * @param   oauth2\Token    $token
93
     */
94
    public function request(string $method, string $url, auth\interfaces\Token $token = null, array $options = []) : Promise
95
    {
96
        return $this->getHttpClient()->requestAsync($method, $url, array_merge_recursive($this->getDefaultRequestOptions($token), $options))->then(
97
            function (Response $response) use($token) {
98
                return $this->onRequestSuccess($response, $token);
99
            },
100
            function(\Exception $exception) use($token) {
101
                return $this->onRequestError($exception, $token);
102
            });
103
    }
104
105
    /**
106
     * Success callback for self::request().
107
     *
108
     * Note: Some Providers may return valid HTTP response codes for requests that were actually unsuccessful
109
     * and instead provide arbitrary responses like "ok => false" or "errors => []". Those misbehaving cases should
110
     * be caught within specific Providers.
111
     *
112
     * @param   Response                $response   The Response received to the Request made.
113
     * @param   auth\interfaces\Token   $token      The Access Token that was used to authorize the Request, if applicable.
114
     * @return  mixed
115
     */
116
    protected function onRequestSuccess(Response $response, auth\interfaces\Token $token = null)
117
    {
118
        return json_decode($response->getBody(), true);
119
    }
120
121
    /**
122
     * Failure callback for self::request().
123
     *
124
     * @param   \Exception              $exception  The Exception that occurred during the Request.
125
     * @param   auth\interfaces\Token   $token      The Access Token that was used to authorize the Request, if applicable.
126
     * @throws  \Exception                          Always re-throws the Exception. Child classes may, however, provide
127
     *                                              recovery paths.
128
     * @return  mixed
129
     */
130
    protected function onRequestError(\Exception $exception, auth\interfaces\Token $token = null)
0 ignored issues
show
Unused Code introduced by
The parameter $token is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
131
    {
132
        throw $exception;
133
    }
134
135
    /**
136
     * Returns the default options (in a format recognized by Guzzle) for requests made by this Provider.
137
     *
138
     * @param   auth\interfaces\Token   $token      The Access Token that should be used to authorize the Request.
139
     * @return  array
140
     */
141
    protected function getDefaultRequestOptions(auth\interfaces\Token $token = null) : array
142
    {
143
        $options = [
144
            'headers' => [
145
                'Accept' => 'application/json'
146
            ]
147
        ];
148
149
        // If the $token is explicitly given, it will override the respective authorization header.
150
        if (isset($token)) {
151
            $options['headers']['Authorization'] = 'Bearer '.$token;
152
        }
153
154
        return $options;
155
    }
156
157
    /**
158
     * Creates an OAuth 2.0 Access Token instance based on raw response data.
159
     *
160
     * @param   array           $data   The raw (response) data to base on.
161
     * @return  oauth2\Token            The resulting OAuth 2.0 Access Token instance.
162
     * @throws  \RuntimeException       When the data did not contain an access token in a recognized format.
163
     */
164
    protected function createToken(array $data) : oauth2\Token
165
    {
166
        // The HTTP Client will throw on an unsuccessful response, but we'll double check that we actually got
167
        // an access token in response.
168
        if (empty($data['access_token'])) {
169
            throw new \RuntimeException('The Provider did not return an access token or it was in an unrecognized format.');
170
        }
171
172
        $token = new oauth2\Token($data['access_token']);
173
174
        if (!empty($data['refresh_token'])) {
175
            $token->setRefreshToken($data['refresh_token']);
176
        }
177
178
        if (!empty($data['expires_in'])) {
179
            $token->setExpiry($data['expires_in']);
180
        }
181
182
        // Some providers, like Github or Slack, return the granted scopes along with the Tokens. Let's make
183
        // use of that in the base class since an isset isn't exactly expensive and if other providers happen
184
        // to return the scopes under a different key, child classes can just remap the value.
185
        if (isset($data['scope'])) {
186
            $token->setScopes(is_array($data['scope']) ? $data['scope'] : explode(static::SCOPE_SEPARATOR, $data['scope']));
187
        }
188
189
        return $token;
190
    }
191
192
    /**
193
     * Creates an Identity instance of a type specific to the Provider, using an Access Token and raw data also
194
     * specific to the Provider.
195
     *
196
     * @param   oauth2\Token    $token  The Access Token that had been used to retrieve information about the entity.
197
     * @param   array           $data   The raw data about the entity given by the Provider.
198
     * @return  oauth2\Identity         The resulting OAuth 2.0 Identity instance.
199
     */
200
    protected function createIdentity(oauth2\Token $token, array $data) : oauth2\Identity
201
    {
202
        $class = static::IDENTITY;
203
204
        return new $class($token, $data);
205
    }
206
}
207