Campustream 1.0
A social network MQP for WPI
|
00001 <?php 00009 ini_set('display_errors', 1); 00010 error_reporting(E_ALL | E_STRICT); 00011 00012 // Regex to filter out the client identifier 00013 // (described in Section 2 of IETF draft) 00014 // IETF draft does not prescribe a format for these, however 00015 // I've arbitrarily chosen alphanumeric strings with hyphens and underscores, 3-12 characters long 00016 // Feel free to change. 00017 define("REGEX_CLIENT_ID", "/^[a-z0-9-_]{3,12}$/i"); 00018 00019 // Used to define the name of the OAuth access token parameter (POST/GET/etc.) 00020 // IETF Draft sections 5.2 and 5.3 specify that it should be called "oauth_token" 00021 // but other implementations use things like "access_token" 00022 // I won't be heartbroken if you change it, but it might be better to adhere to the spec 00023 define("OAUTH_TOKEN_PARAM_NAME", "oauth_token"); 00024 00025 // Client types (for client authorization) 00026 //define("WEB_SERVER_CLIENT_TYPE", "web_server"); 00027 //define("USER_AGENT_CLIENT_TYPE", "user_agent"); 00028 //define("REGEX_CLIENT_TYPE", "/^(web_server|user_agent)$/"); 00029 define("ACCESS_TOKEN_AUTH_RESPONSE_TYPE", "token"); 00030 define("AUTH_CODE_AUTH_RESPONSE_TYPE", "code"); 00031 define("CODE_AND_TOKEN_AUTH_RESPONSE_TYPE", "code-and-token"); 00032 define("REGEX_AUTH_RESPONSE_TYPE", "/^(token|code|code-and-token)$/"); 00033 00034 // Grant Types (for token obtaining) 00035 define("AUTH_CODE_GRANT_TYPE", "authorization_code"); 00036 define("USER_CREDENTIALS_GRANT_TYPE", "password"); 00037 define("ASSERTION_GRANT_TYPE", "assertion"); 00038 define("REFRESH_TOKEN_GRANT_TYPE", "refresh_token"); 00039 define("NONE_GRANT_TYPE", "none"); 00040 define("REGEX_TOKEN_GRANT_TYPE", "/^(authorization_code|password|assertion|refresh_token|none)$/"); 00041 00042 /* Error handling constants */ 00043 00044 // HTTP status codes 00045 define("ERROR_NOT_FOUND", "404 Not Found"); 00046 define("ERROR_BAD_REQUEST", "400 Bad Request"); 00047 00048 // TODO: Extend for i18n 00049 00050 // "Official" OAuth 2.0 errors 00051 define("ERROR_REDIRECT_URI_MISMATCH", "redirect_uri_mismatch"); 00052 define("ERROR_INVALID_CLIENT_CREDENTIALS", "invalid_client_credentials"); 00053 define("ERROR_UNAUTHORIZED_CLIENT", "unauthorized_client"); 00054 define("ERROR_USER_DENIED", "access_denied"); 00055 define("ERROR_INVALID_REQUEST", "invalid_request"); 00056 define("ERROR_INVALID_CLIENT_ID", "invalid_client_id"); 00057 define("ERROR_UNSUPPORTED_RESPONSE_TYPE", "unsupported_response_type"); 00058 define("ERROR_INVALID_SCOPE", "invalid_scope"); 00059 define("ERROR_INVALID_GRANT", "invalid_grant"); 00060 00061 // Protected resource errors 00062 define("ERROR_INVALID_TOKEN", "invalid_token"); 00063 define("ERROR_EXPIRED_TOKEN", "expired_token"); 00064 define("ERROR_INSUFFICIENT_SCOPE", "insufficient_scope"); 00065 00066 // Messages 00067 define("ERROR_INVALID_RESPONSE_TYPE", "Invalid response type."); 00068 00069 // Errors that we made up 00070 00071 // Error for trying to use a grant type that we haven't implemented 00072 define("ERROR_UNSUPPORTED_GRANT_TYPE", "unsupported_grant_type"); 00073 00074 // Whether to show verbose error messages in the JSON response 00075 define("ERROR_VERBOSE", TRUE); 00076 00077 abstract class OAuth2 { 00078 00079 /* Subclasses must implement the following functions */ 00080 00081 // Make sure that the client id is valid 00082 // If a secret is required, check that they've given the right one 00083 // Must return false if the client credentials are invalid 00084 abstract protected function auth_client_credentials($client_id, $client_secret = null); 00085 00086 // OAuth says we should store request URIs for each registered client 00087 // Implement this function to grab the stored URI for a given client id 00088 // Must return false if the given client does not exist or is invalid 00089 abstract protected function get_redirect_uri($client_id); 00090 00091 // We need to store and retrieve access token data as we create and verify tokens 00092 // Implement these functions to do just that 00093 00094 // Look up the supplied token id from storage, and return an array like: 00095 // 00096 // array( 00097 // "client_id" => <stored client id>, 00098 // "expires" => <stored expiration timestamp>, 00099 // "scope" => <stored scope (may be null) 00100 // ) 00101 // 00102 // Return null if the supplied token is invalid 00103 // 00104 abstract protected function get_access_token($token_id); 00105 00106 // Store the supplied values 00107 abstract protected function store_access_token($token_id, $client_id, $expires, $scope = null); 00108 00109 /* 00110 * 00111 * Stuff that should get overridden by subclasses 00112 * 00113 * I don't want to make these abstract, because then subclasses would have 00114 * to implement all of them, which is too much work. 00115 * 00116 * So they're just stubs. Override the ones you need. 00117 * 00118 */ 00119 00120 // You should override this function with something, 00121 // or else your OAuth provider won't support any grant types! 00122 protected function get_supported_grant_types() { 00123 // If you support all grant types, then you'd do: 00124 // return array( 00125 // AUTH_CODE_GRANT_TYPE, 00126 // USER_CREDENTIALS_GRANT_TYPE, 00127 // ASSERTION_GRANT_TYPE, 00128 // REFRESH_TOKEN_GRANT_TYPE, 00129 // NONE_GRANT_TYPE 00130 // ); 00131 00132 return array(); 00133 } 00134 00135 // You should override this function with your supported response types 00136 protected function get_supported_auth_response_types() { 00137 return array( 00138 AUTH_CODE_AUTH_RESPONSE_TYPE, 00139 ACCESS_TOKEN_AUTH_RESPONSE_TYPE, 00140 CODE_AND_TOKEN_AUTH_RESPONSE_TYPE 00141 ); 00142 } 00143 00144 // If you want to support scope use, then have this function return a list 00145 // of all acceptable scopes (used to throw the invalid-scope error) 00146 protected function get_supported_scopes() { 00147 // Example: 00148 // return array("my-friends", "photos", "whatever-else"); 00149 return array(); 00150 } 00151 00152 // If you want to restrict clients to certain authorization response types, 00153 // override this function 00154 // Given a client identifier and auth type, return true or false 00155 // (auth type would be one of the values contained in REGEX_AUTH_RESPONSE_TYPE) 00156 protected function authorize_client_response_type($client_id, $response_type) { 00157 return true; 00158 } 00159 00160 // If you want to restrict clients to certain grant types, override this function 00161 // Given a client identifier and grant type, return true or false 00162 protected function authorize_client($client_id, $grant_type) { 00163 return true; 00164 } 00165 00166 /* Functions that help grant access tokens for various grant types */ 00167 00168 // Fetch authorization code data (probably the most common grant type) 00169 // IETF Draft 4.1.1: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.1 00170 // Required for AUTH_CODE_GRANT_TYPE 00171 protected function get_stored_auth_code($code) { 00172 // Retrieve the stored data for the given authorization code 00173 // Should return: 00174 // 00175 // array ( 00176 // "client_id" => <stored client id>, 00177 // "redirect_uri" => <stored redirect URI>, 00178 // "expires" => <stored code expiration time>, 00179 // "scope" => <stored scope values (space-separated string), or can be omitted if scope is unused> 00180 // ) 00181 // 00182 // Return null if the code is invalid. 00183 00184 return null; 00185 } 00186 00187 // Take the provided authorization code values and store them somewhere (db, etc.) 00188 // Required for AUTH_CODE_GRANT_TYPE 00189 protected function store_auth_code($code, $client_id, $redirect_uri, $expires, $scope) { 00190 // This function should be the storage counterpart to get_stored_auth_code 00191 00192 // If storage fails for some reason, we're not currently checking 00193 // for any sort of success/failure, so you should bail out of the 00194 // script and provide a descriptive fail message 00195 } 00196 00197 // Grant access tokens for basic user credentials 00198 // IETF Draft 4.1.2: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.2 00199 // Required for USER_CREDENTIALS_GRANT_TYPE 00200 protected function check_user_credentials($client_id, $username, $password) { 00201 // Check the supplied username and password for validity 00202 // You can also use the $client_id param to do any checks required 00203 // based on a client, if you need that 00204 // If the username and password are invalid, return false 00205 00206 // If the username and password are valid, and you want to verify the scope of 00207 // a user's access, return an array with the scope values, like so: 00208 // 00209 // array ( 00210 // "scope" => <stored scope values (space-separated string)> 00211 // ) 00212 // 00213 // We'll check the scope you provide against the requested scope before 00214 // providing an access token. 00215 // 00216 // Otherwise, just return true. 00217 00218 return false; 00219 } 00220 00221 // Grant access tokens for assertions 00222 // IETF Draft 4.1.3: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.3 00223 // Required for ASSERTION_GRANT_TYPE 00224 protected function check_assertion($client_id, $assertion_type, $assertion) { 00225 // Check the supplied assertion for validity 00226 // You can also use the $client_id param to do any checks required 00227 // based on a client, if you need that 00228 // If the assertion is invalid, return false 00229 00230 // If the assertion is valid, and you want to verify the scope of 00231 // an access request, return an array with the scope values, like so: 00232 // 00233 // array ( 00234 // "scope" => <stored scope values (space-separated string)> 00235 // ) 00236 // 00237 // We'll check the scope you provide against the requested scope before 00238 // providing an access token. 00239 // 00240 // Otherwise, just return true. 00241 00242 return false; 00243 } 00244 00245 // Grant refresh access tokens 00246 // IETF Draft 4.1.4: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.4 00247 // Required for REFRESH_TOKEN_GRANT_TYPE 00248 protected function get_refresh_token($refresh_token) { 00249 // Retrieve the stored data for the given refresh token 00250 // Should return: 00251 // 00252 // array ( 00253 // "client_id" => <stored client id>, 00254 // "expires" => <refresh token expiration time>, 00255 // "scope" => <stored scope values (space-separated string), or can be omitted if scope is unused> 00256 // ) 00257 // 00258 // Return null if the token id is invalid. 00259 00260 return null; 00261 } 00262 00263 // Store refresh access tokens 00264 // Required for REFRESH_TOKEN_GRANT_TYPE 00265 protected function store_refresh_token($token, $client_id, $expires, $scope = null) { 00266 // If storage fails for some reason, we're not currently checking 00267 // for any sort of success/failure, so you should bail out of the 00268 // script and provide a descriptive fail message 00269 00270 return; 00271 } 00272 00273 private $old_refresh_token = ''; 00274 00275 // Expire a used refresh token. 00276 // This is not explicitly required in the spec, but is almost implied. After granting a new refresh token, 00277 // the old one is no longer useful and so should be forcibly expired in the data store so it can't be used again. 00278 protected function expire_refresh_token($token) { 00279 // If storage fails for some reason, we're not currently checking 00280 // for any sort of success/failure, so you should bail out of the 00281 // script and provide a descriptive fail message 00282 00283 return; 00284 } 00285 00286 // Grant access tokens for the "none" grant type 00287 // Not really described in the IETF Draft, so I just left a method stub...do whatever you want! 00288 // Required for NONE_GRANT_TYPE 00289 protected function check_none_access($client_id) { 00290 return false; 00291 } 00292 00293 protected function get_default_authentication_realm() { 00294 // Change this to whatever authentication realm you want to send in a WWW-Authenticate header 00295 return "Service"; 00296 } 00297 00298 /* End stuff that should get overridden */ 00299 00300 private $access_token_lifetime = 3600; 00301 private $auth_code_lifetime = 30; 00302 private $refresh_token_lifetime = 1209600; // Two weeks 00303 00304 public function __construct($access_token_lifetime = 3600, $auth_code_lifetime = 30, $refresh_token_lifetime = 1209600) { 00305 $this->access_token_lifetime = $access_token_lifetime; 00306 $this->auth_code_lifetime = $auth_code_lifetime; 00307 $this->refresh_token_lifetime = $refresh_token_lifetime; 00308 } 00309 00310 /* Resource protecting (Section 5) */ 00311 00312 // Check that a valid access token has been provided 00313 // 00314 // The scope parameter defines any required scope that the token must have 00315 // If a scope param is provided and the token does not have the required scope, 00316 // we bounce the request 00317 // 00318 // Some implementations may choose to return a subset of the protected resource 00319 // (i.e. "public" data) if the user has not provided an access token 00320 // or if the access token is invalid or expired 00321 // 00322 // The IETF spec says that we should send a 401 Unauthorized header and bail immediately 00323 // so that's what the defaults are set to 00324 // 00325 // Here's what each parameter does: 00326 // $scope = A space-separated string of required scope(s), if you want to check for scope 00327 // $exit_not_present = If true and no access token is provided, send a 401 header and exit, otherwise return false 00328 // $exit_invalid = If true and the implementation of get_access_token returns null, exit, otherwise return false 00329 // $exit_expired = If true and the access token has expired, exit, otherwise return false 00330 // $exit_scope = If true the access token does not have the required scope(s), exit, otherwise return false 00331 // $realm = If you want to specify a particular realm for the WWW-Authenticate header, supply it here 00332 public function verify_access_token($scope = null, $exit_not_present = true, $exit_invalid = true, $exit_expired = true, $exit_scope = true, $realm = null) { 00333 $token_param = $this->get_access_token_param(); 00334 if ($token_param === false) // Access token was not provided 00335 return $exit_not_present ? $this->send_401_unauthorized($realm, $scope) : false; 00336 00337 // Get the stored token data (from the implementing subclass) 00338 $token = $this->get_access_token($token_param); 00339 if ($token === null) 00340 return $exit_invalid ? $this->send_401_unauthorized($realm, $scope, ERROR_INVALID_TOKEN) : false; 00341 00342 // Check token expiration (I'm leaving this check separated, later we'll fill in better error messages) 00343 if (isset($token["expires"]) && time() > $token["expires"]) 00344 return $exit_expired ? $this->send_401_unauthorized($realm, $scope, ERROR_EXPIRED_TOKEN) : false; 00345 00346 // Check scope, if provided 00347 // If token doesn't have a scope, it's null/empty, or it's insufficient, then throw an error 00348 if ($scope && 00349 (!isset($token["scope"]) || !$token["scope"] || !$this->check_scope($scope, $token["scope"]))) 00350 return $exit_scope ? $this->send_401_unauthorized($realm, $scope, ERROR_INSUFFICIENT_SCOPE) : false; 00351 00352 return true; 00353 } 00354 00355 00356 // Returns true if everything in required scope is contained in available scope 00357 // False if something in required scope is not in available scope 00358 private function check_scope($required_scope, $available_scope) { 00359 // The required scope should match or be a subset of the available scope 00360 if (!is_array($required_scope)) 00361 $required_scope = explode(" ", $required_scope); 00362 00363 if (!is_array($available_scope)) 00364 $available_scope = explode(" ", $available_scope); 00365 00366 return (count(array_diff($required_scope, $available_scope)) == 0); 00367 } 00368 00369 // Send a 401 unauthorized header with the given realm 00370 // and an error, if provided 00371 private function send_401_unauthorized($realm, $scope, $error = null) { 00372 $realm = $realm === null ? $this->get_default_authentication_realm() : $realm; 00373 00374 $auth_header = "WWW-Authenticate: Token realm='".$realm."'"; 00375 00376 if ($scope) 00377 $auth_header .= ", scope='".$scope."'"; 00378 00379 if ($error !== null) 00380 $auth_header .= ", error='".$error."'"; 00381 00382 header("HTTP/1.1 401 Unauthorized"); 00383 header($auth_header); 00384 00385 exit; 00386 } 00387 00388 // Pulls the access token out of the HTTP request 00389 // Either from the Authorization header or GET/POST/etc. 00390 // Returns false if no token is present 00391 // TODO: Support POST or DELETE 00392 private function get_access_token_param() { 00393 $auth_header = $this->get_authorization_header(); 00394 00395 if ($auth_header !== false) { 00396 // Make sure only the auth header is set 00397 if (isset($_GET[OAUTH_TOKEN_PARAM_NAME]) || isset($_POST[OAUTH_TOKEN_PARAM_NAME])) 00398 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST, 'Auth token found in GET or POST when token present in header'); 00399 00400 $auth_header = trim($auth_header); 00401 00402 // Make sure it's Token authorization 00403 if (strcmp(substr($auth_header, 0, 6),"Token ") !== 0) 00404 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST, 'Auth header found that doesn\'t start with "Token"'); 00405 00406 // Parse the rest of the header 00407 if (preg_match('/\s*token\s*="(.+)"/', substr($auth_header, 6), $matches) == 0 || count($matches) < 2) 00408 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST, 'Malformed auth header'); 00409 00410 return $matches[1]; 00411 } 00412 00413 if (isset($_GET[OAUTH_TOKEN_PARAM_NAME])) { 00414 if (isset($_POST[OAUTH_TOKEN_PARAM_NAME])) // Both GET and POST are not allowed 00415 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST, 'Only send the token in GET or POST, not both'); 00416 00417 return $_GET[OAUTH_TOKEN_PARAM_NAME]; 00418 } 00419 00420 if (isset($_POST[OAUTH_TOKEN_PARAM_NAME])) 00421 return $_POST[OAUTH_TOKEN_PARAM_NAME]; 00422 00423 return false; 00424 } 00425 00426 /* Access token granting (Section 4) */ 00427 00428 // Grant or deny a requested access token 00429 // This would be called from the "/token" endpoint as defined in the spec 00430 // Obviously, you can call your endpoint whatever you want 00431 public function grant_access_token() { 00432 $filters = array( 00433 "grant_type" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => REGEX_TOKEN_GRANT_TYPE), "flags" => FILTER_REQUIRE_SCALAR), 00434 "scope" => array("flags" => FILTER_REQUIRE_SCALAR), 00435 "code" => array("flags" => FILTER_REQUIRE_SCALAR), 00436 "redirect_uri" => array("filter" => FILTER_VALIDATE_URL, "flags" => array(FILTER_FLAG_SCHEME_REQUIRED, FILTER_REQUIRE_SCALAR)), 00437 "username" => array("flags" => FILTER_REQUIRE_SCALAR), 00438 "password" => array("flags" => FILTER_REQUIRE_SCALAR), 00439 "assertion_type" => array("flags" => FILTER_REQUIRE_SCALAR), 00440 "assertion" => array("flags" => FILTER_REQUIRE_SCALAR), 00441 "refresh_token" => array("flags" => FILTER_REQUIRE_SCALAR), 00442 ); 00443 00444 $input = filter_input_array(INPUT_POST, $filters); 00445 00446 // Grant Type must be specified. 00447 if (!$input["grant_type"]) 00448 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST, 'Invalid grant_type parameter or parameter missing'); 00449 00450 // Make sure we've implemented the requested grant type 00451 if (!in_array($input["grant_type"], $this->get_supported_grant_types())) 00452 $this->error(ERROR_BAD_REQUEST, ERROR_UNSUPPORTED_GRANT_TYPE); 00453 00454 // Authorize the client 00455 $client = $this->get_client_credentials(); 00456 00457 if ($this->auth_client_credentials($client[0], $client[1]) === false) 00458 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_CREDENTIALS); 00459 00460 if (!$this->authorize_client($client[0], $input["grant_type"])) 00461 $this->error(ERROR_BAD_REQUEST, ERROR_UNAUTHORIZED_CLIENT); 00462 00463 // Do the granting 00464 switch ($input["grant_type"]) { 00465 case AUTH_CODE_GRANT_TYPE: 00466 if (!$input["code"] || !$input["redirect_uri"]) 00467 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST); 00468 00469 $stored = $this->get_stored_auth_code($input["code"]); 00470 00471 if ($stored === null || $input["redirect_uri"] != $stored["redirect_uri"] || $client[0] != $stored["client_id"]) 00472 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT); 00473 00474 if ($stored["expires"] < time()) 00475 $this->error(ERROR_BAD_REQUEST, ERROR_EXPIRED_TOKEN); 00476 00477 break; 00478 case USER_CREDENTIALS_GRANT_TYPE: 00479 if (!$input["username"] || !$input["password"]) 00480 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST, 'Missing parameters. "username" and "password" required'); 00481 00482 $stored = $this->check_user_credentials($client[0], $input["username"], $input["password"]); 00483 00484 if ($stored === false) 00485 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT); 00486 00487 break; 00488 case ASSERTION_GRANT_TYPE: 00489 if (!$input["assertion_type"] || !$input["assertion"]) 00490 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST); 00491 00492 $stored = $this->check_assertion($client[0], $input["assertion_type"], $input["assertion"]); 00493 00494 if ($stored === false) 00495 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT); 00496 00497 break; 00498 case REFRESH_TOKEN_GRANT_TYPE: 00499 if (!$input["refresh_token"]) 00500 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST, 'No "refresh_token" parameter found'); 00501 00502 $stored = $this->get_refresh_token($input["refresh_token"]); 00503 00504 if ($stored === null || $client[0] != $stored["client_id"]) 00505 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT); 00506 00507 if ($stored["expires"] < time()) 00508 $this->error(ERROR_BAD_REQUEST, ERROR_EXPIRED_TOKEN); 00509 00510 // store the refresh token locally so we can delete it when a new refresh token is generated 00511 $this->old_refresh_token = $stored["token"]; 00512 00513 break; 00514 case NONE_GRANT_TYPE: 00515 $stored = $this->check_none_access($client[0]); 00516 00517 if ($stored === false) 00518 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST); 00519 } 00520 00521 // Check scope, if provided 00522 if ($input["scope"] && (!is_array($stored) || !isset($stored["scope"]) || !$this->check_scope($input["scope"], $stored["scope"]))) 00523 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_SCOPE); 00524 00525 if (!$input["scope"]) 00526 $input["scope"] = null; 00527 00528 $token = $this->create_access_token($client[0], $input["scope"]); 00529 00530 $this->send_json_headers(); 00531 echo json_encode($token); 00532 } 00533 00534 // Internal function used to get the client credentials from HTTP basic auth or POST data 00535 // See http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-2 00536 protected function get_client_credentials() { 00537 if (isset($_SERVER["PHP_AUTH_USER"]) && $_POST && isset($_POST["client_id"])) 00538 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_CREDENTIALS); 00539 00540 // Try basic auth 00541 if (isset($_SERVER["PHP_AUTH_USER"])) 00542 return array($_SERVER["PHP_AUTH_USER"], $_SERVER["PHP_AUTH_PW"]); 00543 00544 // Try POST 00545 if ($_POST && isset($_POST["client_id"])) { 00546 if (isset($_POST["client_secret"])) 00547 return array($_POST["client_id"], $_POST["client_secret"]); 00548 00549 return array($_POST["client_id"], NULL); 00550 } 00551 00552 // No credentials were specified 00553 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_CREDENTIALS); 00554 } 00555 00556 /* End-user/client Authorization (Section 3 of IETF Draft) */ 00557 00558 // Pull the authorization request data out of the HTTP request 00559 // and return it so the authorization server can prompt the user 00560 // for approval 00561 public function get_authorize_params() { 00562 $filters = array( 00563 "client_id" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => REGEX_CLIENT_ID), "flags" => FILTER_REQUIRE_SCALAR), 00564 "response_type" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => REGEX_AUTH_RESPONSE_TYPE), "flags" => FILTER_REQUIRE_SCALAR), 00565 "redirect_uri" => array("filter" => FILTER_VALIDATE_URL, "flags" => array(FILTER_FLAG_SCHEME_REQUIRED, FILTER_REQUIRE_SCALAR)), 00566 "state" => array("flags" => FILTER_REQUIRE_SCALAR), 00567 "scope" => array("flags" => FILTER_REQUIRE_SCALAR), 00568 ); 00569 00570 $input = filter_input_array(INPUT_GET, $filters); 00571 00572 // Make sure a valid client id was supplied 00573 if (!$input["client_id"]) { 00574 if ($input["redirect_uri"]) 00575 $this->callback_error($input["redirect_uri"], ERROR_INVALID_CLIENT_ID, $input["state"]); 00576 00577 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_ID); // We don't have a good URI to use 00578 } 00579 00580 // redirect_uri is not required if already established via other channels 00581 // check an existing redirect URI against the one supplied 00582 $redirect_uri = $this->get_redirect_uri($input["client_id"]); 00583 00584 // At least one of: existing redirect URI or input redirect URI must be specified 00585 if (!$redirect_uri && !$input["redirect_uri"]) 00586 $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST); 00587 00588 // get_redirect_uri should return false if the given client ID is invalid 00589 // this probably saves us from making a separate db call, and simplifies the method set 00590 if ($redirect_uri === false) 00591 $this->callback_error($input["redirect_uri"], ERROR_INVALID_CLIENT_ID, $input["state"]); 00592 00593 // If there's an existing uri and one from input, verify that they match 00594 if ($redirect_uri && $input["redirect_uri"]) { 00595 // Ensure that the input uri starts with the stored uri 00596 if (strcasecmp(substr($input["redirect_uri"], 0, strlen($redirect_uri)),$redirect_uri) !== 0) 00597 $this->callback_error($input["redirect_uri"], ERROR_REDIRECT_URI_MISMATCH, $input["state"]); 00598 } elseif ($redirect_uri) { // They did not provide a uri from input, so use the stored one 00599 $input["redirect_uri"] = $redirect_uri; 00600 } 00601 00602 // type and client_id are required 00603 if (!$input["response_type"]) 00604 $this->callback_error($input["redirect_uri"], ERROR_INVALID_REQUEST, $input["state"], ERROR_INVALID_RESPONSE_TYPE); 00605 00606 // Check requested auth response type against the list of supported types 00607 if (array_search($input["response_type"], $this->get_supported_auth_response_types()) === false) 00608 $this->callback_error($input["redirect_uri"], ERROR_UNSUPPORTED_RESPONSE_TYPE, $input["state"]); 00609 00610 // Validate that the requested scope is supported 00611 if ($input["scope"] && !$this->check_scope($input["scope"], $this->get_supported_scopes())) 00612 $this->callback_error($input["redirect_uri"], ERROR_INVALID_SCOPE, $input["state"]); 00613 00614 return $input; 00615 } 00616 00617 // After the user has approved or denied the access request 00618 // the authorization server should call this function to redirect 00619 // the user appropriately 00620 00621 // The params all come from the results of get_authorize_params 00622 // except for $is_authorized -- this is true or false depending on whether 00623 // the user authorized the access 00624 public function finish_client_authorization($is_authorized, $type, $client_id, $redirect_uri, $state, $scope = null) { 00625 if ($state !== null) 00626 $result["query"]["state"] = $state; 00627 00628 if ($is_authorized === false) { 00629 $result["query"]["error"] = ERROR_USER_DENIED; 00630 } else { 00631 if ($type == AUTH_CODE_AUTH_RESPONSE_TYPE || $type == CODE_AND_TOKEN_AUTH_RESPONSE_TYPE) 00632 $result["query"]["code"] = $this->create_auth_code($client_id, $redirect_uri, $scope); 00633 00634 if ($type == ACCESS_TOKEN_AUTH_RESPONSE_TYPE || $type == CODE_AND_TOKEN_AUTH_RESPONSE_TYPE) 00635 $result["fragment"] = $this->create_access_token($client_id, $scope); 00636 } 00637 00638 $this->do_redirect_uri_callback($redirect_uri, $result); 00639 } 00640 00641 /* Other/utility functions */ 00642 00643 private function do_redirect_uri_callback($redirect_uri, $result) { 00644 header("HTTP/1.1 302 Found"); 00645 header("Location: " . $this->build_uri($redirect_uri, $result)); 00646 exit; 00647 } 00648 00649 private function build_uri($uri, $data) { 00650 $parse_url = parse_url($uri); 00651 00652 // Add our data to the parsed uri 00653 foreach ($data as $k => $v) { 00654 if (isset($parse_url[$k])) 00655 $parse_url[$k] .= "&" . http_build_query($v); 00656 else 00657 $parse_url[$k] = http_build_query($v); 00658 } 00659 00660 // Put humpty dumpty back together 00661 return 00662 ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "") 00663 .((isset($parse_url["user"])) ? $parse_url["user"] . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") ."@" : "") 00664 .((isset($parse_url["host"])) ? $parse_url["host"] : "") 00665 .((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "") 00666 .((isset($parse_url["path"])) ? $parse_url["path"] : "") 00667 .((isset($parse_url["query"])) ? "?" . $parse_url["query"] : "") 00668 .((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : ""); 00669 } 00670 00671 // This belongs in a separate factory, but to keep it simple, I'm just keeping it here. 00672 protected function create_access_token($client_id, $scope) { 00673 $token = array( 00674 "access_token" => $this->gen_access_token(), 00675 "expires_in" => $this->access_token_lifetime, 00676 "scope" => $scope 00677 ); 00678 00679 $this->store_access_token($token["access_token"], $client_id, time() + $this->access_token_lifetime, $scope); 00680 00681 // Issue a refresh token also, if we support them 00682 if (in_array(REFRESH_TOKEN_GRANT_TYPE, $this->get_supported_grant_types())) { 00683 $token["refresh_token"] = $this->gen_access_token(); 00684 $this->store_refresh_token($token["refresh_token"], $client_id, time() + $this->refresh_token_lifetime, $scope); 00685 // If we've granted a new refresh token, expire the old one 00686 if($this->old_refresh_token) 00687 $this->expire_refresh_token($this->old_refresh_token); 00688 } 00689 00690 return $token; 00691 } 00692 00693 private function create_auth_code($client_id, $redirect_uri, $scope) { 00694 $code = $this->gen_auth_code(); 00695 $this->store_auth_code($code, $client_id, $redirect_uri, time() + $this->auth_code_lifetime, $scope); 00696 return $code; 00697 } 00698 00699 // Implementing classes may want to override these two functions 00700 // to implement other access token or auth code generation schemes 00701 protected function gen_access_token() { 00702 return base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand())); 00703 } 00704 00705 protected function gen_auth_code() { 00706 return base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand())); 00707 } 00708 00709 // Implementing classes may need to override this function for use on non-Apache web servers 00710 // Just pull out the Authorization HTTP header and return it 00711 // Return false if the Authorization header does not exist 00712 private function get_authorization_header() { 00713 if (array_key_exists("HTTP_AUTHORIZATION", $_SERVER)) 00714 return $_SERVER["HTTP_AUTHORIZATION"]; 00715 00716 if (function_exists("apache_request_headers")) { 00717 $headers = apache_request_headers(); 00718 00719 if (array_key_exists("Authorization", $headers)) 00720 return $headers["Authorization"]; 00721 } 00722 00723 return false; 00724 } 00725 00726 private function send_json_headers() { 00727 header("Content-Type: application/json"); 00728 header("Cache-Control: no-store"); 00729 } 00730 00731 public function error($code, $message = null, $description = null) { 00732 header("HTTP/1.1 " . $code); 00733 00734 if ($message) { 00735 $this->send_json_headers(); 00736 $response = array("error" => $message); 00737 if(ERROR_VERBOSE && $description) 00738 $response["error_description"] = $description; 00739 echo json_encode($response); 00740 } 00741 00742 exit; 00743 } 00744 00745 public function callback_error($redirect_uri, $error, $state, $message = null, $error_uri = null) { 00746 $result["query"]["error"] = $error; 00747 00748 if ($state) 00749 $result["query"]["state"] = $state; 00750 00751 if ($message) 00752 $result["query"]["error_description"] = $message; 00753 00754 if ($error_uri) 00755 $result["query"]["error_uri"] = $error_uri; 00756 00757 $this->do_redirect_uri_callback($redirect_uri, $result); 00758 } 00759 }