From 7975bc244df2a33dd637303c8b7377de9cdf6c6d Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 13 May 2021 11:26:56 +0000 Subject: [PATCH] Refine OAuth flow --- doc/API-Mastodon.md | 15 ++++++++ src/Factory/Api/Mastodon/Card.php | 2 +- src/Model/Item.php | 4 +-- src/Module/Api/Mastodon/Apps.php | 12 +++++++ src/Module/BaseApi.php | 22 ++++++------ src/Module/OAuth/Authorize.php | 24 ++++++++++--- src/Module/OAuth/Token.php | 29 +++++++++------- src/Object/Api/Mastodon/Token.php | 58 +++++++++++++++++++++++++++++++ 8 files changed, 134 insertions(+), 32 deletions(-) create mode 100644 src/Object/Api/Mastodon/Token.php diff --git a/doc/API-Mastodon.md b/doc/API-Mastodon.md index 7f5b55797a..18af62be68 100644 --- a/doc/API-Mastodon.md +++ b/doc/API-Mastodon.md @@ -9,6 +9,21 @@ Friendica provides the following endpoints defined in [the official Mastodon API Authentication is the same as described in [Using the APIs](help/api#Authentication). +## Clients + +Supported mobile apps: + +- Tusky +- Husky +- twitlatte + +Unsupported mobile apps: + +- [Subway Tooter](https://github.com/tateisu/SubwayTooter) Uses the wrong grant_type when requesting a token, possibly a problem in the server type detection of the app. See issue https://github.com/tateisu/SubwayTooter/issues/156 +- [Mammut](https://github.com/jamiesanson/Mammut) States that the instance doesn't exist. Most likely an issue in the vitality check of the app, see issue https://github.com/jamiesanson/Mammut/issues/19 +- [AndStatus](https://github.com/andstatus/andstatus) Doesn't provide all data at token request, see issue https://github.com/andstatus/andstatus/issues/537 +- [Fedilab](https://framagit.org/tom79/fedilab) Automatically uses the legacy API, see issue: https://framagit.org/tom79/fedilab/-/issues/520 + ## Entities These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/entities/). diff --git a/src/Factory/Api/Mastodon/Card.php b/src/Factory/Api/Mastodon/Card.php index 1505fa8529..f7512b3903 100644 --- a/src/Factory/Api/Mastodon/Card.php +++ b/src/Factory/Api/Mastodon/Card.php @@ -37,7 +37,7 @@ class Card extends BaseFactory */ public function createFromUriId(int $uriId) { - $item = Post::selectFirst(['nody'], ['uri-id' => $uriId]); + $item = Post::selectFirst(['body'], ['uri-id' => $uriId]); if (!empty($item['body'])) { $data = BBCode::getAttachmentData($item['body']); } else { diff --git a/src/Model/Item.php b/src/Model/Item.php index 2ce05589de..e5b95d6893 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -2914,11 +2914,11 @@ class Item $data['description'] = ''; } - if (!empty($data['author_name']) && !empty($data['provider_name'])) { + if (($data['author_name'] ?? '') == ($data['provider_name'] ?? '')) { $data['author_name'] = ''; } - if (!empty($data['author_url']) && !empty($data['provider_url'])) { + if (($data['author_url'] ?? '') && ($data['provider_url'] ?? '')) { $data['author_url'] = ''; } } elseif (preg_match("/.*(\[attachment.*?\].*?\[\/attachment\]).*/ism", $body, $match)) { diff --git a/src/Module/Api/Mastodon/Apps.php b/src/Module/Api/Mastodon/Apps.php index 7b13b17018..0fc206d43b 100644 --- a/src/Module/Api/Mastodon/Apps.php +++ b/src/Module/Api/Mastodon/Apps.php @@ -25,6 +25,7 @@ use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Module\BaseApi; +use Friendica\Util\Network; /** * Apps class to register new OAuth clients @@ -37,6 +38,17 @@ class Apps extends BaseApi */ public static function post(array $parameters = []) { + // Workaround for AndStatus, see issue https://github.com/andstatus/andstatus/issues/538 + if (empty($_REQUEST['client_name']) || empty($_REQUEST['redirect_uris'])) { + $postdata = Network::postdata(); + if (!empty($postdata)) { + $_REQUEST = json_decode($postdata, true); + if (empty($_REQUEST)) { + DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Missing parameters')); + } + } + } + $name = $_REQUEST['client_name'] ?? ''; $redirect = $_REQUEST['redirect_uris'] ?? ''; $scopes = $_REQUEST['scopes'] ?? ''; diff --git a/src/Module/BaseApi.php b/src/Module/BaseApi.php index b4f228a6ee..b18f0402c7 100644 --- a/src/Module/BaseApi.php +++ b/src/Module/BaseApi.php @@ -21,6 +21,7 @@ namespace Friendica\Module; +use Exception; use Friendica\BaseModule; use Friendica\Core\Logger; use Friendica\Core\System; @@ -206,19 +207,13 @@ class BaseApi extends BaseModule /** * Get the application record via the proved request header fields * + * @param string $client_id + * @param string $client_secret + * @param string $redirect_uri * @return array application record */ - public static function getApplication() + public static function getApplication(string $client_id, string $client_secret, string $redirect_uri) { - $redirect_uri = $_REQUEST['redirect_uri'] ?? ''; - $client_id = $_REQUEST['client_id'] ?? ''; - $client_secret = $_REQUEST['client_secret'] ?? ''; - - if ((empty($redirect_uri) && empty($client_secret)) || empty($client_id)) { - Logger::warning('Incomplete request', ['request' => $_REQUEST]); - return []; - } - $condition = ['client_id' => $client_id]; if (!empty($client_secret)) { $condition['client_secret'] = $client_secret; @@ -262,15 +257,18 @@ class BaseApi extends BaseModule /** * Create and fetch an token for the application and user * - * @param array $application + * @param array $application * @param integer $uid + * @param string $scope * @return array application record */ - public static function createTokenForUser(array $application, int $uid) + public static function createTokenForUser(array $application, int $uid, string $scope) { $code = bin2hex(random_bytes(32)); $access_token = bin2hex(random_bytes(32)); + // @todo store the scope + $fields = ['application-id' => $application['id'], 'uid' => $uid, 'code' => $code, 'access_token' => $access_token, 'created_at' => DateTimeFormat::utcNow(DateTimeFormat::MYSQL)]; if (!DBA::insert('application-token', $fields, Database::INSERT_UPDATE)) { return []; diff --git a/src/Module/OAuth/Authorize.php b/src/Module/OAuth/Authorize.php index 7d9f67ad35..c956207705 100644 --- a/src/Module/OAuth/Authorize.php +++ b/src/Module/OAuth/Authorize.php @@ -27,6 +27,7 @@ use Friendica\Module\BaseApi; /** * @see https://docs.joinmastodon.org/spec/oauth/ + * @see https://aaronparecki.com/oauth-2-simplified/ */ class Authorize extends BaseApi { @@ -37,16 +38,29 @@ class Authorize extends BaseApi public static function rawContent(array $parameters = []) { $response_type = $_REQUEST['response_type'] ?? ''; + $client_id = $_REQUEST['client_id'] ?? ''; + $client_secret = $_REQUEST['client_secret'] ?? ''; // Isn't normally provided. We will use it if present. + $redirect_uri = $_REQUEST['redirect_uri'] ?? ''; + $scope = $_REQUEST['scope'] ?? ''; + $state = $_REQUEST['state'] ?? ''; + if ($response_type != 'code') { - Logger::warning('Wrong or missing response type', ['response_type' => $response_type]); - DI::mstdnError()->UnprocessableEntity(); + Logger::warning('Unsupported or missing response type', ['request' => $_REQUEST]); + DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Unsupported or missing response type')); } - $application = self::getApplication(); + if (empty($client_id) || empty($redirect_uri)) { + Logger::warning('Incomplete request data', ['request' => $_REQUEST]); + DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Incomplete request data')); + } + + $application = self::getApplication($client_id, $client_secret, $redirect_uri); if (empty($application)) { DI::mstdnError()->UnprocessableEntity(); } + // @todo Compare the application scope and requested scope + $request = $_REQUEST; unset($request['pagename']); $redirect = 'oauth/authorize?' . http_build_query($request); @@ -66,11 +80,11 @@ class Authorize extends BaseApi DI::session()->remove('oauth_acknowledge'); - $token = self::createTokenForUser($application, $uid); + $token = self::createTokenForUser($application, $uid, $scope); if (!$token) { DI::mstdnError()->UnprocessableEntity(); } - DI::app()->redirect($application['redirect_uri'] . '?code=' . $token['code']); + DI::app()->redirect($application['redirect_uri'] . '?' . http_build_query(['code' => $token['code'], 'state' => $state])); } } diff --git a/src/Module/OAuth/Token.php b/src/Module/OAuth/Token.php index 17f2e2b820..c3aaac6d1e 100644 --- a/src/Module/OAuth/Token.php +++ b/src/Module/OAuth/Token.php @@ -29,39 +29,44 @@ use Friendica\Module\BaseApi; /** * @see https://docs.joinmastodon.org/spec/oauth/ + * @see https://aaronparecki.com/oauth-2-simplified/ */ class Token extends BaseApi { public static function post(array $parameters = []) { - $client_secret = $_REQUEST['client_secret'] ?? ''; - $code = $_REQUEST['code'] ?? ''; $grant_type = $_REQUEST['grant_type'] ?? ''; + $code = $_REQUEST['code'] ?? ''; + $redirect_uri = $_REQUEST['redirect_uri'] ?? ''; + $client_id = $_REQUEST['client_id'] ?? ''; + $client_secret = $_REQUEST['client_secret'] ?? ''; if ($grant_type != 'authorization_code') { Logger::warning('Unsupported or missing grant type', ['request' => $_REQUEST]); DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Unsupported or missing grant type')); } - $application = self::getApplication(); + if (empty($client_id) || empty($client_secret) || empty($redirect_uri)) { + Logger::warning('Incomplete request data', ['request' => $_REQUEST]); + DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Incomplete request data')); + } + + $application = self::getApplication($client_id, $client_secret, $redirect_uri); if (empty($application)) { DI::mstdnError()->UnprocessableEntity(); } - if ($application['client_secret'] != $client_secret) { - Logger::warning('Wrong client secret', $client_secret); - DI::mstdnError()->Unauthorized(); - } - - $condition = ['application-id' => $application['id'], 'code' => $code]; + // For security reasons only allow freshly created tokens + $condition = ["`application-id` = ? AND `code` = ? AND `created_at` > UTC_TIMESTAMP() - INTERVAL ? MINUTE", $application['id'], $code, 5]; $token = DBA::selectFirst('application-token', ['access_token', 'created_at'], $condition); if (!DBA::isResult($token)) { - Logger::warning('Token not found', $condition); + Logger::warning('Token not found or outdated', $condition); DI::mstdnError()->Unauthorized(); } - // @todo Use entity class - System::jsonExit(['access_token' => $token['access_token'], 'token_type' => 'Bearer', 'scope' => $application['scopes'], 'created_at' => $token['created_at']]); + $object = new \Friendica\Object\Api\Mastodon\Token($token['access_token'], 'Bearer', $application['scopes'], $token['created_at']); + + System::jsonExit($object->toArray()); } } diff --git a/src/Object/Api/Mastodon/Token.php b/src/Object/Api/Mastodon/Token.php new file mode 100644 index 0000000000..a3921fc8a7 --- /dev/null +++ b/src/Object/Api/Mastodon/Token.php @@ -0,0 +1,58 @@ +. + * + */ + +namespace Friendica\Object\Api\Mastodon; + +use Friendica\BaseDataTransferObject; +use Friendica\Util\DateTimeFormat; + +/** + * Class Error + * + * @see https://docs.joinmastodon.org/entities/error + */ +class Token extends BaseDataTransferObject +{ + /** @var string */ + protected $access_token; + /** @var string */ + protected $token_type; + /** @var string */ + protected $scope; + /** @var string (Datetime) */ + protected $created_at; + + /** + * Creates a token record + * + * @param string $access_token + * @param string $token_type + * @param string $scope + * @param string $created_at + */ + public function __construct(string $access_token, string $token_type, string $scope, string $created_at) + { + $this->access_token = $access_token; + $this->token_type = $token_type; + $this->scope = $scope; + $this->created_at = DateTimeFormat::utc($created_at, DateTimeFormat::ATOM); + } +}