1<?php
2/**
3 * Copyright 2017 Facebook, Inc.
4 *
5 * You are hereby granted a non-exclusive, worldwide, royalty-free license to
6 * use, copy, modify, and distribute this software in source code or binary
7 * form for use in connection with the web services and APIs provided by
8 * Facebook.
9 *
10 * As with any software that integrates with the Facebook platform, your use
11 * of this software is subject to the Facebook Developer Principles and
12 * Policies [http://developers.facebook.com/policy/]. This copyright notice
13 * shall be included in all copies or substantial portions of the software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 * DEALINGS IN THE SOFTWARE.
22 *
23 */
24namespace Facebook\Authentication;
25
26use Facebook\Facebook;
27use Facebook\FacebookApp;
28use Facebook\FacebookRequest;
29use Facebook\FacebookResponse;
30use Facebook\FacebookClient;
31use Facebook\Exceptions\FacebookResponseException;
32use Facebook\Exceptions\FacebookSDKException;
33
34/**
35 * Class OAuth2Client
36 *
37 * @package Facebook
38 */
39class OAuth2Client
40{
41    /**
42     * @const string The base authorization URL.
43     */
44    const BASE_AUTHORIZATION_URL = 'https://www.facebook.com';
45
46    /**
47     * The FacebookApp entity.
48     *
49     * @var FacebookApp
50     */
51    protected $app;
52
53    /**
54     * The Facebook client.
55     *
56     * @var FacebookClient
57     */
58    protected $client;
59
60    /**
61     * The version of the Graph API to use.
62     *
63     * @var string
64     */
65    protected $graphVersion;
66
67    /**
68     * The last request sent to Graph.
69     *
70     * @var FacebookRequest|null
71     */
72    protected $lastRequest;
73
74    /**
75     * @param FacebookApp    $app
76     * @param FacebookClient $client
77     * @param string|null    $graphVersion The version of the Graph API to use.
78     */
79    public function __construct(FacebookApp $app, FacebookClient $client, $graphVersion = null)
80    {
81        $this->app = $app;
82        $this->client = $client;
83        $this->graphVersion = $graphVersion ?: Facebook::DEFAULT_GRAPH_VERSION;
84    }
85
86    /**
87     * Returns the last FacebookRequest that was sent.
88     * Useful for debugging and testing.
89     *
90     * @return FacebookRequest|null
91     */
92    public function getLastRequest()
93    {
94        return $this->lastRequest;
95    }
96
97    /**
98     * Get the metadata associated with the access token.
99     *
100     * @param AccessToken|string $accessToken The access token to debug.
101     *
102     * @return AccessTokenMetadata
103     */
104    public function debugToken($accessToken)
105    {
106        $accessToken = $accessToken instanceof AccessToken ? $accessToken->getValue() : $accessToken;
107        $params = ['input_token' => $accessToken];
108
109        $this->lastRequest = new FacebookRequest(
110            $this->app,
111            $this->app->getAccessToken(),
112            'GET',
113            '/debug_token',
114            $params,
115            null,
116            $this->graphVersion
117        );
118        $response = $this->client->sendRequest($this->lastRequest);
119        $metadata = $response->getDecodedBody();
120
121        return new AccessTokenMetadata($metadata);
122    }
123
124    /**
125     * Generates an authorization URL to begin the process of authenticating a user.
126     *
127     * @param string $redirectUrl The callback URL to redirect to.
128     * @param string $state       The CSPRNG-generated CSRF value.
129     * @param array  $scope       An array of permissions to request.
130     * @param array  $params      An array of parameters to generate URL.
131     * @param string $separator   The separator to use in http_build_query().
132     *
133     * @return string
134     */
135    public function getAuthorizationUrl($redirectUrl, $state, array $scope = [], array $params = [], $separator = '&')
136    {
137        $params += [
138            'client_id' => $this->app->getId(),
139            'state' => $state,
140            'response_type' => 'code',
141            'sdk' => 'php-sdk-' . Facebook::VERSION,
142            'redirect_uri' => $redirectUrl,
143            'scope' => implode(',', $scope)
144        ];
145
146        return static::BASE_AUTHORIZATION_URL . '/' . $this->graphVersion . '/dialog/oauth?' . http_build_query($params, null, $separator);
147    }
148
149    /**
150     * Get a valid access token from a code.
151     *
152     * @param string $code
153     * @param string $redirectUri
154     *
155     * @return AccessToken
156     *
157     * @throws FacebookSDKException
158     */
159    public function getAccessTokenFromCode($code, $redirectUri = '')
160    {
161        $params = [
162            'code' => $code,
163            'redirect_uri' => $redirectUri,
164        ];
165
166        return $this->requestAnAccessToken($params);
167    }
168
169    /**
170     * Exchanges a short-lived access token with a long-lived access token.
171     *
172     * @param AccessToken|string $accessToken
173     *
174     * @return AccessToken
175     *
176     * @throws FacebookSDKException
177     */
178    public function getLongLivedAccessToken($accessToken)
179    {
180        $accessToken = $accessToken instanceof AccessToken ? $accessToken->getValue() : $accessToken;
181        $params = [
182            'grant_type' => 'fb_exchange_token',
183            'fb_exchange_token' => $accessToken,
184        ];
185
186        return $this->requestAnAccessToken($params);
187    }
188
189    /**
190     * Get a valid code from an access token.
191     *
192     * @param AccessToken|string $accessToken
193     * @param string             $redirectUri
194     *
195     * @return AccessToken
196     *
197     * @throws FacebookSDKException
198     */
199    public function getCodeFromLongLivedAccessToken($accessToken, $redirectUri = '')
200    {
201        $params = [
202            'redirect_uri' => $redirectUri,
203        ];
204
205        $response = $this->sendRequestWithClientParams('/oauth/client_code', $params, $accessToken);
206        $data = $response->getDecodedBody();
207
208        if (!isset($data['code'])) {
209            throw new FacebookSDKException('Code was not returned from Graph.', 401);
210        }
211
212        return $data['code'];
213    }
214
215    /**
216     * Send a request to the OAuth endpoint.
217     *
218     * @param array $params
219     *
220     * @return AccessToken
221     *
222     * @throws FacebookSDKException
223     */
224    protected function requestAnAccessToken(array $params)
225    {
226        $response = $this->sendRequestWithClientParams('/oauth/access_token', $params);
227        $data = $response->getDecodedBody();
228
229        if (!isset($data['access_token'])) {
230            throw new FacebookSDKException('Access token was not returned from Graph.', 401);
231        }
232
233        // Graph returns two different key names for expiration time
234        // on the same endpoint. Doh! :/
235        $expiresAt = 0;
236        if (isset($data['expires'])) {
237            // For exchanging a short lived token with a long lived token.
238            // The expiration time in seconds will be returned as "expires".
239            $expiresAt = time() + $data['expires'];
240        } elseif (isset($data['expires_in'])) {
241            // For exchanging a code for a short lived access token.
242            // The expiration time in seconds will be returned as "expires_in".
243            // See: https://developers.facebook.com/docs/facebook-login/access-tokens#long-via-code
244            $expiresAt = time() + $data['expires_in'];
245        }
246
247        return new AccessToken($data['access_token'], $expiresAt);
248    }
249
250    /**
251     * Send a request to Graph with an app access token.
252     *
253     * @param string                  $endpoint
254     * @param array                   $params
255     * @param AccessToken|string|null $accessToken
256     *
257     * @return FacebookResponse
258     *
259     * @throws FacebookResponseException
260     */
261    protected function sendRequestWithClientParams($endpoint, array $params, $accessToken = null)
262    {
263        $params += $this->getClientParams();
264
265        $accessToken = $accessToken ?: $this->app->getAccessToken();
266
267        $this->lastRequest = new FacebookRequest(
268            $this->app,
269            $accessToken,
270            'GET',
271            $endpoint,
272            $params,
273            null,
274            $this->graphVersion
275        );
276
277        return $this->client->sendRequest($this->lastRequest);
278    }
279
280    /**
281     * Returns the client_* params for OAuth requests.
282     *
283     * @return array
284     */
285    protected function getClientParams()
286    {
287        return [
288            'client_id' => $this->app->getId(),
289            'client_secret' => $this->app->getSecret(),
290        ];
291    }
292}
293