Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
AbstractOAuth1Provider
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
7 / 7
22
100.00% covered (success)
100.00%
1 / 1
 getRequestTokenUrl
n/a
0 / 0
n/a
0 / 0
0
 getAuthorizeUrl
n/a
0 / 0
n/a
0 / 0
0
 getAccessTokenUrl
n/a
0 / 0
n/a
0 / 0
0
 getUserProfile
n/a
0 / 0
n/a
0 / 0
0
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 generateOAuthHeader
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
5
 getProviderState
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setProviderState
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 authenticate
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
8
 requestRequestToken
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 requestAccessToken
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace Miniframe\SocialLogin\Provider;
4
5use Miniframe\Core\Config;
6use Miniframe\Core\Registry;
7use Miniframe\Core\Request;
8use Miniframe\Middleware\Session;
9use Miniframe\Response\RedirectResponse;
10use Miniframe\SocialLogin\Middleware\SocialLogin;
11use Miniframe\SocialLogin\Model\User;
12
13abstract class AbstractOAuth1Provider extends AbstractOAuthProvider
14{
15    /**
16     * Returns the request token URL
17     *
18     * @return string
19     */
20    abstract protected function getRequestTokenUrl(): string;
21
22    /**
23     * Returns the authorize URL
24     *
25     * @return string
26     */
27    abstract protected function getAuthorizeUrl(): string;
28
29    /**
30     * Returns the access token URL
31     *
32     * @return string
33     */
34    abstract protected function getAccessTokenUrl(): string;
35    /**
36     * Returns the user profile
37     *
38     * @param array $accessToken The access token.
39     *
40     * @return User
41     */
42    abstract protected function getUserProfile(array $accessToken): User;
43
44    /**
45     * The OAuth consumer key
46     *
47     * @var string
48     */
49    protected $oauthConsumerKey;
50    /**
51     * The OAuth consumer secret
52     *
53     * @var mixed
54     */
55    protected $oauthConsumerSecret;
56    /**
57     * Reference to the SocialLogin middleware
58     *
59     * @var SocialLogin
60     */
61    protected $socialLogin;
62
63    /**
64     * Reference to the Session object.
65     *
66     * @var Session
67     */
68    protected $session;
69
70    /**
71     * Creates a new OAuth 1 provider
72     *
73     * @param Request $request Reference to the Request object.
74     * @param Config  $config  Reference to the Config object.
75     */
76    public function __construct(Request $request, Config $config)
77    {
78        parent::__construct($request, $config);
79
80        // Get config
81        $this->oauthConsumerKey = $config->get('sociallogin-' . $this->shortName, 'consumer_key');
82        $this->oauthConsumerSecret = $config->get('sociallogin-' . $this->shortName, 'consumer_secret');
83
84        // Fetch some required middlewares
85        $this->session = Registry::get(Session::class);
86        $this->socialLogin = Registry::get(SocialLogin::class);
87    }
88
89    /**
90     * Generates a signature
91     *
92     * @param string      $baseUrl           The base request URL.
93     * @param string      $requestMethod     GET or POST.
94     * @param array       $requestParameters The request parameters.
95     * @param string|null $accessTokenSecret The access token secret (optional).
96     *
97     * @see https://developer.twitter.com/en/docs/authentication/oauth-1-0a/creating-a-signature
98     *
99     * @return string
100     */
101    protected function generateOAuthHeader(
102        string $baseUrl,
103        string $requestMethod = 'POST',
104        array $requestParameters = array(),
105        string $accessTokenSecret = null
106    ): string {
107        $oauthParameters = [
108            'oauth_nonce' => md5(time() . __FILE__ . rand(1000, 9999)),
109            'oauth_signature_method' => 'HMAC-SHA1',
110            'oauth_timestamp' => time(),
111            'oauth_consumer_key' => $this->oauthConsumerKey,
112            'oauth_version' => '1.0',
113        ];
114
115        foreach ($requestParameters as $key => $value) {
116            if (substr($key, 0, 6) !== 'oauth_') {
117                continue;
118            }
119            $oauthParameters[$key] = $value;
120        }
121
122        // Take the request parameters and add OAuth parameters
123        $parameters = array_merge($oauthParameters, $requestParameters);
124
125        // Sort by key
126        ksort($parameters);
127
128        // Create parameter string
129        $parameterString = '';
130        foreach ($parameters as $key => $value) {
131            $parameterString .= '&' . rawurlencode($key) . '=' . rawurlencode($value);
132        }
133
134        // Create base string and signing key
135        $signatureBaseString = strtoupper($requestMethod) . '&' . rawurlencode($baseUrl) . '&'
136            . rawurlencode(substr($parameterString, 1));
137        $signingKey = rawurlencode($this->oauthConsumerSecret) . '&' . rawurlencode($accessTokenSecret ?? '');
138
139        // Add signature to parameters
140        $oauthParameters['oauth_signature'] = base64_encode(hash_hmac('sha1', $signatureBaseString, $signingKey, true));
141
142        $header = '';
143        foreach ($oauthParameters as $key => $value) {
144            $header .= ', ' . rawurlencode($key) . '="' . rawurlencode($value) . '"';
145        }
146
147        return 'Authorization: OAuth ' . substr($header, 2);
148    }
149
150    /**
151     * Fetches the provider state from the session.
152     *
153     * @return array
154     */
155    protected function getProviderState(): array
156    {
157        $session = $this->session->get('_SOCIALLOGIN');
158        return $session[get_called_class()] ?? [];
159    }
160
161    /**
162     * Updates the provider state in the session.
163     *
164     * @param array $state The provider state.
165     *
166     * @return void
167     */
168    protected function setProviderState(array $state): void
169    {
170        $session = $this->session->get('_SOCIALLOGIN');
171        $session[get_called_class()] = $state;
172        $this->session->set('_SOCIALLOGIN', $session);
173    }
174
175    /**
176     * Starts the authentication process
177     *
178     * @return User
179     */
180    public function authenticate(): User
181    {
182        // Fetch the provider state
183        $providerState = $this->getProviderState();
184
185        // Store the user state, since redirecting with OAuth 1.0 will result in losing this state.
186        if ($this->request->getRequest('state')) {
187            $providerState['userState'] = $this->request->getRequest('state');
188        } elseif ($this->request->getPost('state')) {
189            $providerState['userState'] = $this->request->getPost('state');
190        }
191        $this->setProviderState($providerState);
192
193        // Returned after authorization, does the token match with the one saved in the providerState?
194        if (
195            isset($providerState['requestOAuthToken'])
196            && $providerState['requestOAuthToken'] === $this->request->getRequest('oauth_token')
197            && $this->request->getRequest('oauth_verifier')
198        ) {
199            // Resets the state
200            unset($providerState['requestOAuthToken']);
201            unset($providerState['requestOAuthTokenSecret']);
202            $this->setProviderState($providerState);
203
204            // Requesting an access token, fetches the user profile and logs in
205            $accessToken = $this->requestAccessToken();
206            $user = $this->getUserProfile($accessToken);
207
208            // When a redirect URL is in the state, we need to manually login and force a redirect since the state has
209            // been lost in the OAuth redirect.
210            $userState = SocialLogin::parseState($providerState['userState'] ?? null);
211            if (empty($userState['redirectUrl'])) {
212                $this->setProviderState([]);
213                return $user;
214            } else {
215                $redirectUrl = $userState['redirectUrl'] ?? $this->baseHref;
216                $providerState['userState'] = null;
217                $this->setProviderState([]);
218                $this->socialLogin->login($user);
219                throw new RedirectResponse($redirectUrl);
220            }
221        }
222
223        // No valid request token known at this point.
224        // Requesting a request token
225        $token = $this->requestRequestToken();
226
227        // Store request token in state
228        $providerState['requestOAuthToken'] = $token['oauth_token'];
229        $providerState['requestOAuthTokenSecret'] = $token['oauth_token_secret'];
230        $this->setProviderState($providerState);
231
232        // Redirect to authorize URL
233        $authorizeUrl = $this->getAuthorizeUrl();
234        $authorizeUrl .= (strpos($authorizeUrl, '?') === false ? '?' : '&');
235        $authorizeUrl .= 'oauth_token=' . rawurlencode($token['oauth_token']);
236        throw new RedirectResponse($authorizeUrl);
237    }
238
239    /**
240     * Requests a Request token and returns an array with keys 'oauth_token' and 'oauth_token_secret'
241     *
242     * @return string[]
243     */
244    protected function requestRequestToken(): array
245    {
246        // Compiles the request
247        $data = ['oauth_callback' => $this->getCurrentUri()];
248        $requestMethod = 'POST';
249        $url = $this->getRequestTokenUrl();
250        $headers = [$this->generateOAuthHeader($url, 'POST', $data)];
251
252        // Requests the token
253        $result = $this->curlRequest($url, $requestMethod, $data, $headers);
254        // Validates the result
255        foreach (['oauth_token', 'oauth_token_secret'] as $key) {
256            if (!isset($result[$key])) {
257                throw new \RuntimeException('Request Token failed: No ' . $key . ' returned');
258            }
259        }
260        return $result;
261    }
262
263    /**
264     * Request an Access token and returns an array with keys 'oauth_token' and 'oauth_token_secret'
265     *
266     * @return string[]
267     */
268    protected function requestAccessToken(): array
269    {
270        // Compiles the request
271        $data = [
272            'oauth_token' => $this->request->getRequest('oauth_token'),
273            'oauth_verifier' => $this->request->getRequest('oauth_verifier')
274        ];
275        $requestMethod = 'POST';
276        $url = $this->getAccessTokenUrl();
277        $headers = [$this->generateOAuthHeader($url, 'POST', $data)];
278
279        // Requests the token
280        $result = $this->curlRequest($url, $requestMethod, $data, $headers);
281        // Validates the result
282        foreach (['oauth_token', 'oauth_token_secret'] as $key) {
283            if (!isset($result[$key])) {
284                throw new \RuntimeException('Request Token failed: No ' . $key . ' returned');
285            }
286        }
287        return $result;
288    }
289}