Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[13.x] Make Passport headless (Support Laravel Jetstream and Breeze) #1771

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 0 additions & 93 deletions resources/views/authorize.blade.php

This file was deleted.

6 changes: 2 additions & 4 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
5 changes: 2 additions & 3 deletions src/Contracts/AuthorizationViewResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, mixed> $parameters
*/
public function withParameters($parameters = []);
public function withParameters(array $parameters = []): static;
}
10 changes: 10 additions & 0 deletions src/HasApiTokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, mixed>
*/
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<string, mixed> $parameters
*/
public function withParameters($parameters = [])
public function withParameters(array $parameters = []): static
{
$this->parameters = $parameters;

Expand Down
58 changes: 49 additions & 9 deletions src/Passport.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand All @@ -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.
*
Expand Down Expand Up @@ -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));
}

/**
Expand Down
17 changes: 0 additions & 17 deletions src/PassportServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ class PassportServiceProvider extends ServiceProvider
public function boot()
{
$this->registerRoutes();
$this->registerResources();
$this->registerPublishing();
$this->registerCommands();

Expand All @@ -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.
*
Expand All @@ -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');
Expand Down Expand Up @@ -134,8 +119,6 @@ public function register()
$this->registerJWTParser();
$this->registerResourceServer();
$this->registerGuard();

Passport::authorizationView('passport::authorize');
}

/**
Expand Down
14 changes: 7 additions & 7 deletions tests/Unit/AuthorizationControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
));
}
Expand Down Expand Up @@ -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
));
}
Expand Down
Loading