From a7c9e4bb7e9a9d4fd2d47a7d9c912328e0464caf Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 14 Jul 2023 17:28:45 -0700 Subject: [PATCH] Improve OAuth2 Error Handling Update the OAuth2 class to throw an exception if an API call to the OAuth2 provider fails and update the endpoint to redirect to the failure url with the information from the OAuth2 provider. --- app/controllers/api/account.php | 79 +++++++++++++++++++++------------ src/Appwrite/Auth/OAuth2.php | 10 ++++- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 4869a30dd6..4e862b822e 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2,6 +2,7 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; +use Appwrite\Auth\OAuth2\Exception as OAuth2Exception; use Appwrite\Auth\Validator\Password; use Appwrite\Auth\Validator\Phone; use Appwrite\Detector\Detector; @@ -413,27 +414,22 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}'; $providerEnabled = $project->getAttribute('authProviders', [])[$provider . 'Enabled'] ?? false; - if (!$providerEnabled) { - throw new Exception(Exception::PROJECT_PROVIDER_DISABLED, 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.'); - } - - if (!empty($appSecret) && isset($appSecret['version'])) { - $key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']); - $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); - } - $className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider); if (!\class_exists($className)) { throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); } + $providers = Config::getParam('providers'); + $providerName = $providers[$provider]['name'] ?? ''; + + /** @var Appwrite\Auth\OAuth2 $oauth2 */ $oauth2 = new $className($appId, $appSecret, $callback); if (!empty($state)) { try { $state = \array_merge($defaultState, $oauth2->parseState($state)); - } catch (\Exception$exception) { + } catch (\Exception $exception) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to parse login state params as passed from OAuth2 provider'); } } else { @@ -447,27 +443,54 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') if (!empty($state['failure']) && !$validateURL->isValid($state['failure'])) { throw new Exception(Exception::PROJECT_INVALID_FAILURE_URL); } - - $accessToken = $oauth2->getAccessToken($code); - $refreshToken = $oauth2->getRefreshToken($code); - $accessTokenExpiry = $oauth2->getAccessTokenExpiry($code); - - if (empty($accessToken)) { - if (!empty($state['failure'])) { - $response->redirect($state['failure'], 301, 0); + $failure = []; + if (!empty($state['failure'])) { + $failure = URLParser::parse($state['failure']); + } + $failureRedirect = (function (string $type, ?string $message = null, ?int $code = null) use ($failure, $response) { + $exception = new Exception($type, $message, $code); + if (!empty($failure)) { + $query = URLParser::parseQuery($failure['query']); + $query['error'] = json_encode([ + 'message' => $exception->getMessage(), + 'type' => $exception->getType(), + 'code' => !\is_null($code) ? $code : $exception->getCode(), + ]); + $failure['query'] = URLParser::unparseQuery($query); + $response->redirect(URLParser::unparse($failure), 301); } - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to obtain access token'); + throw $exception; + }); + + if (!$providerEnabled) { + $failureRedirect(Exception::PROJECT_PROVIDER_DISABLED, 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.'); + } + + if (!empty($appSecret) && isset($appSecret['version'])) { + $key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']); + $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); + } + + $accessToken = ''; + $refreshToken = ''; + $accessTokenExpiry = 0; + + try { + $accessToken = $oauth2->getAccessToken($code); + $refreshToken = $oauth2->getRefreshToken($code); + $accessTokenExpiry = $oauth2->getAccessTokenExpiry($code); + } catch (OAuth2Exception $ex) { + $failureRedirect( + $ex->getType(), + 'Failed to obtain access token. The ' . $providerName . ' OAuth2 provider returned an error: ' . $ex->getMessage(), + $ex->getCode(), + ); } $oauth2ID = $oauth2->getUserID($accessToken); - if (empty($oauth2ID)) { - if (!empty($state['failure'])) { - $response->redirect($state['failure'], 301, 0); - } - - throw new Exception(Exception::USER_MISSING_ID); + $failureRedirect(Exception::USER_MISSING_ID); } $sessions = $user->getAttribute('sessions', []); @@ -515,7 +538,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $total = $dbForProject->count('users', max: APP_LIMIT_USERS); if ($total >= $limit) { - throw new Exception(Exception::USER_COUNT_EXCEEDED); + $failureRedirect(Exception::USER_COUNT_EXCEEDED); } } @@ -550,13 +573,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ]); Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); } catch (Duplicate $th) { - throw new Exception(Exception::USER_ALREADY_EXISTS); + $failureRedirect(Exception::USER_ALREADY_EXISTS); } } } if (false === $user->getAttribute('status')) { // Account is blocked - throw new Exception(Exception::USER_BLOCKED); // User is in status blocked + $failureRedirect(Exception::USER_BLOCKED); // User is in status blocked } // Create session token, verify user account and update OAuth2 ID and Access Token diff --git a/src/Appwrite/Auth/OAuth2.php b/src/Appwrite/Auth/OAuth2.php index 28de8dd914..c737e183f8 100644 --- a/src/Appwrite/Auth/OAuth2.php +++ b/src/Appwrite/Auth/OAuth2.php @@ -2,6 +2,8 @@ namespace Appwrite\Auth; +use Appwrite\Auth\OAuth2\Exception; + abstract class OAuth2 { /** @@ -75,7 +77,7 @@ abstract class OAuth2 /** * @param string $accessToken - * + * * @return string */ abstract public function getUserID(string $accessToken): string; @@ -202,8 +204,14 @@ abstract class OAuth2 // Send the request & save response to $response $response = \curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + \curl_close($ch); + if ($code != 200) { + throw new Exception($response, $code); + } + return (string)$response; } }