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 | } |