Api.php (8279B)
1 <?php 2 3 /** 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU Lesser General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU Lesser General Public License for more details. 13 * 14 * You should have received a copy of the GNU Lesser General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 namespace fkooman\OAuth\Client; 19 20 use fkooman\OAuth\Client\Exception\ApiException; 21 22 /** 23 * API for talking to OAuth 2.0 protected resources. 24 * 25 * @author François Kooman <fkooman@tuxed.net> 26 */ 27 class Api 28 { 29 const RANDOM_LENGTH = 8; 30 31 private $clientConfigId; 32 private $clientConfig; 33 private $tokenStorage; 34 private $httpClient; 35 36 public function __construct($clientConfigId, ClientConfigInterface $clientConfig, StorageInterface $tokenStorage, \Guzzle\Http\Client $httpClient) 37 { 38 $this->setClientConfigId($clientConfigId); 39 $this->setClientConfig($clientConfig); 40 $this->setTokenStorage($tokenStorage); 41 $this->setHttpClient($httpClient); 42 } 43 44 public function setClientConfigId($clientConfigId) 45 { 46 if (!is_string($clientConfigId) || 0 >= strlen($clientConfigId)) { 47 throw new ApiException("clientConfigId must be a non-empty string"); 48 } 49 $this->clientConfigId = $clientConfigId; 50 } 51 52 public function setClientConfig(ClientConfigInterface $clientConfig) 53 { 54 $this->clientConfig = $clientConfig; 55 } 56 57 public function setTokenStorage(StorageInterface $tokenStorage) 58 { 59 $this->tokenStorage = $tokenStorage; 60 } 61 62 public function setHttpClient(\Guzzle\Http\Client $httpClient) 63 { 64 $this->httpClient = $httpClient; 65 } 66 67 public function getRefreshToken(Context $context) 68 { 69 return $this->tokenStorage->getRefreshToken($this->clientConfigId, $context); 70 } 71 72 public function getAccessToken(Context $context) 73 { 74 // do we have a valid access token? 75 $accessToken = $this->tokenStorage->getAccessToken($this->clientConfigId, $context); 76 if (false !== $accessToken) { 77 if (null === $accessToken->getExpiresIn()) { 78 // no expiry set, assume always valid 79 return $accessToken; 80 } 81 // check if expired 82 if (time() < $accessToken->getIssueTime() + $accessToken->getExpiresIn()) { 83 // not expired 84 return $accessToken; 85 } 86 // expired, delete it and continue 87 $this->tokenStorage->deleteAccessToken($accessToken); 88 } 89 90 // no valid access token, is there a refresh_token? 91 $refreshToken = $this->getRefreshToken($context); 92 if (false !== $refreshToken) { 93 // obtain a new access token with refresh token 94 $tokenRequest = new TokenRequest($this->httpClient, $this->clientConfig); 95 $tokenResponse = $tokenRequest->withRefreshToken($refreshToken->getRefreshToken()); 96 if (false === $tokenResponse) { 97 // unable to fetch with RefreshToken, delete it 98 $this->tokenStorage->deleteRefreshToken($refreshToken); 99 100 return false; 101 } 102 103 if (null === $tokenResponse->getScope()) { 104 // no scope in response, we assume we got the requested scope 105 $scope = $context->getScope(); 106 } else { 107 // the scope we got should be a superset of what we requested 108 $scope = $tokenResponse->getScope(); 109 if (!$scope->hasScope($context->getScope())) { 110 // we didn't get the scope we requested, stop for now 111 // FIXME: we need to implement a way to request certain 112 // scope as being optional, while others need to be 113 // required 114 throw new ApiException("requested scope not obtained"); 115 } 116 } 117 118 $accessToken = new AccessToken( 119 array( 120 "client_config_id" => $this->clientConfigId, 121 "user_id" => $context->getUserId(), 122 "scope" => $scope, 123 "access_token" => $tokenResponse->getAccessToken(), 124 "token_type" => $tokenResponse->getTokenType(), 125 "issue_time" => time(), 126 "expires_in" => $tokenResponse->getExpiresIn(), 127 ) 128 ); 129 $this->tokenStorage->storeAccessToken($accessToken); 130 if (null !== $tokenResponse->getRefreshToken()) { 131 // delete the existing refresh token as we'll store a new one 132 $this->tokenStorage->deleteRefreshToken($refreshToken); 133 $refreshToken = new RefreshToken( 134 array( 135 "client_config_id" => $this->clientConfigId, 136 "user_id" => $context->getUserId(), 137 "scope" => $scope, 138 "refresh_token" => $tokenResponse->getRefreshToken(), 139 "issue_time" => time(), 140 ) 141 ); 142 $this->tokenStorage->storeRefreshToken($refreshToken); 143 } 144 145 return $accessToken; 146 } 147 // no access token, and refresh token didn't work either or was not there, probably the tokens were revoked 148 return false; 149 } 150 151 public function deleteAccessToken(Context $context) 152 { 153 $accessToken = $this->getAccessToken($context); 154 if (false !== $accessToken) { 155 $this->tokenStorage->deleteAccessToken($accessToken); 156 } 157 } 158 159 public function deleteRefreshToken(Context $context) 160 { 161 $refreshToken = $this->getRefreshToken($context); 162 if (false !== $refreshToken) { 163 $this->tokenStorage->deleteRefreshToken($refreshToken); 164 } 165 } 166 167 public function getAuthorizeUri(Context $context, $stateValue = null) 168 { 169 // allow caller to override a random generated state 170 // FIXME: is this actually used anywhere? 171 if (null === $stateValue) { 172 $stateValue = bin2hex(openssl_random_pseudo_bytes(self::RANDOM_LENGTH)); 173 } else { 174 if (!is_string($stateValue) || 0 >= strlen($stateValue)) { 175 throw new ApiException("state must be a non-empty string"); 176 } 177 } 178 179 // try to get a new access token 180 $this->tokenStorage->deleteStateForContext($this->clientConfigId, $context); 181 $state = new State( 182 array( 183 "client_config_id" => $this->clientConfigId, 184 "user_id" => $context->getUserId(), 185 "scope" => $context->getScope(), 186 "issue_time" => time(), 187 "state" => $stateValue, 188 ) 189 ); 190 if (false === $this->tokenStorage->storeState($state)) { 191 throw new ApiException("unable to store state"); 192 } 193 194 $q = array ( 195 "client_id" => $this->clientConfig->getClientId(), 196 "response_type" => "code", 197 "state" => $state->getState(), 198 ); 199 200 // scope 201 $contextScope = $context->getScope(); 202 if (!$contextScope->isEmpty()) { 203 if ($this->clientConfig->getUseCommaSeparatedScope()) { 204 $q['scope'] = $contextScope->toString(","); 205 } else { 206 $q['scope'] = $contextScope->toString(); 207 } 208 } 209 210 // redirect_uri 211 if ($this->clientConfig->getRedirectUri()) { 212 $q['redirect_uri'] = $this->clientConfig->getRedirectUri(); 213 } 214 215 $separator = (false === strpos($this->clientConfig->getAuthorizeEndpoint(), "?")) ? "?" : "&"; 216 $authorizeUri = $this->clientConfig->getAuthorizeEndpoint().$separator.http_build_query($q, null, '&'); 217 218 return $authorizeUri; 219 } 220 }