Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
80 / 80 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
1 / 1 |
AbstractOAuth1Provider | |
100.00% |
80 / 80 |
|
100.00% |
7 / 7 |
22 | |
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% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
generateOAuthHeader | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
5 | |||
getProviderState | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setProviderState | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
authenticate | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
8 | |||
requestRequestToken | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
requestAccessToken | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace Miniframe\SocialLogin\Provider; |
4 | |
5 | use Miniframe\Core\Config; |
6 | use Miniframe\Core\Registry; |
7 | use Miniframe\Core\Request; |
8 | use Miniframe\Middleware\Session; |
9 | use Miniframe\Response\RedirectResponse; |
10 | use Miniframe\SocialLogin\Middleware\SocialLogin; |
11 | use Miniframe\SocialLogin\Model\User; |
12 | |
13 | abstract 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 | } |