From 8afbdb03a81c74d22ac065aaa4cfe2f397459d1d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 3 Jan 2025 19:01:29 -0700 Subject: [PATCH] Fix oauth oob (urn:ietf:wg:oauth:2.0:oob) support. Fixes #2522 --- .../OAuth/OobAuthorizationController.php | 99 +++++++++++++++++++ app/Providers/AuthServiceProvider.php | 1 + routes/web.php | 87 ++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 app/Http/Controllers/OAuth/OobAuthorizationController.php diff --git a/app/Http/Controllers/OAuth/OobAuthorizationController.php b/app/Http/Controllers/OAuth/OobAuthorizationController.php new file mode 100644 index 000000000..f69368b79 --- /dev/null +++ b/app/Http/Controllers/OAuth/OobAuthorizationController.php @@ -0,0 +1,99 @@ +assertValidAuthToken($request); + + $authRequest = $this->getAuthRequestFromSession($request); + $authRequest->setAuthorizationApproved(true); + + return $this->withErrorHandling(function () use ($authRequest) { + $response = $this->server->completeAuthorizationRequest($authRequest, new Psr7Response); + + if ($this->isOutOfBandRequest($authRequest)) { + $code = $this->extractAuthorizationCode($response); + return response()->json([ + 'code' => $code, + 'state' => $authRequest->getState() + ]); + } + + return $this->convertResponse($response); + }); + } + + /** + * Check if the request is an out-of-band OAuth request. + * + * @param \League\OAuth2\Server\RequestTypes\AuthorizationRequest $authRequest + * @return bool + */ + protected function isOutOfBandRequest($authRequest) + { + return $authRequest->getRedirectUri() === 'urn:ietf:wg:oauth:2.0:oob'; + } + + /** + * Extract the authorization code from the PSR-7 response. + * + * @param \Psr\Http\Message\ResponseInterface $response + * @return string + * @throws \League\OAuth2\Server\Exception\OAuthServerException + */ + protected function extractAuthorizationCode($response) + { + $location = $response->getHeader('Location')[0] ?? ''; + + if (empty($location)) { + throw OAuthServerException::serverError('Missing authorization code in response'); + } + + parse_str(parse_url($location, PHP_URL_QUERY), $params); + + if (!isset($params['code'])) { + throw OAuthServerException::serverError('Invalid authorization code format'); + } + + return $params['code']; + } + + /** + * Handle OAuth errors for both redirect and OOB flows. + * + * @param \Closure $callback + * @return \Illuminate\Http\Response + */ + protected function withErrorHandling($callback) + { + try { + return $callback(); + } catch (OAuthServerException $e) { + if ($this->isOutOfBandRequest($this->getAuthRequestFromSession(request()))) { + return response()->json([ + 'error' => $e->getErrorType(), + 'message' => $e->getMessage(), + 'hint' => $e->getHint() + ], $e->getHttpStatusCode()); + } + + return $this->convertResponse( + $e->generateHttpResponse(new Psr7Response) + ); + } + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index dfd4518c3..8bedbfd53 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -25,6 +25,7 @@ class AuthServiceProvider extends ServiceProvider public function boot() { if(config('pixelfed.oauth_enabled') == true) { + Passport::ignoreRoutes(); Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356))); Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400))); Passport::enableImplicitGrant(); diff --git a/routes/web.php b/routes/web.php index de2143d87..b9fbad7d9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -45,6 +45,93 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('auth/forgot/email', 'UserEmailForgotController@index')->name('email.forgot'); Route::post('auth/forgot/email', 'UserEmailForgotController@store')->middleware('throttle:10,900,forgotEmail'); + Route::group([ + 'as' => 'passport.', + 'prefix' => config('passport.path', 'oauth'), + ], function () { + Route::post('/token', [ + 'uses' => '\Laravel\Passport\Http\Controllers\AccessTokenController@issueToken', + 'as' => 'token', + 'middleware' => 'throttle', + ]); + + Route::get('/authorize', [ + 'uses' => '\Laravel\Passport\Http\Controllers\AuthorizationController@authorize', + 'as' => 'authorizations.authorize', + 'middleware' => 'web', + ]); + + $guard = config('passport.guard', null); + + Route::middleware(['web', $guard ? 'auth:'.$guard : 'auth'])->group(function () { + Route::post('/token/refresh', [ + 'uses' => '\Laravel\Passport\Http\Controllers\TransientTokenController@refresh', + 'as' => 'token.refresh', + ]); + + Route::post('/authorize', [ + 'uses' => '\App\Http\Controllers\OAuth\OobAuthorizationController@approve', + 'as' => 'authorizations.approve', + ]); + + Route::delete('/authorize', [ + 'uses' => '\Laravel\Passport\Http\Controllers\DenyAuthorizationController@deny', + 'as' => 'authorizations.deny', + ]); + + Route::get('/tokens', [ + 'uses' => '\Laravel\Passport\Http\Controllers\AuthorizedAccessTokenController@forUser', + 'as' => 'tokens.index', + ]); + + Route::delete('/tokens/{token_id}', [ + 'uses' => '\Laravel\Passport\Http\Controllers\AuthorizedAccessTokenController@destroy', + 'as' => 'tokens.destroy', + ]); + + Route::get('/clients', [ + 'uses' => '\Laravel\Passport\Http\Controllers\ClientController@forUser', + 'as' => 'clients.index', + ]); + + Route::post('/clients', [ + 'uses' => '\Laravel\Passport\Http\Controllers\ClientController@store', + 'as' => 'clients.store', + ]); + + Route::put('/clients/{client_id}', [ + 'uses' => '\Laravel\Passport\Http\Controllers\ClientController@update', + 'as' => 'clients.update', + ]); + + Route::delete('/clients/{client_id}', [ + 'uses' => '\Laravel\Passport\Http\Controllers\ClientController@destroy', + 'as' => 'clients.destroy', + ]); + + Route::get('/scopes', [ + 'uses' => '\Laravel\Passport\Http\Controllers\ScopeController@all', + 'as' => 'scopes.index', + ]); + + Route::get('/personal-access-tokens', [ + 'uses' => '\Laravel\Passport\Http\Controllers\PersonalAccessTokenController@forUser', + 'as' => 'personal.tokens.index', + ]); + + Route::post('/personal-access-tokens', [ + 'uses' => '\Laravel\Passport\Http\Controllers\PersonalAccessTokenController@store', + 'as' => 'personal.tokens.store', + ]); + + Route::delete('/personal-access-tokens/{token_id}', [ + 'uses' => '\Laravel\Passport\Http\Controllers\PersonalAccessTokenController@destroy', + 'as' => 'personal.tokens.destroy', + ]); + }); + + }); + Route::get('discover', 'DiscoverController@home')->name('discover'); Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags');