Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

727 lines
30 KiB

  1. <?php
  2. //ini_set('display_errors', 1);
  3. //error_reporting(E_ALL | E_STRICT);
  4. // Regex to filter out the client identifier
  5. // (described in Section 2 of IETF draft)
  6. // IETF draft does not prescribe a format for these, however
  7. // I've arbitrarily chosen alphanumeric strings with hyphens and underscores, 3-12 characters long
  8. // Feel free to change.
  9. define("REGEX_CLIENT_ID", "/^[a-z0-9-_]{3,12}$/i");
  10. // Used to define the name of the OAuth access token parameter (POST/GET/etc.)
  11. // IETF Draft sections 5.2 and 5.3 specify that it should be called "oauth_token"
  12. // but other implementations use things like "access_token"
  13. // I won't be heartbroken if you change it, but it might be better to adhere to the spec
  14. define("OAUTH_TOKEN_PARAM_NAME", "oauth_token");
  15. // Client types (for client authorization)
  16. //define("WEB_SERVER_CLIENT_TYPE", "web_server");
  17. //define("USER_AGENT_CLIENT_TYPE", "user_agent");
  18. //define("REGEX_CLIENT_TYPE", "/^(web_server|user_agent)$/");
  19. define("ACCESS_TOKEN_AUTH_RESPONSE_TYPE", "token");
  20. define("AUTH_CODE_AUTH_RESPONSE_TYPE", "code");
  21. define("CODE_AND_TOKEN_AUTH_RESPONSE_TYPE", "code-and-token");
  22. define("REGEX_AUTH_RESPONSE_TYPE", "/^(token|code|code-and-token)$/");
  23. // Grant Types (for token obtaining)
  24. define("AUTH_CODE_GRANT_TYPE", "authorization-code");
  25. define("USER_CREDENTIALS_GRANT_TYPE", "basic-credentials");
  26. define("ASSERTION_GRANT_TYPE", "assertion");
  27. define("REFRESH_TOKEN_GRANT_TYPE", "refresh-token");
  28. define("NONE_GRANT_TYPE", "none");
  29. define("REGEX_TOKEN_GRANT_TYPE", "/^(authorization-code|basic-credentials|assertion|refresh-token|none)$/");
  30. /* Error handling constants */
  31. // HTTP status codes
  32. define("ERROR_NOT_FOUND", "404 Not Found");
  33. define("ERROR_BAD_REQUEST", "400 Bad Request");
  34. // TODO: Extend for i18n
  35. // "Official" OAuth 2.0 errors
  36. define("ERROR_REDIRECT_URI_MISMATCH", "redirect-uri-mismatch");
  37. define("ERROR_INVALID_CLIENT_CREDENTIALS", "invalid-client-credentials");
  38. define("ERROR_UNAUTHORIZED_CLIENT", "unauthorized-client");
  39. define("ERROR_USER_DENIED", "access-denied");
  40. define("ERROR_INVALID_REQUEST", "invalid-request");
  41. define("ERROR_INVALID_CLIENT_ID", "invalid-client-id");
  42. define("ERROR_UNSUPPORTED_RESPONSE_TYPE", "unsupported-response-type");
  43. define("ERROR_INVALID_SCOPE", "invalid-scope");
  44. define("ERROR_INVALID_GRANT", "invalid-grant");
  45. // Protected resource errors
  46. define("ERROR_INVALID_TOKEN", "invalid-token");
  47. define("ERROR_EXPIRED_TOKEN", "expired-token");
  48. define("ERROR_INSUFFICIENT_SCOPE", "insufficient-scope");
  49. // Messages
  50. define("ERROR_INVALID_RESPONSE_TYPE", "Invalid response type.");
  51. // Errors that we made up
  52. // Error for trying to use a grant type that we haven't implemented
  53. define("ERROR_UNSUPPORTED_GRANT_TYPE", "unsupported-grant-type");
  54. abstract class OAuth2 {
  55. /* Subclasses must implement the following functions */
  56. // Make sure that the client id is valid
  57. // If a secret is required, check that they've given the right one
  58. // Must return false if the client credentials are invalid
  59. abstract protected function auth_client_credentials($client_id, $client_secret = null);
  60. // OAuth says we should store request URIs for each registered client
  61. // Implement this function to grab the stored URI for a given client id
  62. // Must return false if the given client does not exist or is invalid
  63. abstract protected function get_redirect_uri($client_id);
  64. // We need to store and retrieve access token data as we create and verify tokens
  65. // Implement these functions to do just that
  66. // Look up the supplied token id from storage, and return an array like:
  67. //
  68. // array(
  69. // "client_id" => <stored client id>,
  70. // "expires" => <stored expiration timestamp>,
  71. // "scope" => <stored scope (may be null)
  72. // )
  73. //
  74. // Return null if the supplied token is invalid
  75. //
  76. abstract protected function get_access_token($token_id);
  77. // Store the supplied values
  78. abstract protected function store_access_token($token_id, $client_id, $expires, $scope = null);
  79. /*
  80. *
  81. * Stuff that should get overridden by subclasses
  82. *
  83. * I don't want to make these abstract, because then subclasses would have
  84. * to implement all of them, which is too much work.
  85. *
  86. * So they're just stubs. Override the ones you need.
  87. *
  88. */
  89. // You should override this function with something,
  90. // or else your OAuth provider won't support any grant types!
  91. protected function get_supported_grant_types() {
  92. // If you support all grant types, then you'd do:
  93. // return array(
  94. // AUTH_CODE_GRANT_TYPE,
  95. // USER_CREDENTIALS_GRANT_TYPE,
  96. // ASSERTION_GRANT_TYPE,
  97. // REFRESH_TOKEN_GRANT_TYPE,
  98. // NONE_GRANT_TYPE
  99. // );
  100. return array();
  101. }
  102. // You should override this function with your supported response types
  103. protected function get_supported_auth_response_types() {
  104. return array(
  105. AUTH_CODE_AUTH_RESPONSE_TYPE,
  106. ACCESS_TOKEN_AUTH_RESPONSE_TYPE,
  107. CODE_AND_TOKEN_AUTH_RESPONSE_TYPE
  108. );
  109. }
  110. // If you want to support scope use, then have this function return a list
  111. // of all acceptable scopes (used to throw the invalid-scope error)
  112. protected function get_supported_scopes() {
  113. // Example:
  114. // return array("my-friends", "photos", "whatever-else");
  115. return array();
  116. }
  117. // If you want to restrict clients to certain authorization response types,
  118. // override this function
  119. // Given a client identifier and auth type, return true or false
  120. // (auth type would be one of the values contained in REGEX_AUTH_RESPONSE_TYPE)
  121. protected function authorize_client_response_type($client_id, $response_type) {
  122. return true;
  123. }
  124. // If you want to restrict clients to certain grant types, override this function
  125. // Given a client identifier and grant type, return true or false
  126. protected function authorize_client($client_id, $grant_type) {
  127. return true;
  128. }
  129. /* Functions that help grant access tokens for various grant types */
  130. // Fetch authorization code data (probably the most common grant type)
  131. // IETF Draft 4.1.1: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.1
  132. // Required for AUTH_CODE_GRANT_TYPE
  133. protected function get_stored_auth_code($code) {
  134. // Retrieve the stored data for the given authorization code
  135. // Should return:
  136. //
  137. // array (
  138. // "client_id" => <stored client id>,
  139. // "redirect_uri" => <stored redirect URI>,
  140. // "expires" => <stored code expiration time>,
  141. // "scope" => <stored scope values (space-separated string), or can be omitted if scope is unused>
  142. // )
  143. //
  144. // Return null if the code is invalid.
  145. return null;
  146. }
  147. // Take the provided authorization code values and store them somewhere (db, etc.)
  148. // Required for AUTH_CODE_GRANT_TYPE
  149. protected function store_auth_code($code, $client_id, $redirect_uri, $expires, $scope) {
  150. // This function should be the storage counterpart to get_stored_auth_code
  151. // If storage fails for some reason, we're not currently checking
  152. // for any sort of success/failure, so you should bail out of the
  153. // script and provide a descriptive fail message
  154. }
  155. // Grant access tokens for basic user credentials
  156. // IETF Draft 4.1.2: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.2
  157. // Required for USER_CREDENTIALS_GRANT_TYPE
  158. protected function check_user_credentials($client_id, $username, $password) {
  159. // Check the supplied username and password for validity
  160. // You can also use the $client_id param to do any checks required
  161. // based on a client, if you need that
  162. // If the username and password are invalid, return false
  163. // If the username and password are valid, and you want to verify the scope of
  164. // a user's access, return an array with the scope values, like so:
  165. //
  166. // array (
  167. // "scope" => <stored scope values (space-separated string)>
  168. // )
  169. //
  170. // We'll check the scope you provide against the requested scope before
  171. // providing an access token.
  172. //
  173. // Otherwise, just return true.
  174. return false;
  175. }
  176. // Grant access tokens for assertions
  177. // IETF Draft 4.1.3: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.3
  178. // Required for ASSERTION_GRANT_TYPE
  179. protected function check_assertion($client_id, $assertion_type, $assertion) {
  180. // Check the supplied assertion for validity
  181. // You can also use the $client_id param to do any checks required
  182. // based on a client, if you need that
  183. // If the assertion is invalid, return false
  184. // If the assertion is valid, and you want to verify the scope of
  185. // an access request, return an array with the scope values, like so:
  186. //
  187. // array (
  188. // "scope" => <stored scope values (space-separated string)>
  189. // )
  190. //
  191. // We'll check the scope you provide against the requested scope before
  192. // providing an access token.
  193. //
  194. // Otherwise, just return true.
  195. return false;
  196. }
  197. // Grant refresh access tokens
  198. // IETF Draft 4.1.4: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.4
  199. // Required for REFRESH_TOKEN_GRANT_TYPE
  200. protected function get_refresh_token($refresh_token) {
  201. // Retrieve the stored data for the given refresh token
  202. // Should return:
  203. //
  204. // array (
  205. // "client_id" => <stored client id>,
  206. // "expires" => <refresh token expiration time>,
  207. // "scope" => <stored scope values (space-separated string), or can be omitted if scope is unused>
  208. // )
  209. //
  210. // Return null if the token id is invalid.
  211. return null;
  212. }
  213. // Store refresh access tokens
  214. // Required for REFRESH_TOKEN_GRANT_TYPE
  215. protected function store_refresh_token($token, $client_id, $expires, $scope = null) {
  216. // If storage fails for some reason, we're not currently checking
  217. // for any sort of success/failure, so you should bail out of the
  218. // script and provide a descriptive fail message
  219. return;
  220. }
  221. // Grant access tokens for the "none" grant type
  222. // Not really described in the IETF Draft, so I just left a method stub...do whatever you want!
  223. // Required for NONE_GRANT_TYPE
  224. protected function check_none_access($client_id) {
  225. return false;
  226. }
  227. protected function get_default_authentication_realm() {
  228. // Change this to whatever authentication realm you want to send in a WWW-Authenticate header
  229. return "Service";
  230. }
  231. /* End stuff that should get overridden */
  232. private $access_token_lifetime = 3600;
  233. private $auth_code_lifetime = 30;
  234. private $refresh_token_lifetime = 1209600; // Two weeks
  235. public function __construct($access_token_lifetime = 3600, $auth_code_lifetime = 30, $refresh_token_lifetime = 1209600) {
  236. $this->access_token_lifetime = $access_token_lifetime;
  237. $this->auth_code_lifetime = $auth_code_lifetime;
  238. $this->refresh_token_lifetime = $refresh_token_lifetime;
  239. }
  240. /* Resource protecting (Section 5) */
  241. // Check that a valid access token has been provided
  242. //
  243. // The scope parameter defines any required scope that the token must have
  244. // If a scope param is provided and the token does not have the required scope,
  245. // we bounce the request
  246. //
  247. // Some implementations may choose to return a subset of the protected resource
  248. // (i.e. "public" data) if the user has not provided an access token
  249. // or if the access token is invalid or expired
  250. //
  251. // The IETF spec says that we should send a 401 Unauthorized header and bail immediately
  252. // so that's what the defaults are set to
  253. //
  254. // Here's what each parameter does:
  255. // $scope = A space-separated string of required scope(s), if you want to check for scope
  256. // $exit_not_present = If true and no access token is provided, send a 401 header and exit, otherwise return false
  257. // $exit_invalid = If true and the implementation of get_access_token returns null, exit, otherwise return false
  258. // $exit_expired = If true and the access token has expired, exit, otherwise return false
  259. // $exit_scope = If true the access token does not have the required scope(s), exit, otherwise return false
  260. // $realm = If you want to specify a particular realm for the WWW-Authenticate header, supply it here
  261. public function verify_access_token($scope = null, $exit_not_present = true, $exit_invalid = true, $exit_expired = true, $exit_scope = true, $realm = null) {
  262. $token_param = $this->get_access_token_param();
  263. if ($token_param === false) // Access token was not provided
  264. return $exit_not_present ? $this->send_401_unauthorized($realm, $scope) : false;
  265. // Get the stored token data (from the implementing subclass)
  266. $token = $this->get_access_token($token_param);
  267. if ($token === null)
  268. return $exit_invalid ? $this->send_401_unauthorized($realm, $scope, ERROR_INVALID_TOKEN) : false;
  269. // Check token expiration (I'm leaving this check separated, later we'll fill in better error messages)
  270. if (isset($token["expires"]) && time() > $token["expires"])
  271. return $exit_expired ? $this->send_401_unauthorized($realm, $scope, ERROR_EXPIRED_TOKEN) : false;
  272. // Check scope, if provided
  273. // If token doesn't have a scope, it's null/empty, or it's insufficient, then throw an error
  274. if ($scope &&
  275. (!isset($token["scope"]) || !$token["scope"] || !$this->check_scope($scope, $token["scope"])))
  276. return $exit_scope ? $this->send_401_unauthorized($realm, $scope, ERROR_INSUFFICIENT_SCOPE) : false;
  277. return true;
  278. }
  279. // Returns true if everything in required scope is contained in available scope
  280. // False if something in required scope is not in available scope
  281. private function check_scope($required_scope, $available_scope) {
  282. // The required scope should match or be a subset of the available scope
  283. if (!is_array($required_scope))
  284. $required_scope = explode(" ", $required_scope);
  285. if (!is_array($available_scope))
  286. $available_scope = explode(" ", $available_scope);
  287. return (count(array_diff($required_scope, $available_scope)) == 0);
  288. }
  289. // Send a 401 unauthorized header with the given realm
  290. // and an error, if provided
  291. private function send_401_unauthorized($realm, $scope, $error = null) {
  292. $realm = $realm === null ? $this->get_default_authentication_realm() : $realm;
  293. $auth_header = "WWW-Authenticate: Token realm='".$realm."'";
  294. if ($scope)
  295. $auth_header .= ", scope='".$scope."'";
  296. if ($error !== null)
  297. $auth_header .= ", error='".$error."'";
  298. header("HTTP/1.1 401 Unauthorized");
  299. header($auth_header);
  300. exit;
  301. }
  302. // Pulls the access token out of the HTTP request
  303. // Either from the Authorization header or GET/POST/etc.
  304. // Returns false if no token is present
  305. // TODO: Support POST or DELETE
  306. private function get_access_token_param() {
  307. $auth_header = $this->get_authorization_header();
  308. if ($auth_header !== false) {
  309. // Make sure only the auth header is set
  310. if (isset($_GET[OAUTH_TOKEN_PARAM_NAME]) || isset($_POST[OAUTH_TOKEN_PARAM_NAME]))
  311. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
  312. $auth_header = trim($auth_header);
  313. // Make sure it's Token authorization
  314. if (strcmp(substr($auth_header, 0, 6),"Token ") !== 0)
  315. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
  316. // Parse the rest of the header
  317. if (preg_match('/\s*token\s*="(.+)"/', substr($auth_header, 6), $matches) == 0 || count($matches) < 2)
  318. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
  319. return $matches[1];
  320. }
  321. if (isset($_GET[OAUTH_TOKEN_PARAM_NAME])) {
  322. if (isset($_POST[OAUTH_TOKEN_PARAM_NAME])) // Both GET and POST are not allowed
  323. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
  324. return $_GET[OAUTH_TOKEN_PARAM_NAME];
  325. }
  326. if (isset($_POST[OAUTH_TOKEN_PARAM_NAME]))
  327. return $_POST[OAUTH_TOKEN_PARAM_NAME];
  328. return false;
  329. }
  330. /* Access token granting (Section 4) */
  331. // Grant or deny a requested access token
  332. // This would be called from the "/token" endpoint as defined in the spec
  333. // Obviously, you can call your endpoint whatever you want
  334. public function grant_access_token() {
  335. $filters = array(
  336. "grant_type" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => REGEX_TOKEN_GRANT_TYPE), "flags" => FILTER_REQUIRE_SCALAR),
  337. "scope" => array("flags" => FILTER_REQUIRE_SCALAR),
  338. "code" => array("flags" => FILTER_REQUIRE_SCALAR),
  339. "redirect_uri" => array("filter" => FILTER_VALIDATE_URL, "flags" => array(FILTER_FLAG_SCHEME_REQUIRED, FILTER_REQUIRE_SCALAR)),
  340. "username" => array("flags" => FILTER_REQUIRE_SCALAR),
  341. "password" => array("flags" => FILTER_REQUIRE_SCALAR),
  342. "assertion_type" => array("flags" => FILTER_REQUIRE_SCALAR),
  343. "assertion" => array("flags" => FILTER_REQUIRE_SCALAR),
  344. "refresh_token" => array("flags" => FILTER_REQUIRE_SCALAR),
  345. );
  346. $input = filter_input_array(INPUT_POST, $filters);
  347. // Grant Type must be specified.
  348. if (!$input["grant_type"])
  349. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
  350. // Make sure we've implemented the requested grant type
  351. if (!in_array($input["grant_type"], $this->get_supported_grant_types()))
  352. $this->error(ERROR_BAD_REQUEST, ERROR_UNSUPPORTED_GRANT_TYPE);
  353. // Authorize the client
  354. $client = $this->get_client_credentials();
  355. if ($this->auth_client_credentials($client[0], $client[1]) === false)
  356. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_CREDENTIALS);
  357. if (!$this->authorize_client($client[0], $input["grant_type"]))
  358. $this->error(ERROR_BAD_REQUEST, ERROR_UNAUTHORIZED_CLIENT);
  359. // Do the granting
  360. switch ($input["grant_type"]) {
  361. case AUTH_CODE_GRANT_TYPE:
  362. if (!$input["code"] || !$input["redirect_uri"])
  363. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
  364. $stored = $this->get_stored_auth_code($input["code"]);
  365. if ($stored === null || $input["redirect_uri"] != $stored["redirect_uri"] || $client[0] != $stored["client_id"])
  366. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
  367. if ($stored["expires"] > time())
  368. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
  369. break;
  370. case USER_CREDENTIALS_GRANT_TYPE:
  371. if (!$input["username"] || !$input["password"])
  372. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
  373. $stored = $this->check_user_credentials($client[0], $input["username"], $input["password"]);
  374. if ($stored === false)
  375. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
  376. break;
  377. case ASSERTION_GRANT_TYPE:
  378. if (!$input["assertion_type"] || !$input["assertion"])
  379. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
  380. $stored = $this->check_assertion($client[0], $input["assertion_type"], $input["assertion"]);
  381. if ($stored === false)
  382. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
  383. break;
  384. case REFRESH_TOKEN_GRANT_TYPE:
  385. if (!$input["refresh_token"])
  386. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
  387. $stored = $this->get_refresh_token($input["refresh_token"]);
  388. if ($stored === null || $client[0] != $stored["client_id"])
  389. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
  390. if ($stored["expires"] > time())
  391. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
  392. break;
  393. case NONE_GRANT_TYPE:
  394. $stored = $this->check_none_access($client[0]);
  395. if ($stored === false)
  396. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
  397. }
  398. // Check scope, if provided
  399. if ($input["scope"] && (!is_array($stored) || !isset($stored["scope"]) || !$this->check_scope($input["scope"], $stored["scope"])))
  400. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_SCOPE);
  401. if (!$input["scope"])
  402. $input["scope"] = null;
  403. $token = $this->create_access_token($client[0], $input["scope"]);
  404. $this->send_json_headers();
  405. echo json_encode($token);
  406. }
  407. // Internal function used to get the client credentials from HTTP basic auth or POST data
  408. // See http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-2
  409. private function get_client_credentials() {
  410. if (isset($_SERVER["PHP_AUTH_USER"]) && $_POST && isset($_POST["client_id"]))
  411. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_CREDENTIALS);
  412. // Try basic auth
  413. if (isset($_SERVER["PHP_AUTH_USER"]))
  414. return array($_SERVER["PHP_AUTH_USER"], $_SERVER["PHP_AUTH_PW"]);
  415. // Try POST
  416. if ($_POST && isset($_POST["client_id"])) {
  417. if (isset($_POST["client_secret"]))
  418. return array($_POST["client_id"], $_POST["client_secret"]);
  419. return array($_POST["client_id"], NULL);
  420. }
  421. // No credentials were specified
  422. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_CREDENTIALS);
  423. }
  424. /* End-user/client Authorization (Section 3 of IETF Draft) */
  425. // Pull the authorization request data out of the HTTP request
  426. // and return it so the authorization server can prompt the user
  427. // for approval
  428. public function get_authorize_params() {
  429. $filters = array(
  430. "client_id" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => REGEX_CLIENT_ID), "flags" => FILTER_REQUIRE_SCALAR),
  431. "response_type" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => REGEX_AUTH_RESPONSE_TYPE), "flags" => FILTER_REQUIRE_SCALAR),
  432. "redirect_uri" => array("filter" => FILTER_VALIDATE_URL, "flags" => array(FILTER_FLAG_SCHEME_REQUIRED, FILTER_REQUIRE_SCALAR)),
  433. "state" => array("flags" => FILTER_REQUIRE_SCALAR),
  434. "scope" => array("flags" => FILTER_REQUIRE_SCALAR),
  435. );
  436. $input = filter_input_array(INPUT_GET, $filters);
  437. // Make sure a valid client id was supplied
  438. if (!$input["client_id"]) {
  439. if ($input["redirect_uri"])
  440. $this->callback_error($input["redirect_uri"], ERROR_INVALID_CLIENT_ID, $input["state"]);
  441. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_ID); // We don't have a good URI to use
  442. }
  443. // redirect_uri is not required if already established via other channels
  444. // check an existing redirect URI against the one supplied
  445. $redirect_uri = $this->get_redirect_uri($input["client_id"]);
  446. // At least one of: existing redirect URI or input redirect URI must be specified
  447. if (!$redirect_uri && !$input["redirect_uri"])
  448. $this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
  449. // get_redirect_uri should return false if the given client ID is invalid
  450. // this probably saves us from making a separate db call, and simplifies the method set
  451. if ($redirect_uri === false)
  452. $this->callback_error($input["redirect_uri"], ERROR_INVALID_CLIENT_ID, $input["state"]);
  453. // If there's an existing uri and one from input, verify that they match
  454. if ($redirect_uri && $input["redirect_uri"]) {
  455. // Ensure that the input uri starts with the stored uri
  456. if (strcasecmp(substr($input["redirect_uri"], 0, strlen($redirect_uri)),$redirect_uri) !== 0)
  457. $this->callback_error($input["redirect_uri"], ERROR_REDIRECT_URI_MISMATCH, $input["state"]);
  458. } elseif ($redirect_uri) { // They did not provide a uri from input, so use the stored one
  459. $input["redirect_uri"] = $redirect_uri;
  460. }
  461. // type and client_id are required
  462. if (!$input["response_type"])
  463. $this->callback_error($input["redirect_uri"], ERROR_INVALID_REQUEST, $input["state"], ERROR_INVALID_RESPONSE_TYPE);
  464. // Check requested auth response type against the list of supported types
  465. if (array_search($input["response_type"], $this->get_supported_auth_response_types()) === false)
  466. $this->callback_error($input["redirect_uri"], ERROR_UNSUPPORTED_RESPONSE_TYPE, $input["state"]);
  467. // Validate that the requested scope is supported
  468. if ($input["scope"] && !$this->check_scope($input["scope"], $this->get_supported_scopes()))
  469. $this->callback_error($input["redirect_uri"], ERROR_INVALID_SCOPE, $input["state"]);
  470. return $input;
  471. }
  472. // After the user has approved or denied the access request
  473. // the authorization server should call this function to redirect
  474. // the user appropriately
  475. // The params all come from the results of get_authorize_params
  476. // except for $is_authorized -- this is true or false depending on whether
  477. // the user authorized the access
  478. public function finish_client_authorization($is_authorized, $type, $client_id, $redirect_uri, $state, $scope = null) {
  479. if ($state !== null)
  480. $result["query"]["state"] = $state;
  481. if ($is_authorized === false) {
  482. $result["query"]["error"] = ERROR_USER_DENIED;
  483. } else {
  484. if ($type == AUTH_CODE_AUTH_RESPONSE_TYPE || $type == CODE_AND_TOKEN_AUTH_RESPONSE_TYPE)
  485. $result["query"]["code"] = $this->create_auth_code($client_id, $redirect_uri, $scope);
  486. if ($type == ACCESS_TOKEN_AUTH_RESPONSE_TYPE || $type == CODE_AND_TOKEN_AUTH_RESPONSE_TYPE)
  487. $result["fragment"] = $this->create_access_token($client_id, $scope);
  488. }
  489. $this->do_redirect_uri_callback($redirect_uri, $result);
  490. }
  491. /* Other/utility functions */
  492. private function do_redirect_uri_callback($redirect_uri, $result) {
  493. header("HTTP/1.1 302 Found");
  494. header("Location: " . $this->build_uri($redirect_uri, $result));
  495. exit;
  496. }
  497. private function build_uri($uri, $data) {
  498. $parse_url = parse_url($uri);
  499. // Add our data to the parsed uri
  500. foreach ($data as $k => $v) {
  501. if (isset($parse_url[$k]))
  502. $parse_url[$k] .= "&" . http_build_query($v);
  503. else
  504. $parse_url[$k] = http_build_query($v);
  505. }
  506. // Put humpty dumpty back together
  507. return
  508. ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "")
  509. .((isset($parse_url["user"])) ? $parse_url["user"] . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") ."@" : "")
  510. .((isset($parse_url["host"])) ? $parse_url["host"] : "")
  511. .((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "")
  512. .((isset($parse_url["path"])) ? $parse_url["path"] : "")
  513. .((isset($parse_url["query"])) ? "?" . $parse_url["query"] : "")
  514. .((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : "");
  515. }
  516. // This belongs in a separate factory, but to keep it simple, I'm just keeping it here.
  517. private function create_access_token($client_id, $scope) {
  518. $token = array(
  519. "access_token" => $this->gen_access_token(),
  520. "expires_in" => $this->access_token_lifetime,
  521. "scope" => $scope
  522. );
  523. $this->store_access_token($token["access_token"], $client_id, time() + $this->access_token_lifetime, $scope);
  524. // Issue a refresh token also, if we support them
  525. if (in_array(REFRESH_TOKEN_GRANT_TYPE, $this->get_supported_grant_types())) {
  526. $token["refresh_token"] = $this->gen_access_token();
  527. $this->store_refresh_token($token["refresh_token"], $client_id, time() + $this->refresh_token_lifetime, $scope);
  528. }
  529. return $token;
  530. }
  531. private function create_auth_code($client_id, $redirect_uri, $scope) {
  532. $code = $this->gen_auth_code();
  533. $this->store_auth_code($code, $client_id, $redirect_uri, time() + $this->auth_code_lifetime, $scope);
  534. return $code;
  535. }
  536. // Implementing classes may want to override these two functions
  537. // to implement other access token or auth code generation schemes
  538. private function gen_access_token() {
  539. return base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand()));
  540. }
  541. private function gen_auth_code() {
  542. return base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand()));
  543. }
  544. // Implementing classes may need to override this function for use on non-Apache web servers
  545. // Just pull out the Authorization HTTP header and return it
  546. // Return false if the Authorization header does not exist
  547. private function get_authorization_header() {
  548. if (array_key_exists("HTTP_AUTHORIZATION", $_SERVER))
  549. return $_SERVER["HTTP_AUTHORIZATION"];
  550. if (function_exists("apache_request_headers")) {
  551. $headers = apache_request_headers();
  552. if (array_key_exists("Authorization", $headers))
  553. return $headers["Authorization"];
  554. }
  555. return false;
  556. }
  557. private function send_json_headers() {
  558. header("Content-Type: application/json");
  559. header("Cache-Control: no-store");
  560. }
  561. public function error($code, $message = null) {
  562. header("HTTP/1.1 " . $code);
  563. if ($message) {
  564. $this->send_json_headers();
  565. echo json_encode(array("error" => $message));
  566. }
  567. exit;
  568. }
  569. public function callback_error($redirect_uri, $error, $state, $message = null, $error_uri = null) {
  570. $result["query"]["error"] = $error;
  571. if ($state)
  572. $result["query"]["state"] = $state;
  573. if ($message)
  574. $result["query"]["error_description"] = $message;
  575. if ($error_uri)
  576. $result["query"]["error_uri"] = $error_uri;
  577. $this->do_redirect_uri_callback($redirect_uri, $result);
  578. }
  579. }