From 6142f9c3036eafc55e1ab854e0ed0a22e34e9a39 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Wed, 18 Sep 2024 21:02:41 +0330 Subject: [PATCH] [13.x] Make Passport headless (Support Laravel Jetstream and Breeze) (#1771) * add stubs * fix styling * formatting * add support for Jetstream * formatting * formatting * wip * add client revoke * wip * fix view response * move stubs to related repos * wip * fix URI rule * wip * wip * formatting * formatting * add upgrade guide * move action classes to Jetstream * formatting * wip * revert prev commit * fix first party check * fix tests --- UPGRADE.md | 13 +++ resources/views/authorize.blade.php | 93 ------------------- src/Client.php | 6 +- src/Contracts/AuthorizationViewResponse.php | 5 +- src/HasApiTokens.php | 10 ++ ...iewResponse.php => SimpleViewResponse.php} | 27 ++---- src/Passport.php | 58 ++++++++++-- src/PassportServiceProvider.php | 17 ---- tests/Unit/AuthorizationControllerTest.php | 14 +-- .../AuthorizedAccessTokenControllerTest.php | 1 + 10 files changed, 92 insertions(+), 152 deletions(-) delete mode 100644 resources/views/authorize.blade.php rename src/Http/Responses/{AuthorizationViewResponse.php => SimpleViewResponse.php} (63%) diff --git a/UPGRADE.md b/UPGRADE.md index 0d5e70e84..c26ef4184 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -22,6 +22,19 @@ PR: https://github.com/laravel/passport/pull/1734 The `league/oauth2-server` Composer package which is utilized internally by Passport has been updated to 9.0, which adds additional types to method signatures. To ensure your application is compatible, you should review this package's complete [changelog](https://github.com/thephpleague/oauth2-server/blob/master/CHANGELOG.md#900---released-2024-05-13). +### Headless + +PR: https://github.com/laravel/passport/pull/1771 + +Passport's views were not rendering properly for several release cycles. Passport is now a headless OAuth2 library. If you would like a frontend implementation of Laravel Passport's OAuth features that are already completed for you, you should use an [application starter kit](https://laravel.com/docs/11.x/starter-kits). + +All the authorization view's rendering logic may be customized using the appropriate methods available via the `Laravel\Passport\Passport` class. Typically, you should call these methods within the `boot` method of your application's `App\Providers\AppServiceProvider` class. Passport will take care of defining the routes that return these views: + + public function boot(): void + { + Passport::authorizationView('auth.oauth.authorize'); + } + ### Identify Clients by UUIDs PR: https://github.com/laravel/passport/pull/1764 diff --git a/resources/views/authorize.blade.php b/resources/views/authorize.blade.php deleted file mode 100644 index d0a4a991c..000000000 --- a/resources/views/authorize.blade.php +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - {{ config('app.name') }} - Authorization - - - - - - - -
-
-
-
-
- Authorization Request -
-
- -

{{ $client->name }} is requesting permission to access your account.

- - - @if (count($scopes) > 0) -
-

This application will be able to:

- -
    - @foreach ($scopes as $scope) -
  • {{ $scope->description }}
  • - @endforeach -
-
- @endif - -
- -
- @csrf - - - - - -
- - -
- @csrf - @method('DELETE') - - - - - -
-
-
-
-
-
-
- - diff --git a/src/Client.php b/src/Client.php index f211670dc..3c3e2d4d9 100644 --- a/src/Client.php +++ b/src/Client.php @@ -153,12 +153,10 @@ protected function redirectUris(): Attribute /** * Determine if the client is a "first party" client. - * - * @return bool */ - public function firstParty() + public function firstParty(): bool { - return $this->hasGrantType('personal_access') || $this->hasGrantType('password'); + return empty($this->user_id); } /** diff --git a/src/Contracts/AuthorizationViewResponse.php b/src/Contracts/AuthorizationViewResponse.php index 6594c6624..54369be63 100644 --- a/src/Contracts/AuthorizationViewResponse.php +++ b/src/Contracts/AuthorizationViewResponse.php @@ -9,8 +9,7 @@ interface AuthorizationViewResponse extends Responsable /** * Specify the parameters that should be passed to the view. * - * @param array $parameters - * @return $this + * @param array $parameters */ - public function withParameters($parameters = []); + public function withParameters(array $parameters = []): static; } diff --git a/src/HasApiTokens.php b/src/HasApiTokens.php index c7b5742a2..8d09a4598 100644 --- a/src/HasApiTokens.php +++ b/src/HasApiTokens.php @@ -55,6 +55,16 @@ public function token() return $this->accessToken; } + /** + * Get the access token currently associated with the user. + * + * @return \Laravel\Passport\AccessToken|\Laravel\Passport\TransientToken|null + */ + public function currentAccessToken() + { + return $this->token(); + } + /** * Determine if the current API token has a given scope. * diff --git a/src/Http/Responses/AuthorizationViewResponse.php b/src/Http/Responses/SimpleViewResponse.php similarity index 63% rename from src/Http/Responses/AuthorizationViewResponse.php rename to src/Http/Responses/SimpleViewResponse.php index 36761d486..7d1335d19 100644 --- a/src/Http/Responses/AuthorizationViewResponse.php +++ b/src/Http/Responses/SimpleViewResponse.php @@ -2,43 +2,32 @@ namespace Laravel\Passport\Http\Responses; +use Closure; use Illuminate\Contracts\Support\Responsable; -use Laravel\Passport\Contracts\AuthorizationViewResponse as AuthorizationViewResponseContract; +use Laravel\Passport\Contracts\AuthorizationViewResponse; -class AuthorizationViewResponse implements AuthorizationViewResponseContract +class SimpleViewResponse implements AuthorizationViewResponse { - /** - * The name of the view or the callable used to generate the view. - * - * @var string - */ - protected $view; - /** * An array of arguments that may be passed to the view response and used in the view. * - * @var string + * @var array */ - protected $parameters; + protected array $parameters = []; /** * Create a new response instance. - * - * @param callable|string $view - * @return void */ - public function __construct($view) + public function __construct(protected Closure|string $view) { - $this->view = $view; } /** * Add parameters to response. * - * @param array $parameters - * @return $this + * @param array $parameters */ - public function withParameters($parameters = []) + public function withParameters(array $parameters = []): static { $this->parameters = $parameters; diff --git a/src/Passport.php b/src/Passport.php index d24412f5b..561c62da7 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -3,11 +3,12 @@ namespace Laravel\Passport; use Carbon\Carbon; +use Closure; use DateInterval; use DateTimeInterface; use Illuminate\Contracts\Encryption\Encrypter; -use Laravel\Passport\Contracts\AuthorizationViewResponse as AuthorizationViewResponseContract; -use Laravel\Passport\Http\Responses\AuthorizationViewResponse; +use Laravel\Passport\Contracts\AuthorizationViewResponse; +use Laravel\Passport\Http\Responses\SimpleViewResponse; use League\OAuth2\Server\ResourceServer; use Mockery; use Psr\Http\Message\ServerRequestInterface; @@ -204,6 +205,8 @@ public static function enablePasswordGrant() /** * Set the default scope(s). Multiple scopes may be an array or specified delimited by spaces. * + * @deprecated Use defaultScopes. + * * @param array|string $scope * @return void */ @@ -212,6 +215,32 @@ public static function setDefaultScope($scope) static::$defaultScope = is_array($scope) ? implode(' ', $scope) : $scope; } + /** + * Set or get the default scopes. + * + * @param string[]|string|null $scopes + * @return string[] + */ + public static function defaultScopes(array|string|null $scopes = null): array + { + if (! is_null($scopes)) { + static::$defaultScope = is_array($scopes) ? implode(' ', $scopes) : $scopes; + } + + return static::$defaultScope ? explode(' ', static::$defaultScope) : []; + } + + /** + * Return the scopes in the given list that are actually defined scopes for the application. + * + * @param string[] $scopes + * @return string[] + */ + public static function validScopes(array $scopes): array + { + return array_values(array_unique(array_intersect($scopes, array_keys(static::$scopes)))); + } + /** * Get all of the defined scope IDs. * @@ -599,17 +628,28 @@ public static function tokenEncryptionKey(Encrypter $encrypter) $encrypter->getKey(); } + /** + * Register the views for Passport using conventional names under the given namespace. + */ + public static function viewNamespace(string $namespace): void + { + static::viewPrefix($namespace.'::'); + } + + /** + * Register the views for Passport using conventional names under the given prefix. + */ + public static function viewPrefix(string $prefix): void + { + static::authorizationView($prefix.'authorize'); + } + /** * Specify which view should be used as the authorization view. - * - * @param callable|string $view - * @return void */ - public static function authorizationView($view) + public static function authorizationView(Closure|string $view): void { - app()->singleton(AuthorizationViewResponseContract::class, function ($app) use ($view) { - return new AuthorizationViewResponse($view); - }); + app()->singleton(AuthorizationViewResponse::class, fn () => new SimpleViewResponse($view)); } /** diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index 38e9f8faf..f2cce1c96 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -38,7 +38,6 @@ class PassportServiceProvider extends ServiceProvider public function boot() { $this->registerRoutes(); - $this->registerResources(); $this->registerPublishing(); $this->registerCommands(); @@ -63,16 +62,6 @@ protected function registerRoutes() } } - /** - * Register the Passport resources. - * - * @return void - */ - protected function registerResources() - { - $this->loadViewsFrom(__DIR__.'/../resources/views', 'passport'); - } - /** * Register the package's publishable resources. * @@ -89,10 +78,6 @@ protected function registerPublishing() __DIR__.'/../database/migrations' => database_path('migrations'), ], 'passport-migrations'); - $this->publishes([ - __DIR__.'/../resources/views' => base_path('resources/views/vendor/passport'), - ], 'passport-views'); - $this->publishes([ __DIR__.'/../config/passport.php' => config_path('passport.php'), ], 'passport-config'); @@ -134,8 +119,6 @@ public function register() $this->registerJWTParser(); $this->registerResourceServer(); $this->registerGuard(); - - Passport::authorizationView('passport::authorize'); } /** diff --git a/tests/Unit/AuthorizationControllerTest.php b/tests/Unit/AuthorizationControllerTest.php index b57d0a1d1..cb925834a 100644 --- a/tests/Unit/AuthorizationControllerTest.php +++ b/tests/Unit/AuthorizationControllerTest.php @@ -8,10 +8,10 @@ use Laravel\Passport\Bridge\Scope; use Laravel\Passport\Client; use Laravel\Passport\ClientRepository; +use Laravel\Passport\Contracts\AuthorizationViewResponse; use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Exceptions\OAuthServerException; use Laravel\Passport\Http\Controllers\AuthorizationController; -use Laravel\Passport\Http\Responses\AuthorizationViewResponse; use Laravel\Passport\Passport; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException as LeagueException; @@ -63,16 +63,16 @@ public function test_authorization_view_is_presented() $user->shouldReceive('getAuthIdentifier')->andReturn(1); - $response->shouldReceive('withParameters')->once()->andReturnUsing(function ($data) use ($client, $user, $request) { + $response->shouldReceive('withParameters')->once()->andReturnUsing(function ($data) use ($client, $user, $request, $response) { $this->assertEquals($client, $data['client']); $this->assertEquals($user, $data['user']); $this->assertEquals($request, $data['request']); $this->assertSame('description', $data['scopes'][0]->description); - return 'view'; + return $response; }); - $this->assertSame('view', $controller->authorize( + $this->assertSame($response, $controller->authorize( m::mock(ServerRequestInterface::class), $request, $clients )); } @@ -221,16 +221,16 @@ public function test_authorization_view_is_presented_if_request_has_prompt_equal $clients->shouldReceive('find')->with(1)->andReturn($client = m::mock(Client::class)); $client->shouldReceive('skipsAuthorization')->andReturn(false); - $response->shouldReceive('withParameters')->once()->andReturnUsing(function ($data) use ($client, $user, $request) { + $response->shouldReceive('withParameters')->once()->andReturnUsing(function ($data) use ($client, $user, $request, $response) { $this->assertEquals($client, $data['client']); $this->assertEquals($user, $data['user']); $this->assertEquals($request, $data['request']); $this->assertSame('description', $data['scopes'][0]->description); - return 'view'; + return $response; }); - $this->assertSame('view', $controller->authorize( + $this->assertSame($response, $controller->authorize( m::mock(ServerRequestInterface::class), $request, $clients )); } diff --git a/tests/Unit/AuthorizedAccessTokenControllerTest.php b/tests/Unit/AuthorizedAccessTokenControllerTest.php index 2846918bb..1fc48c716 100644 --- a/tests/Unit/AuthorizedAccessTokenControllerTest.php +++ b/tests/Unit/AuthorizedAccessTokenControllerTest.php @@ -55,6 +55,7 @@ public function test_tokens_can_be_retrieved_for_users() $client1->grant_types = ['personal_access']; $client2 = new Client; $client2->grant_types = []; + $client2->user_id = 2; $token1->client = $client1; $token2->client = $client2; $userTokens->shouldReceive('load')->with('client')->andReturn(collect([