diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e54dde560..b30cbf405 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,14 +29,14 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + extensions: dom, curl, libxml, mbstring, zip ini-values: error_reporting=E_ALL tools: composer:v2 coverage: none - name: Install dependencies run: | - composer update --prefer-dist --no-interaction --no-progress --with="illuminate/contracts=^${{ matrix.laravel }}" + composer update --prefer-dist --no-interaction --no-progress --with="illuminate/contracts=^${{ matrix.laravel }}" - name: Execute tests - run: vendor/bin/phpunit ${{ matrix.laravel >= 10 && '--display-deprecations' || '' }} + run: vendor/bin/phpunit --display-deprecations --display-phpunit-deprecations diff --git a/UPGRADE.md b/UPGRADE.md index b83baad38..74802492d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -12,9 +12,9 @@ PHP 8.2 is now the minimum required version. ### Minimum Laravel Version -PR: https://github.com/laravel/passport/pull/1757, https://github.com/laravel/passport/pull/1783 +PR: https://github.com/laravel/passport/pull/1757, https://github.com/laravel/passport/pull/1783, https://github.com/laravel/passport/pull/1797 -Laravel 11.14 is now the minimum required version. +Laravel 11.35 is now the minimum required version. ### OAuth2 Server diff --git a/composer.json b/composer.json index 0113b913c..31a534965 100644 --- a/composer.json +++ b/composer.json @@ -18,27 +18,27 @@ "ext-json": "*", "ext-openssl": "*", "firebase/php-jwt": "^6.4", - "illuminate/auth": "^11.14|^12.0", - "illuminate/console": "^11.14|^12.0", - "illuminate/container": "^11.14|^12.0", - "illuminate/contracts": "^11.14|^12.0", - "illuminate/cookie": "^11.14|^12.0", - "illuminate/database": "^11.14|^12.0", - "illuminate/encryption": "^11.14|^12.0", - "illuminate/http": "^11.14|^12.0", - "illuminate/support": "^11.14|^12.0", - "lcobucci/jwt": "^5.0", - "league/oauth2-server": "^9.0", - "nyholm/psr7": "^1.5", + "illuminate/auth": "^11.35|^12.0", + "illuminate/console": "^11.35|^12.0", + "illuminate/container": "^11.35|^12.0", + "illuminate/contracts": "^11.35|^12.0", + "illuminate/cookie": "^11.35|^12.0", + "illuminate/database": "^11.35|^12.0", + "illuminate/encryption": "^11.35|^12.0", + "illuminate/http": "^11.35|^12.0", + "illuminate/support": "^11.35|^12.0", + "league/oauth2-server": "^9.2", + "php-http/discovery": "^1.20", "phpseclib/phpseclib": "^3.0", - "symfony/console": "^7.0", + "psr/http-factory-implementation": "*", + "symfony/console": "^7.1", "symfony/psr-http-message-bridge": "^7.1" }, "require-dev": { - "mockery/mockery": "^1.0", - "orchestra/testbench": "^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5|^11.5" + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.9|^10.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5|^12.0" }, "autoload": { "psr-4": { @@ -61,7 +61,10 @@ } }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "php-http/discovery": false + } }, "scripts": { "post-autoload-dump": "@prepare", diff --git a/database/factories/ClientFactory.php b/database/factories/ClientFactory.php index b5a9229e6..0278e22e5 100644 --- a/database/factories/ClientFactory.php +++ b/database/factories/ClientFactory.php @@ -29,7 +29,8 @@ public function modelName(): string public function definition(): array { return [ - 'user_id' => null, + 'owner_id' => null, + 'owner_type' => null, 'name' => $this->faker->company(), 'secret' => Str::random(40), 'redirect_uris' => [$this->faker->url()], diff --git a/database/migrations/2016_06_01_000004_create_oauth_clients_table.php b/database/migrations/2016_06_01_000004_create_oauth_clients_table.php index 7068d9239..9794dc860 100644 --- a/database/migrations/2016_06_01_000004_create_oauth_clients_table.php +++ b/database/migrations/2016_06_01_000004_create_oauth_clients_table.php @@ -13,7 +13,7 @@ public function up(): void { Schema::create('oauth_clients', function (Blueprint $table) { $table->uuid('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); + $table->nullableMorphs('owner'); $table->string('name'); $table->string('secret')->nullable(); $table->string('provider')->nullable(); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d01804708..c146672cf 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,12 @@ - + ./tests/Unit diff --git a/src/AccessToken.php b/src/AccessToken.php index 0fdff78c7..13be7ab32 100644 --- a/src/AccessToken.php +++ b/src/AccessToken.php @@ -6,39 +6,39 @@ use Illuminate\Contracts\Support\Jsonable; use Illuminate\Support\Traits\ForwardsCalls; use JsonSerializable; +use Laravel\Passport\Contracts\ScopeAuthorizable; use Psr\Http\Message\ServerRequestInterface; /** - * @template TKey of string * @template TValue * - * @implements \Illuminate\Contracts\Support\Arrayable + * @implements \Illuminate\Contracts\Support\Arrayable * * @property string $oauth_access_token_id * @property string $oauth_client_id * @property string $oauth_user_id * @property string[] $oauth_scopes */ -class AccessToken implements Arrayable, Jsonable, JsonSerializable +class AccessToken implements ScopeAuthorizable, Arrayable, Jsonable, JsonSerializable { use ResolvesInheritedScopes, ForwardsCalls; /** * The token instance. */ - protected ?Token $token; + protected ?Token $token = null; /** * All the attributes set on the access token instance. * - * @var array + * @var array */ protected array $attributes = []; /** * Create a new access token instance. * - * @param array $attributes + * @param array $attributes */ public function __construct(array $attributes = []) { @@ -60,7 +60,7 @@ public static function fromPsrRequest(ServerRequestInterface $request): static */ public function can(string $scope): bool { - return in_array('*', $this->oauth_scopes) || $this->scopeExists($scope, $this->oauth_scopes); + return in_array('*', $this->oauth_scopes) || $this->scopeExistsIn($scope, $this->oauth_scopes); } /** @@ -98,7 +98,7 @@ protected function getToken(): ?Token /** * Convert the access token instance to an array. * - * @return array + * @return array */ public function toArray(): array { @@ -108,7 +108,7 @@ public function toArray(): array /** * Convert the object into something JSON serializable. * - * @return array + * @return array */ public function jsonSerialize(): array { diff --git a/src/ApiTokenCookieFactory.php b/src/ApiTokenCookieFactory.php index dfee08068..c298bf817 100644 --- a/src/ApiTokenCookieFactory.php +++ b/src/ApiTokenCookieFactory.php @@ -2,10 +2,10 @@ namespace Laravel\Passport; -use Carbon\Carbon; use Firebase\JWT\JWT; use Illuminate\Contracts\Config\Repository as Config; use Illuminate\Contracts\Encryption\Encrypter; +use Illuminate\Support\Facades\Date; use Symfony\Component\HttpFoundation\Cookie; class ApiTokenCookieFactory @@ -26,7 +26,7 @@ public function make(string|int $userId, string $csrfToken): Cookie { $config = $this->config->get('session'); - $expiration = Carbon::now()->addMinutes((int) $config['lifetime']); + $expiration = Date::now()->addMinutes((int) $config['lifetime'])->getTimestamp(); return new Cookie( Passport::cookie(), @@ -37,19 +37,20 @@ public function make(string|int $userId, string $csrfToken): Cookie $config['secure'], true, false, - $config['same_site'] ?? null + $config['same_site'] ?? null, + $config['partitioned'] ?? false ); } /** * Create a new JWT token for the given user ID and CSRF token. */ - protected function createToken(string|int $userId, string $csrfToken, Carbon $expiration): string + protected function createToken(string|int $userId, string $csrfToken, int $expiration): string { return JWT::encode([ 'sub' => $userId, 'csrf' => $csrfToken, - 'expiry' => $expiration->getTimestamp(), + 'exp' => $expiration, ], Passport::tokenEncryptionKey($this->encrypter), 'HS256'); } } diff --git a/src/AuthCode.php b/src/AuthCode.php index 4733db98e..1318ee4c4 100644 --- a/src/AuthCode.php +++ b/src/AuthCode.php @@ -31,7 +31,7 @@ class AuthCode extends Model /** * The attributes that should be cast to native types. * - * @var array + * @var array */ protected $casts = [ 'revoked' => 'bool', diff --git a/src/Bridge/AccessTokenRepository.php b/src/Bridge/AccessTokenRepository.php index 7eb243bca..3eab0cdf4 100644 --- a/src/Bridge/AccessTokenRepository.php +++ b/src/Bridge/AccessTokenRepository.php @@ -2,7 +2,6 @@ namespace Laravel\Passport\Bridge; -use DateTime; use Illuminate\Contracts\Events\Dispatcher; use Laravel\Passport\Events\AccessTokenCreated; use Laravel\Passport\Events\AccessTokenRevoked; @@ -13,8 +12,6 @@ class AccessTokenRepository implements AccessTokenRepositoryInterface { - use FormatsScopesForStorage; - /** * Create a new repository instance. */ @@ -39,16 +36,14 @@ public function getNewToken( */ public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity): void { - Passport::token()->newQuery()->create([ + Passport::token()->forceFill([ 'id' => $id = $accessTokenEntity->getIdentifier(), 'user_id' => $userId = $accessTokenEntity->getUserIdentifier(), 'client_id' => $clientId = $accessTokenEntity->getClient()->getIdentifier(), - 'scopes' => $this->scopesToArray($accessTokenEntity->getScopes()), + 'scopes' => $accessTokenEntity->getScopes(), 'revoked' => false, - 'created_at' => new DateTime, - 'updated_at' => new DateTime, 'expires_at' => $accessTokenEntity->getExpiryDateTime(), - ]); + ])->save(); $this->events->dispatch(new AccessTokenCreated($id, $userId, $clientId)); } diff --git a/src/Bridge/AuthCodeRepository.php b/src/Bridge/AuthCodeRepository.php index 036a6be1d..941cb75fb 100644 --- a/src/Bridge/AuthCodeRepository.php +++ b/src/Bridge/AuthCodeRepository.php @@ -8,8 +8,6 @@ class AuthCodeRepository implements AuthCodeRepositoryInterface { - use FormatsScopesForStorage; - /** * {@inheritdoc} */ @@ -27,7 +25,7 @@ public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): voi 'id' => $authCodeEntity->getIdentifier(), 'user_id' => $authCodeEntity->getUserIdentifier(), 'client_id' => $authCodeEntity->getClient()->getIdentifier(), - 'scopes' => $this->formatScopesForStorage($authCodeEntity->getScopes()), + 'scopes' => json_encode($authCodeEntity->getScopes()), 'revoked' => false, 'expires_at' => $authCodeEntity->getExpiryDateTime(), ])->save(); diff --git a/src/Bridge/FormatsScopesForStorage.php b/src/Bridge/FormatsScopesForStorage.php deleted file mode 100644 index 7abea9cc1..000000000 --- a/src/Bridge/FormatsScopesForStorage.php +++ /dev/null @@ -1,29 +0,0 @@ -scopesToArray($scopes)); - } - - /** - * Get an array of scope identifiers for storage. - * - * @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes - * @return string[] - */ - public function scopesToArray(array $scopes): array - { - return array_map(fn (ScopeEntityInterface $scope): string => $scope->getIdentifier(), $scopes); - } -} diff --git a/src/Bridge/PersonalAccessBearerTokenResponse.php b/src/Bridge/PersonalAccessBearerTokenResponse.php new file mode 100644 index 000000000..d4e47fe93 --- /dev/null +++ b/src/Bridge/PersonalAccessBearerTokenResponse.php @@ -0,0 +1,19 @@ + $accessToken->getIdentifier(), + ]; + } +} diff --git a/src/Bridge/PersonalAccessGrant.php b/src/Bridge/PersonalAccessGrant.php index 2b1d81630..0b3b06a74 100644 --- a/src/Bridge/PersonalAccessGrant.php +++ b/src/Bridge/PersonalAccessGrant.php @@ -3,8 +3,11 @@ namespace Laravel\Passport\Bridge; use DateInterval; +use Laravel\Passport\Passport; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\AbstractGrant; +use League\OAuth2\Server\RequestAccessTokenEvent; +use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ServerRequestInterface; @@ -47,6 +50,14 @@ public function respondToAccessTokenRequest( $scopes ); + // Send event to emitter + $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); + + // Persist access token's name + Passport::token()->newQuery()->whereKey($accessToken->getIdentifier())->update([ + 'name' => $this->getRequestParameter('name', $request), + ]); + // Inject access token into response type $responseType->setAccessToken($accessToken); diff --git a/src/Bridge/RefreshTokenRepository.php b/src/Bridge/RefreshTokenRepository.php index c798fb508..48beb70ce 100644 --- a/src/Bridge/RefreshTokenRepository.php +++ b/src/Bridge/RefreshTokenRepository.php @@ -31,12 +31,12 @@ public function getNewRefreshToken(): ?RefreshTokenEntityInterface */ public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void { - Passport::refreshToken()->newQuery()->create([ + Passport::refreshToken()->forceFill([ 'id' => $id = $refreshTokenEntity->getIdentifier(), 'access_token_id' => $accessTokenId = $refreshTokenEntity->getAccessToken()->getIdentifier(), 'revoked' => false, 'expires_at' => $refreshTokenEntity->getExpiryDateTime(), - ]); + ])->save(); $this->events->dispatch(new RefreshTokenCreated($id, $accessTokenId)); } diff --git a/src/Client.php b/src/Client.php index c0bee6c54..dfd45389d 100644 --- a/src/Client.php +++ b/src/Client.php @@ -4,13 +4,13 @@ use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Str; +use Illuminate\Database\Eloquent\Relations\MorphTo; use Laravel\Passport\Database\Factories\ClientFactory; class Client extends Model @@ -18,6 +18,7 @@ class Client extends Model /** @use \Illuminate\Database\Eloquent\Factories\HasFactory<\Laravel\Passport\Database\Factories\ClientFactory> */ use HasFactory; use ResolvesInheritedScopes; + use HasUuids; /** * The database table used by the model. @@ -45,7 +46,7 @@ class Client extends Model /** * The attributes that should be cast to native types. * - * @var array + * @var array */ protected $casts = [ 'grant_types' => 'array', @@ -64,20 +65,18 @@ class Client extends Model public ?string $plainSecret = null; /** - * Create a new Eloquent model instance. - * - * @param array $attributes + * Initialize the trait. */ - public function __construct(array $attributes = []) + public function initializeHasUniqueStringIds(): void { - parent::__construct($attributes); - $this->usesUniqueIds = Passport::$clientUuids; } /** * Get the user that the client belongs to. * + * @deprecated Use owner() + * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Illuminate\Foundation\Auth\User, $this> */ public function user(): BelongsTo @@ -89,6 +88,16 @@ public function user(): BelongsTo ); } + /** + * Get the owner of the registered client. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Foundation\Auth\User, $this> + */ + public function owner(): MorphTo + { + return $this->morphTo('owner'); + } + /** * Get all of the authentication codes for the client. * @@ -112,37 +121,29 @@ public function tokens(): HasMany } /** - * The temporary non-hashed client secret. - * - * This is only available once during the request that created the client. + * Interact with the client's secret. */ - public function getPlainSecretAttribute(): ?string + protected function secret(): Attribute { - return $this->plainSecret; - } - - /** - * Set the value of the secret attribute. - */ - public function setSecretAttribute(?string $value): void - { - $this->plainSecret = $value; + return Attribute::make( + set: function (?string $value): ?string { + $this->plainSecret = $value; - $this->attributes['secret'] = is_null($value) ? $value : Hash::make($value); + return $this->castAttributeAsHashedString('secret', $value); + }, + ); } /** - * Get the client's redirect URIs. + * Interact with the client's redirect URIs. */ protected function redirectUris(): Attribute { return Attribute::make( - get: function (?string $value, array $attributes) { - if (isset($value)) { - return $this->fromJson($value); - } - - return empty($attributes['redirect']) ? [] : explode(',', $attributes['redirect']); + get: fn (?string $value, array $attributes): array => match (true) { + isset($value) => $this->fromJson($value), + ! empty($attributes['redirect']) => explode(',', $attributes['redirect']), + default => [], }, ); } @@ -170,7 +171,11 @@ protected function grantTypes(): Attribute */ public function firstParty(): bool { - return empty($this->user_id); + if (array_key_exists('user_id', $this->attributes)) { + return empty($this->user_id); + } + + return empty($this->owner_id); } /** @@ -196,7 +201,7 @@ public function hasGrantType(string $grantType): bool */ public function hasScope(string $scope): bool { - return ! isset($this->attributes['scopes']) || $this->scopeExists($scope, $this->scopes); + return ! isset($this->attributes['scopes']) || $this->scopeExistsIn($scope, $this->scopes); } /** @@ -207,40 +212,6 @@ public function confidential(): bool return ! empty($this->secret); } - /** - * Get the columns that should receive a unique identifier. - * - * @return array - */ - public function uniqueIds(): array - { - return $this->usesUniqueIds ? [$this->getKeyName()] : []; - } - - /** - * Generate a new key for the model. - */ - public function newUniqueId(): ?string - { - return $this->usesUniqueIds ? (string) Str::orderedUuid() : null; - } - - /** - * Get the auto-incrementing key type. - */ - public function getKeyType(): string - { - return $this->usesUniqueIds ? 'string' : $this->keyType; - } - - /** - * Get the value indicating whether the IDs are incrementing. - */ - public function getIncrementing(): bool - { - return $this->usesUniqueIds ? false : $this->incrementing; - } - /** * Get the current connection name for the model. */ diff --git a/src/ClientRepository.php b/src/ClientRepository.php index 2d94cd646..cd1482978 100644 --- a/src/ClientRepository.php +++ b/src/ClientRepository.php @@ -31,9 +31,9 @@ public function findActive(string|int $id): ?Client /** * Get a client instance for the given ID and user ID. * - * @deprecated Use $user->clients()->find() + * @deprecated Use $user->oauthApps()->find() * - * @param \Laravel\Passport\HasApiTokens $user + * @param \Laravel\Passport\Contracts\OAuthenticatable $user */ public function findForUser(string|int $clientId, Authenticatable $user): ?Client { @@ -43,9 +43,9 @@ public function findForUser(string|int $clientId, Authenticatable $user): ?Clien /** * Get the client instances for the given user ID. * - * @deprecated Use $user->clients() + * @deprecated Use $user->oauthApps() * - * @param \Laravel\Passport\HasApiTokens $user + * @param \Laravel\Passport\Contracts\OAuthenticatable $user * @return \Illuminate\Database\Eloquent\Collection */ public function forUser(Authenticatable $user): Collection @@ -63,15 +63,14 @@ public function personalAccessClient(string $provider): Client return Passport::client() ->newQuery() ->where('revoked', false) - ->whereNull('user_id') - ->where(function (Builder $query) use ($provider) { - $query->when($provider === config('auth.guards.api.provider'), function (Builder $query) { + ->where(function (Builder $query) use ($provider): void { + $query->when($provider === config('auth.guards.api.provider'), function (Builder $query): void { $query->orWhereNull('provider'); })->orWhere('provider', $provider); }) ->latest() ->get() - ->first(fn (Client $client) => $client->hasGrantType('personal_access')) + ->first(fn (Client $client): bool => $client->hasGrantType('personal_access')) ?? throw new RuntimeException( "Personal access client not found for '$provider' user provider. Please create one." ); @@ -82,7 +81,7 @@ public function personalAccessClient(string $provider): Client * * @param string[] $grantTypes * @param string[] $redirectUris - * @param \Laravel\Passport\HasApiTokens $user + * @param \Laravel\Passport\Contracts\OAuthenticatable|null $user */ protected function create( string $name, @@ -113,9 +112,11 @@ protected function create( ]), ]; - return $user - ? $user->clients()->forceCreate($attributes) - : $client->newQuery()->forceCreate($attributes); + return match (true) { + ! is_null($user) && in_array('user_id', $columns) => $user->clients()->forceCreate($attributes), + ! is_null($user) => $user->oauthApps()->forceCreate($attributes), + default => $client->newQuery()->forceCreate($attributes), + }; } /** @@ -155,7 +156,7 @@ public function createImplicitGrantClient(string $name, array $redirectUris): Cl /** * Store a new device authorization grant client. * - * @param \Laravel\Passport\HasApiTokens|null $user + * @param \Laravel\Passport\Contracts\OAuthenticatable|null $user */ public function createDeviceAuthorizationGrantClient( string $name, @@ -171,6 +172,7 @@ public function createDeviceAuthorizationGrantClient( * Store a new authorization code grant client. * * @param string[] $redirectUris + * @param \Laravel\Passport\Contracts\OAuthenticatable|null $user */ public function createAuthorizationCodeGrantClient( string $name, diff --git a/src/Console/ClientCommand.php b/src/Console/ClientCommand.php index 3a06ee5a1..bd63c8ebb 100644 --- a/src/Console/ClientCommand.php +++ b/src/Console/ClientCommand.php @@ -38,8 +38,8 @@ class ClientCommand extends Command */ public function handle(ClientRepository $clients): void { - if (! $this->hasOption('name')) { - $this->input->setOption('name', $this->ask( + if (! $this->option('name')) { + $this->input->setOption('name', $this->components->ask( 'What should we name the client?', config('app.name') )); @@ -71,7 +71,7 @@ public function handle(ClientRepository $clients): void */ protected function createPersonalAccessClient(ClientRepository $clients): ?Client { - $provider = $this->option('provider') ?: $this->choice( + $provider = $this->option('provider') ?: $this->components->choice( 'Which user provider should this client use to retrieve users?', collect(config('auth.guards'))->where('driver', 'passport')->pluck('provider')->all(), config('auth.guards.api.provider') @@ -87,7 +87,7 @@ protected function createPersonalAccessClient(ClientRepository $clients): ?Clien */ protected function createPasswordClient(ClientRepository $clients): Client { - $provider = $this->option('provider') ?: $this->choice( + $provider = $this->option('provider') ?: $this->components->choice( 'Which user provider should this client use to retrieve users?', collect(config('auth.guards'))->where('driver', 'passport')->pluck('provider')->all(), config('auth.guards.api.provider') @@ -95,7 +95,7 @@ protected function createPasswordClient(ClientRepository $clients): Client $confidential = $this->hasOption('public') ? ! $this->option('public') - : $this->confirm('Would you like to make this client confidential?'); + : $this->components->confirm('Would you like to make this client confidential?'); return $clients->createPasswordGrantClient($this->option('name'), $provider, $confidential); } @@ -113,7 +113,7 @@ protected function createClientCredentialsClient(ClientRepository $clients): Cli */ protected function createImplicitClient(ClientRepository $clients): Client { - $redirect = $this->option('redirect_uri') ?: $this->ask( + $redirect = $this->option('redirect_uri') ?: $this->components->ask( 'Where should we redirect the request after authorization?', url('/auth/callback') ); @@ -138,14 +138,14 @@ protected function createDeviceCodeClient(ClientRepository $clients): Client */ protected function createAuthCodeClient(ClientRepository $clients): Client { - $redirect = $this->option('redirect_uri') ?: $this->ask( + $redirect = $this->option('redirect_uri') ?: $this->components->ask( 'Where should we redirect the request after authorization?', url('/auth/callback') ); $confidential = $this->hasOption('public') ? ! $this->option('public') - : $this->confirm('Would you like to make this client confidential?', true); + : $this->components->confirm('Would you like to make this client confidential?', true); $enableDeviceFlow = $this->confirm('Would you like to enable the device authorization flow for this client?'); diff --git a/src/Console/HashCommand.php b/src/Console/HashCommand.php index 92a18e806..c21a825bd 100644 --- a/src/Console/HashCommand.php +++ b/src/Console/HashCommand.php @@ -30,7 +30,7 @@ class HashCommand extends Command public function handle(): void { if ($this->option('force') || - $this->confirm('Are you sure you want to hash all client secrets? This cannot be undone.')) { + $this->components->confirm('Are you sure you want to hash all client secrets? This cannot be undone.')) { foreach (Passport::client()->newQuery()->whereNotNull('secret')->cursor() as $client) { if (Hash::isHashed($client->secret) && ! Hash::needsRehash($client->secret)) { continue; diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 013201cf7..32bd7a391 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -37,10 +37,10 @@ public function handle(): void $this->call('vendor:publish', ['--tag' => 'passport-config']); $this->call('vendor:publish', ['--tag' => 'passport-migrations']); - if ($this->confirm('Would you like to run all pending database migrations?', true)) { + if ($this->components->confirm('Would you like to run all pending database migrations?', true)) { $this->call('migrate'); - if ($this->confirm('Would you like to create the "personal access" grant client?', true)) { + if ($this->components->confirm('Would you like to create the "personal access" grant client?', true)) { $this->call('passport:client', [ '--personal' => true, '--name' => config('app.name'), diff --git a/src/Console/PurgeCommand.php b/src/Console/PurgeCommand.php index 656cecef9..a258eb688 100644 --- a/src/Console/PurgeCommand.php +++ b/src/Console/PurgeCommand.php @@ -4,7 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Date; use Laravel\Passport\Passport; use Symfony\Component\Console\Attribute\AsCommand; @@ -36,10 +36,10 @@ public function handle(): void $revoked = $this->option('revoked') || ! $this->option('expired'); $expired = $this->option('expired') || ! $this->option('revoked') - ? Carbon::now()->subHours($this->option('hours')) + ? Date::now()->subHours($this->option('hours')) : false; - $constraint = fn (Builder $query) => $query + $constraint = fn (Builder $query): Builder => $query ->when($revoked, fn () => $query->orWhere('revoked', true)) ->when($expired, fn () => $query->orWhere('expires_at', '<', $expired)); diff --git a/src/Contracts/OAuthenticatable.php b/src/Contracts/OAuthenticatable.php new file mode 100644 index 000000000..3f724792d --- /dev/null +++ b/src/Contracts/OAuthenticatable.php @@ -0,0 +1,57 @@ + + */ + public function oauthApps(): MorphMany; + + /** + * Get all the access tokens for the user. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany<\Laravel\Passport\Token, \Illuminate\Foundation\Auth\User> + */ + public function tokens(): HasMany; + + /** + * Determine if the current API token has a given scope. + */ + public function tokenCan(string $scope): bool; + + /** + * Determine if the current API token is missing a given scope. + */ + public function tokenCant(string $scope): bool; + + /** + * Create a new personal access token for the user. + * + * @param string[] $scopes + */ + public function createToken(string $name, array $scopes = []): PersonalAccessTokenResult; + + /** + * Get the access token currently associated with the user. + */ + public function currentAccessToken(): ?ScopeAuthorizable; + + /** + * Set the current access token for the user. + */ + public function withAccessToken(?ScopeAuthorizable $accessToken): static; + + /** + * Get the user provider name. + */ + public function getProviderName(): string; +} diff --git a/src/Contracts/ScopeAuthorizable.php b/src/Contracts/ScopeAuthorizable.php new file mode 100644 index 000000000..24b7106cc --- /dev/null +++ b/src/Contracts/ScopeAuthorizable.php @@ -0,0 +1,16 @@ +createRequest($this->request); + $psr = (new PsrHttpFactory)->createRequest($this->request); try { return $this->server->validateAuthenticatedRequest($psr); @@ -176,6 +180,8 @@ protected function getPsrRequestViaBearerToken(): ?ServerRequestInterface /** * Authenticate the incoming request via the token cookie. + * + * @return \Laravel\Passport\Contracts\OAuthenticatable|null */ protected function authenticateViaCookie(): ?Authenticatable { @@ -209,11 +215,17 @@ protected function getTokenViaCookie(): ?array return null; } + // Token's expiration time is checked using the "exp" claim during decoding, but + // legacy tokens may have an "expiry" claim instead of the standard "exp". So + // we must manually check token's expiry, if the "expiry" claim is present. + if (isset($token['expiry']) && time() >= $token['expiry']) { + return null; + } + // We will compare the CSRF token in the decoded API token against the CSRF header // sent with the request. If they don't match then this request isn't sent from // a valid source and we won't authenticate the request for further handling. - if (! Passport::$ignoreCsrfToken && - (! $this->validCsrf($token) || time() >= $token['expiry'])) { + if (! Passport::$ignoreCsrfToken && ! $this->validCsrf($token)) { return null; } diff --git a/src/HasApiTokens.php b/src/HasApiTokens.php index b57212967..1e77ac60a 100644 --- a/src/HasApiTokens.php +++ b/src/HasApiTokens.php @@ -4,18 +4,25 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\MorphMany; +use Laravel\Passport\Contracts\ScopeAuthorizable; use LogicException; +/** + * @phpstan-require-implements \Laravel\Passport\Contracts\OAuthenticatable + */ trait HasApiTokens { /** * The current access token for the authentication user. */ - protected AccessToken|TransientToken|null $accessToken; + protected ?ScopeAuthorizable $accessToken = null; /** * Get all of the user's registered OAuth clients. * + * @deprecated Use oauthApps() + * * @return \Illuminate\Database\Eloquent\Relations\HasMany<\Laravel\Passport\Client, $this> */ public function clients(): HasMany @@ -23,6 +30,16 @@ public function clients(): HasMany return $this->hasMany(Passport::clientModel(), 'user_id'); } + /** + * Get all of the user's registered OAuth applications. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\Laravel\Passport\Client, $this> + */ + public function oauthApps(): MorphMany + { + return $this->morphMany(Passport::clientModel(), 'owner'); + } + /** * Get all of the access tokens for the user. * @@ -31,12 +48,12 @@ public function clients(): HasMany public function tokens(): HasMany { return $this->hasMany(Passport::tokenModel(), 'user_id') - ->where(function (Builder $query) { - $query->whereHas('client', function (Builder $query) { - $query->where(function (Builder $query) { - $provider = $this->getProvider(); + ->where(function (Builder $query): void { + $query->whereHas('client', function (Builder $query): void { + $query->where(function (Builder $query): void { + $provider = $this->getProviderName(); - $query->when($provider === config('auth.guards.api.provider'), function (Builder $query) { + $query->when($provider === config('auth.guards.api.provider'), function (Builder $query): void { $query->orWhereNull('provider'); })->orWhere('provider', $provider); }); @@ -45,19 +62,19 @@ public function tokens(): HasMany } /** - * Get the current access token being used by the user. + * Get the access token currently associated with the user. */ - public function token(): AccessToken|TransientToken|null + public function token(): ?ScopeAuthorizable { - return $this->accessToken; + return $this->currentAccessToken(); } /** * Get the access token currently associated with the user. */ - public function currentAccessToken(): AccessToken|TransientToken|null + public function currentAccessToken(): ?ScopeAuthorizable { - return $this->token(); + return $this->accessToken; } /** @@ -68,13 +85,23 @@ public function tokenCan(string $scope): bool return $this->accessToken && $this->accessToken->can($scope); } + /** + * Determine if the current API token is missing a given scope. + */ + public function tokenCant(string $scope): bool + { + return ! $this->tokenCan($scope); + } + /** * Create a new personal access token for the user. + * + * @param string[] $scopes */ public function createToken(string $name, array $scopes = []): PersonalAccessTokenResult { return app(PersonalAccessTokenFactory::class)->make( - $this->getAuthIdentifier(), $name, $scopes, $this->getProvider() + $this->getAuthIdentifier(), $name, $scopes, $this->getProviderName() ); } @@ -83,7 +110,7 @@ public function createToken(string $name, array $scopes = []): PersonalAccessTok * * @throws \LogicException */ - public function getProvider(): string + public function getProviderName(): string { $providers = collect(config('auth.guards'))->where('driver', 'passport')->pluck('provider')->all(); @@ -99,7 +126,7 @@ public function getProvider(): string /** * Set the current access token for the user. */ - public function withAccessToken(AccessToken|TransientToken|null $accessToken): static + public function withAccessToken(?ScopeAuthorizable $accessToken): static { $this->accessToken = $accessToken; diff --git a/src/Http/Controllers/AccessTokenController.php b/src/Http/Controllers/AccessTokenController.php index 3b488e568..70abd57eb 100644 --- a/src/Http/Controllers/AccessTokenController.php +++ b/src/Http/Controllers/AccessTokenController.php @@ -3,7 +3,6 @@ namespace Laravel\Passport\Http\Controllers; use League\OAuth2\Server\AuthorizationServer; -use League\OAuth2\Server\Exception\OAuthServerException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Symfony\Component\HttpFoundation\Response; @@ -25,15 +24,8 @@ public function __construct( */ public function issueToken(ServerRequestInterface $psrRequest, ResponseInterface $psrResponse): Response { - return $this->withErrorHandling(function () use ($psrRequest, $psrResponse) { - if (array_key_exists('grant_type', $attributes = (array) $psrRequest->getParsedBody()) && - $attributes['grant_type'] === 'personal_access') { - throw OAuthServerException::unsupportedGrantType(); - } - - return $this->convertResponse( - $this->server->respondToAccessTokenRequest($psrRequest, $psrResponse) - ); - }); + return $this->withErrorHandling(fn () => $this->convertResponse( + $this->server->respondToAccessTokenRequest($psrRequest, $psrResponse) + )); } } diff --git a/src/Http/Controllers/AuthorizationController.php b/src/Http/Controllers/AuthorizationController.php index 150e36890..7e1a52025 100644 --- a/src/Http/Controllers/AuthorizationController.php +++ b/src/Http/Controllers/AuthorizationController.php @@ -45,7 +45,7 @@ public function authorize( AuthorizationViewResponse $viewResponse ): Response|AuthorizationViewResponse { $authRequest = $this->withErrorHandling( - fn () => $this->server->validateAuthorizationRequest($psrRequest), + fn (): AuthorizationRequestInterface => $this->server->validateAuthorizationRequest($psrRequest), ($psrRequest->getQueryParams()['response_type'] ?? null) === 'token' ); @@ -118,10 +118,10 @@ protected function hasGrantedScopes(Authenticatable $user, Client $client, array ['user_id', '=', $user->getAuthIdentifier()], ['revoked', '=', false], ['expires_at', '>', Date::now()], - ])->pluck('scopes'); + ])->pluck('scopes')->flatten(); return $tokensScopes->isNotEmpty() && - collect($scopes)->pluck('id')->diff($tokensScopes->flatten())->isEmpty(); + collect($scopes)->pluck('id')->diff($tokensScopes)->isEmpty(); } /** @@ -145,6 +145,6 @@ protected function promptForLogin(Request $request): never { $request->session()->put('promptedForLogin', true); - throw new AuthenticationException; + throw new AuthenticationException(guards: isset($this->guard->name) ? [$this->guard->name] : []); } } diff --git a/src/Http/Controllers/ConvertsPsrResponses.php b/src/Http/Controllers/ConvertsPsrResponses.php index b39f97235..45ac106e8 100644 --- a/src/Http/Controllers/ConvertsPsrResponses.php +++ b/src/Http/Controllers/ConvertsPsrResponses.php @@ -13,6 +13,6 @@ trait ConvertsPsrResponses */ public function convertResponse(ResponseInterface $psrResponse): Response { - return (new HttpFoundationFactory())->createResponse($psrResponse); + return (new HttpFoundationFactory)->createResponse($psrResponse); } } diff --git a/src/Http/Controllers/RetrievesAuthRequestFromSession.php b/src/Http/Controllers/RetrievesAuthRequestFromSession.php index 47d59c09e..867bbe834 100644 --- a/src/Http/Controllers/RetrievesAuthRequestFromSession.php +++ b/src/Http/Controllers/RetrievesAuthRequestFromSession.php @@ -5,7 +5,7 @@ use Exception; use Illuminate\Http\Request; use Laravel\Passport\Exceptions\InvalidAuthTokenException; -use League\OAuth2\Server\RequestTypes\AuthorizationRequest; +use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface; trait RetrievesAuthRequestFromSession { @@ -15,7 +15,7 @@ trait RetrievesAuthRequestFromSession * @throws \Laravel\Passport\Exceptions\InvalidAuthTokenException * @throws \Exception */ - protected function getAuthRequestFromSession(Request $request): AuthorizationRequest + protected function getAuthRequestFromSession(Request $request): AuthorizationRequestInterface { if ($request->isNotFilled('auth_token') || $request->session()->pull('authToken') !== $request->get('auth_token')) { diff --git a/src/Http/Middleware/EnsureClientIsResourceOwner.php b/src/Http/Middleware/EnsureClientIsResourceOwner.php index a552002cf..f0e3e7cd2 100644 --- a/src/Http/Middleware/EnsureClientIsResourceOwner.php +++ b/src/Http/Middleware/EnsureClientIsResourceOwner.php @@ -2,8 +2,8 @@ namespace Laravel\Passport\Http\Middleware; -use Illuminate\Auth\AuthenticationException; use Laravel\Passport\AccessToken; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Exceptions\MissingScopeException; class EnsureClientIsResourceOwner extends ValidateToken @@ -11,11 +11,11 @@ class EnsureClientIsResourceOwner extends ValidateToken /** * Determine if the token's client is the resource owner and has all the given scopes. * - * @throws \Exception + * @throws \Laravel\Passport\Exceptions\AuthenticationException|\Laravel\Passport\Exceptions\MissingScopeException */ protected function validate(AccessToken $token, string ...$params): void { - if ($token->oauth_user_id !== $token->oauth_client_id) { + if (! is_null($token->oauth_user_id) && $token->oauth_user_id !== $token->oauth_client_id) { throw new AuthenticationException; } diff --git a/src/Http/Middleware/ValidateToken.php b/src/Http/Middleware/ValidateToken.php index 06fae3a24..f57d77293 100644 --- a/src/Http/Middleware/ValidateToken.php +++ b/src/Http/Middleware/ValidateToken.php @@ -24,22 +24,21 @@ public function __construct( /** * Specify the parameters for the middleware. * - * @param string[]|string ...$params + * @param string[]|string $param */ - public static function using(...$params): string + public static function using(array|string $param, string ...$params): string { - if (is_array($params[0])) { - return static::class.':'.implode(',', $params[0]); + if (is_array($param)) { + return static::class.':'.implode(',', $param); } - return static::class.':'.implode(',', $params); + return static::class.':'.implode(',', [$param, ...$params]); } /** * Handle an incoming request. * * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next - * @param string[]|string ...$params */ public function handle(Request $request, Closure $next, string ...$params): Response { @@ -60,14 +59,14 @@ protected function validateToken(Request $request): AccessToken // If the user is authenticated and already has an access token set via // the token guard, there's no need to validate the request's bearer // token again, so we'll return the access token as the valid one. - if ($request->user()?->token()) { - return $request->user()->token(); + if ($request->user()?->currentAccessToken()) { + return $request->user()->currentAccessToken(); } // Otherwise, we will convert the request to a PSR-7 implementation and // pass it to the OAuth2 server to be validated. If the bearer token // passed the validation, we will return an access token instance. - $psrRequest = (new PsrHttpFactory())->createRequest($request); + $psrRequest = (new PsrHttpFactory)->createRequest($request); try { $psrRequest = $this->server->validateAuthenticatedRequest($psrRequest); @@ -80,8 +79,6 @@ protected function validateToken(Request $request): AccessToken /** * Validate the given access token. - * - * @throws \Laravel\Passport\Exceptions\MissingScopeException */ abstract protected function validate(AccessToken $token, string ...$params): void; } diff --git a/src/Passport.php b/src/Passport.php index 351f8eb45..0a5fddf07 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -2,12 +2,13 @@ namespace Laravel\Passport; -use Carbon\Carbon; use Closure; use DateInterval; use DateTimeInterface; +use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Date; use Laravel\Passport\Contracts\AuthorizationViewResponse; use Laravel\Passport\Contracts\DeviceAuthorizationViewResponse; use Laravel\Passport\Contracts\DeviceUserCodeViewResponse; @@ -56,17 +57,17 @@ class Passport /** * The interval when access tokens expire. */ - public static ?DateInterval $tokensExpireIn; + public static ?DateInterval $tokensExpireIn = null; /** * The date when refresh tokens expire. */ - public static ?DateInterval $refreshTokensExpireIn; + public static ?DateInterval $refreshTokensExpireIn = null; /** * The date when personal access tokens expire. */ - public static ?DateInterval $personalAccessTokensExpireIn; + public static ?DateInterval $personalAccessTokensExpireIn = null; /** * The name for API token cookies. @@ -286,7 +287,7 @@ public static function tokensExpireIn(DateTimeInterface|DateInterval|null $date } return static::$tokensExpireIn = $date instanceof DateTimeInterface - ? Carbon::now()->diff($date) + ? Date::now()->diff($date) : $date; } @@ -300,7 +301,7 @@ public static function refreshTokensExpireIn(DateTimeInterface|DateInterval|null } return static::$refreshTokensExpireIn = $date instanceof DateTimeInterface - ? Carbon::now()->diff($date) + ? Date::now()->diff($date) : $date; } @@ -314,7 +315,7 @@ public static function personalAccessTokensExpireIn(DateTimeInterface|DateInterv } return static::$personalAccessTokensExpireIn = $date instanceof DateTimeInterface - ? Carbon::now()->diff($date) + ? Date::now()->diff($date) : $date; } @@ -341,13 +342,11 @@ public static function ignoreCsrfToken(bool $ignoreCsrfToken = true): void /** * Set the current user for the application with the given scopes. * - * @template TUserModel of \Laravel\Passport\HasApiTokens - * - * @param TUserModel $user + * @param \Laravel\Passport\Contracts\OAuthenticatable $user * @param string[] $scopes - * @return TUserModel + * @return \Laravel\Passport\Contracts\OAuthenticatable */ - public static function actingAs($user, array $scopes = [], ?string $guard = 'api') + public static function actingAs(Authenticatable $user, array $scopes = [], ?string $guard = 'api'): Authenticatable { $token = new AccessToken([ 'oauth_user_id' => $user->getAuthIdentifier(), @@ -375,11 +374,11 @@ public static function actingAs($user, array $scopes = [], ?string $guard = 'api public static function actingAsClient(Client $client, array $scopes = [], ?string $guard = 'api'): Client { $mock = Mockery::mock(ResourceServer::class); - $mock->shouldReceive('validateAuthenticatedRequest') - ->andReturnUsing(function (ServerRequestInterface $request) use ($client, $scopes) { - return $request->withAttribute('oauth_client_id', $client->getKey()) - ->withAttribute('oauth_scopes', $scopes); - }); + $mock->shouldReceive('validateAuthenticatedRequest')->andReturnUsing( + fn (ServerRequestInterface $request) => $request + ->withAttribute('oauth_client_id', $client->getKey()) + ->withAttribute('oauth_scopes', $scopes) + ); app()->instance(ResourceServer::class, $mock); diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index 8327d8a7d..4e482a6a4 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; use Laravel\Passport\Bridge\DeviceCodeRepository; +use Laravel\Passport\Bridge\PersonalAccessBearerTokenResponse; use Laravel\Passport\Bridge\PersonalAccessGrant; use Laravel\Passport\Bridge\RefreshTokenRepository; use Laravel\Passport\Contracts\ApprovedDeviceAuthorizationResponse as ApprovedDeviceAuthorizationResponseContract; @@ -21,9 +22,6 @@ use Laravel\Passport\Http\Controllers\DeviceAuthorizationController; use Laravel\Passport\Http\Responses\ApprovedDeviceAuthorizationResponse; use Laravel\Passport\Http\Responses\DeniedDeviceAuthorizationResponse; -use Lcobucci\JWT\Encoding\JoseEncoder; -use Lcobucci\JWT\Parser as ParserContract; -use Lcobucci\JWT\Token\Parser; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\Grant\AuthCodeGrant; @@ -33,6 +31,7 @@ use League\OAuth2\Server\Grant\PasswordGrant; use League\OAuth2\Server\Grant\RefreshTokenGrant; use League\OAuth2\Server\ResourceServer; +use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; class PassportServiceProvider extends ServiceProvider { @@ -58,7 +57,7 @@ protected function registerRoutes(): void 'as' => 'passport.', 'prefix' => config('passport.path', 'oauth'), 'namespace' => 'Laravel\Passport\Http\Controllers', - ], function () { + ], function (): void { $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); }); } @@ -116,7 +115,6 @@ public function register(): void $this->registerResponseBindings(); $this->registerAuthorizationServer(); - $this->registerJWTParser(); $this->registerResourceServer(); $this->registerGuard(); } @@ -135,11 +133,16 @@ protected function registerResponseBindings(): void */ protected function registerAuthorizationServer(): void { - $this->app->singleton(AuthorizationServer::class, function () { - return tap($this->makeAuthorizationServer(), function (AuthorizationServer $server) { - $server->setDefaultScope(Passport::$defaultScope); - $server->revokeRefreshTokens(Passport::$revokeRefreshTokenAfterUse); + $this->app->when(PersonalAccessTokenFactory::class) + ->needs(AuthorizationServer::class) + ->give(fn () => tap($this->makeAuthorizationServer(new PersonalAccessBearerTokenResponse), + function (AuthorizationServer $server): void { + $server->enableGrantType(new PersonalAccessGrant, Passport::personalAccessTokensExpireIn()); + } + )); + $this->app->singleton(AuthorizationServer::class, + fn () => tap($this->makeAuthorizationServer(), function (AuthorizationServer $server): void { $server->enableGrantType( $this->makeAuthCodeGrant(), Passport::tokensExpireIn() ); @@ -154,10 +157,6 @@ protected function registerAuthorizationServer(): void ); } - $server->enableGrantType( - new PersonalAccessGrant, Passport::personalAccessTokensExpireIn() - ); - $server->enableGrantType( new ClientCredentialsGrant, Passport::tokensExpireIn() ); @@ -171,8 +170,8 @@ protected function registerAuthorizationServer(): void $server->enableGrantType( $this->makeDeviceCodeGrant(), Passport::tokensExpireIn() ); - }); - }); + }) + ); } /** @@ -180,7 +179,7 @@ protected function registerAuthorizationServer(): void */ protected function makeAuthCodeGrant(): AuthCodeGrant { - return tap($this->buildAuthCodeGrant(), function (AuthCodeGrant $grant) { + return tap($this->buildAuthCodeGrant(), function (AuthCodeGrant $grant): void { $grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn()); }); } @@ -202,9 +201,9 @@ protected function buildAuthCodeGrant(): AuthCodeGrant */ protected function makeRefreshTokenGrant(): RefreshTokenGrant { - $repository = $this->app->make(RefreshTokenRepository::class); - - return tap(new RefreshTokenGrant($repository), function (RefreshTokenGrant $grant) { + return tap(new RefreshTokenGrant( + $this->app->make(RefreshTokenRepository::class) + ), function (RefreshTokenGrant $grant): void { $grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn()); }); } @@ -217,7 +216,7 @@ protected function makePasswordGrant(): PasswordGrant return tap(new PasswordGrant( $this->app->make(Bridge\UserRepository::class), $this->app->make(Bridge\RefreshTokenRepository::class) - ), function (PasswordGrant $grant) { + ), function (PasswordGrant $grant): void { $grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn()); }); } @@ -251,30 +250,23 @@ protected function makeDeviceCodeGrant(): DeviceCodeGrant /** * Make the authorization service instance. */ - public function makeAuthorizationServer(): AuthorizationServer + protected function makeAuthorizationServer(?ResponseTypeInterface $responseType = null): AuthorizationServer { - return new AuthorizationServer( + return tap(new AuthorizationServer( $this->app->make(Bridge\ClientRepository::class), $this->app->make(Bridge\AccessTokenRepository::class), $this->app->make(Bridge\ScopeRepository::class), $this->makeCryptKey('private'), - $this->app->make('encrypter')->getKey(), - Passport::$authorizationServerResponseType - ); - } - - /** - * Register the JWT Parser. - */ - protected function registerJWTParser(): void - { - $this->app->singleton(ParserContract::class, fn () => new Parser(new JoseEncoder)); + Passport::tokenEncryptionKey($this->app->make('encrypter')), + $responseType ?? Passport::$authorizationServerResponseType + ), function (AuthorizationServer $server): void { + $server->setDefaultScope(Passport::$defaultScope); + $server->revokeRefreshTokens(Passport::$revokeRefreshTokenAfterUse); + }); } /** * Register the resource server. - * - * @return void */ protected function registerResourceServer(): void { @@ -303,8 +295,8 @@ protected function makeCryptKey(string $type): CryptKey */ protected function registerGuard(): void { - Auth::resolved(function ($auth) { - $auth->extend('passport', fn ($app, $name, array $config) => tap($this->makeGuard($config), function ($guard) { + Auth::resolved(function ($auth): void { + $auth->extend('passport', fn ($app, $name, array $config) => tap($this->makeGuard($config), function ($guard): void { app()->refresh('request', $guard, 'setRequest'); })); }); @@ -331,7 +323,7 @@ protected function makeGuard(array $config): TokenGuard */ protected function deleteCookieOnLogout(): void { - Event::listen(Logout::class, function () { + Event::listen(Logout::class, function (): void { if (Request::hasCookie(Passport::cookie())) { Cookie::queue(Cookie::forget(Passport::cookie())); } diff --git a/src/PassportUserProvider.php b/src/PassportUserProvider.php index 7fceb307f..7752b3317 100644 --- a/src/PassportUserProvider.php +++ b/src/PassportUserProvider.php @@ -17,7 +17,10 @@ public function __construct( } /** - * {@inheritdoc} + * Retrieve a user by their unique identifier. + * + * @param string|int $identifier + * @return \Laravel\Passport\Contracts\OAuthenticatable|null */ public function retrieveById($identifier): ?Authenticatable { @@ -25,41 +28,61 @@ public function retrieveById($identifier): ?Authenticatable } /** - * {@inheritdoc} + * Retrieve a user by their unique identifier and "remember me" token. + * + * @param string|int $identifier + * @param string $token + * @return \Laravel\Passport\Contracts\OAuthenticatable|null */ - public function retrieveByToken($identifier, $token): ?Authenticatable + public function retrieveByToken($identifier, #[\SensitiveParameter] $token): ?Authenticatable { return $this->provider->retrieveByToken($identifier, $token); } /** - * {@inheritdoc} + * Update the "remember me" token for the given user in storage. + * + * @param \Laravel\Passport\Contracts\OAuthenticatable $user + * @param string $token + * @return void */ - public function updateRememberToken(Authenticatable $user, $token): void + public function updateRememberToken(Authenticatable $user, #[\SensitiveParameter] $token): void { $this->provider->updateRememberToken($user, $token); } /** - * {@inheritdoc} + * Retrieve a user by the given credentials. + * + * @param array $credentials + * @return \Laravel\Passport\Contracts\OAuthenticatable|null */ - public function retrieveByCredentials(array $credentials): ?Authenticatable + public function retrieveByCredentials(#[\SensitiveParameter] array $credentials): ?Authenticatable { return $this->provider->retrieveByCredentials($credentials); } /** - * {@inheritdoc} + * Validate a user against the given credentials. + * + * @param \Laravel\Passport\Contracts\OAuthenticatable $user + * @param array $credentials + * @return bool */ - public function validateCredentials(Authenticatable $user, array $credentials): bool + public function validateCredentials(Authenticatable $user, #[\SensitiveParameter] array $credentials): bool { return $this->provider->validateCredentials($user, $credentials); } /** - * {@inheritdoc} + * Rehash the user's password if required and supported. + * + * @param \Laravel\Passport\Contracts\OAuthenticatable $user + * @param array $credentials + * @param bool $force + * @return void */ - public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void + public function rehashPasswordIfRequired(Authenticatable $user, #[\SensitiveParameter] array $credentials, bool $force = false): void { $this->provider->rehashPasswordIfRequired($user, $credentials, $force); } diff --git a/src/PersonalAccessTokenFactory.php b/src/PersonalAccessTokenFactory.php index 31419ec03..4c65030ba 100644 --- a/src/PersonalAccessTokenFactory.php +++ b/src/PersonalAccessTokenFactory.php @@ -2,7 +2,6 @@ namespace Laravel\Passport; -use Lcobucci\JWT\Parser as JwtParser; use League\OAuth2\Server\AuthorizationServer; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -15,8 +14,7 @@ class PersonalAccessTokenFactory * Create a new personal access token factory instance. */ public function __construct( - protected AuthorizationServer $server, - protected JwtParser $jwt + protected AuthorizationServer $server ) { } @@ -27,18 +25,10 @@ public function __construct( */ public function make(string|int $userId, string $name, array $scopes, string $provider): PersonalAccessTokenResult { - $response = $this->dispatchRequestToAuthorizationServer( - $this->createRequest($userId, $scopes, $provider) - ); - - $token = tap($this->findAccessToken($response), function (Token $token) use ($name) { - $token->forceFill([ - 'name' => $name, - ])->save(); - }); - return new PersonalAccessTokenResult( - $response['access_token'], $token + $this->dispatchRequestToAuthorizationServer( + $this->createRequest($userId, $name, $scopes, $provider) + ) ); } @@ -47,13 +37,14 @@ public function make(string|int $userId, string $name, array $scopes, string $pr * * @param string[] $scopes */ - protected function createRequest(string|int $userId, array $scopes, string $provider): ServerRequestInterface + protected function createRequest(string|int $userId, string $name, array $scopes, string $provider): ServerRequestInterface { - return (new PsrHttpFactory())->createRequest(Request::create('not-important', 'POST', [ + return (new PsrHttpFactory)->createRequest(Request::create('', 'POST', [ 'grant_type' => 'personal_access', 'provider' => $provider, 'user_id' => $userId, 'scope' => implode(' ', $scopes), + 'name' => $name, ])); } @@ -68,16 +59,4 @@ protected function dispatchRequestToAuthorizationServer(ServerRequestInterface $ $request, app(ResponseInterface::class) )->getBody()->__toString(), true); } - - /** - * Get the access token instance for the parsed response. - * - * @param array $response - */ - public function findAccessToken(array $response): Token - { - return Passport::token()->newQuery()->find( - $this->jwt->parse($response['access_token'])->claims()->get('jti') - ); - } } diff --git a/src/PersonalAccessTokenResult.php b/src/PersonalAccessTokenResult.php index 0e92898f0..e3e0a484d 100644 --- a/src/PersonalAccessTokenResult.php +++ b/src/PersonalAccessTokenResult.php @@ -4,29 +4,71 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Jsonable; +use Illuminate\Support\Str; +use JsonSerializable; -class PersonalAccessTokenResult implements Arrayable, Jsonable +/** + * @template TValue + * + * @implements \Illuminate\Contracts\Support\Arrayable + * + * @property string $accessTokenId + * @property string $accessToken + * @property string $tokenType + * @property int $expiresIn + */ +class PersonalAccessTokenResult implements Arrayable, Jsonable, JsonSerializable { + /** + * The token instance. + */ + protected ?Token $token = null; + + /** + * All the attributes set on the personal access token response. + * + * @var array + */ + protected array $attributes = []; + /** * Create a new result instance. + * + * @param array $attributes */ - public function __construct( - public string $accessToken, - public Token $token - ) { + public function __construct(array $attributes = []) + { + foreach ($attributes as $key => $value) { + $this->attributes[Str::camel($key)] = $value; + } + } + + /** + * Get the token instance. + */ + public function getToken(): ?Token + { + return $this->token ??= Passport::token()->newQuery()->find($this->accessTokenId); } /** * Get the instance as an array. * - * @return array + * @return array */ public function toArray(): array { - return [ - 'accessToken' => $this->accessToken, - 'token' => $this->token, - ]; + return $this->attributes; + } + + /** + * Convert the object into something JSON serializable. + * + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); } /** @@ -36,6 +78,26 @@ public function toArray(): array */ public function toJson($options = 0): string { - return json_encode($this->toArray(), $options); + return json_encode($this->jsonSerialize(), $options); + } + + /** + * Dynamically determine if an attribute is set. + */ + public function __isset(string $key): bool + { + return isset($this->attributes[$key]); + } + + /** + * Dynamically retrieve the value of an attribute. + */ + public function __get(string $key): mixed + { + if ($key === 'token') { + return $this->getToken(); + } + + return $this->attributes[$key] ?? null; } } diff --git a/src/RefreshToken.php b/src/RefreshToken.php index 9b8558444..c2a02c8ef 100644 --- a/src/RefreshToken.php +++ b/src/RefreshToken.php @@ -38,7 +38,7 @@ class RefreshToken extends Model /** * The attributes that should be cast to native types. * - * @var array + * @var array */ protected $casts = [ 'revoked' => 'bool', diff --git a/src/ResolvesInheritedScopes.php b/src/ResolvesInheritedScopes.php index b277cf4d7..8e98ee230 100644 --- a/src/ResolvesInheritedScopes.php +++ b/src/ResolvesInheritedScopes.php @@ -9,7 +9,7 @@ trait ResolvesInheritedScopes * * @param string[] $haystack */ - protected function scopeExists(string $scope, array $haystack): bool + protected function scopeExistsIn(string $scope, array $haystack): bool { $scopes = Passport::$withInheritedScopes ? $this->resolveInheritedScopes($scope) diff --git a/src/Scope.php b/src/Scope.php index 45c6c510d..08e306c0e 100644 --- a/src/Scope.php +++ b/src/Scope.php @@ -5,6 +5,9 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Jsonable; +/** + * @implements \Illuminate\Contracts\Support\Arrayable + */ class Scope implements Arrayable, Jsonable { /** diff --git a/src/Token.php b/src/Token.php index b4339f20c..df9c2a0a2 100644 --- a/src/Token.php +++ b/src/Token.php @@ -39,7 +39,7 @@ class Token extends Model /** * The attributes that should be cast to native types. * - * @var array + * @var array */ protected $casts = [ 'scopes' => 'array', diff --git a/src/TokenRepository.php b/src/TokenRepository.php index 50ff19738..543f4d519 100644 --- a/src/TokenRepository.php +++ b/src/TokenRepository.php @@ -16,7 +16,7 @@ class TokenRepository * * @deprecated Use $user->tokens()->find() * - * @param \Laravel\Passport\HasApiTokens $user + * @param \Laravel\Passport\Contracts\OAuthenticatable $user */ public function findForUser(string $id, Authenticatable $user): ?Token { @@ -32,7 +32,7 @@ public function findForUser(string $id, Authenticatable $user): ?Token * * @deprecated Use $user->tokens() * - * @param \Laravel\Passport\HasApiTokens $user + * @param \Laravel\Passport\Contracts\OAuthenticatable $user * @return \Illuminate\Database\Eloquent\Collection */ public function forUser(Authenticatable $user): Collection diff --git a/src/TransientToken.php b/src/TransientToken.php index 54652a4ca..bdabbc9e2 100644 --- a/src/TransientToken.php +++ b/src/TransientToken.php @@ -2,7 +2,9 @@ namespace Laravel\Passport; -class TransientToken +use Laravel\Passport\Contracts\ScopeAuthorizable; + +class TransientToken implements ScopeAuthorizable { /** * Determine if the token has a given scope. diff --git a/tests/Feature/AccessTokenControllerTest.php b/tests/Feature/AccessTokenControllerTest.php index 4c107057e..15671d763 100644 --- a/tests/Feature/AccessTokenControllerTest.php +++ b/tests/Feature/AccessTokenControllerTest.php @@ -2,13 +2,13 @@ namespace Laravel\Passport\Tests\Feature; -use Carbon\CarbonImmutable; use Illuminate\Contracts\Hashing\Hasher; use Laravel\Passport\Client; use Laravel\Passport\Database\Factories\ClientFactory; use Laravel\Passport\Passport; -use Laravel\Passport\PersonalAccessTokenFactory; use Laravel\Passport\Token; +use League\OAuth2\Server\Entities\AccessTokenEntityInterface; +use League\OAuth2\Server\ResponseTypes\BearerTokenResponse; use Orchestra\Testbench\Concerns\WithLaravelMigrations; use Workbench\Database\Factories\UserFactory; @@ -26,7 +26,7 @@ public function testGettingAccessTokenWithClientCredentialsGrant() ]); /** @var Client $client */ - $client = ClientFactory::new()->asClientCredentials()->create(['user_id' => $user->getKey()]); + $client = ClientFactory::new()->asClientCredentials()->for($user, 'owner')->create(); $response = $this->post( '/oauth/token', @@ -51,14 +51,6 @@ public function testGettingAccessTokenWithClientCredentialsGrant() $this->assertSame('Bearer', $decodedResponse['token_type']); $expiresInSeconds = 31536000; $this->assertEqualsWithDelta($expiresInSeconds, $decodedResponse['expires_in'], 5); - - $token = $this->app->make(PersonalAccessTokenFactory::class)->findAccessToken($decodedResponse); - $this->assertInstanceOf(Token::class, $token); - $this->assertTrue($token->client->is($client)); - $this->assertFalse($token->revoked); - $this->assertNull($token->name); - $this->assertNull($token->user_id); - $this->assertLessThanOrEqual(5, CarbonImmutable::now()->addSeconds($expiresInSeconds)->diffInSeconds($token->expires_at)); } public function testGettingAccessTokenWithClientCredentialsGrantInvalidClientSecret() @@ -69,7 +61,7 @@ public function testGettingAccessTokenWithClientCredentialsGrantInvalidClientSec ]); /** @var Client $client */ - $client = ClientFactory::new()->asClientCredentials()->create(['user_id' => $user->getKey()]); + $client = ClientFactory::new()->asClientCredentials()->for($user, 'owner')->create(); $response = $this->post( '/oauth/token', @@ -113,7 +105,7 @@ public function testGettingAccessTokenWithPasswordGrant() ]); /** @var Client $client */ - $client = ClientFactory::new()->asPasswordClient()->create(['user_id' => $user->getKey()]); + $client = ClientFactory::new()->asPasswordClient()->for($user, 'owner')->create(); $response = $this->post( '/oauth/token', @@ -141,14 +133,6 @@ public function testGettingAccessTokenWithPasswordGrant() $this->assertSame('Bearer', $decodedResponse['token_type']); $expiresInSeconds = 31536000; $this->assertEqualsWithDelta($expiresInSeconds, $decodedResponse['expires_in'], 5); - - $token = $this->app->make(PersonalAccessTokenFactory::class)->findAccessToken($decodedResponse); - $this->assertInstanceOf(Token::class, $token); - $this->assertFalse($token->revoked); - $this->assertSame($user->getAuthIdentifier(), $token->user_id); - $this->assertTrue($token->client->is($client)); - $this->assertNull($token->name); - $this->assertLessThanOrEqual(5, CarbonImmutable::now()->addSeconds($expiresInSeconds)->diffInSeconds($token->expires_at)); } public function testGettingAccessTokenWithPasswordGrantWithInvalidPassword() @@ -162,7 +146,7 @@ public function testGettingAccessTokenWithPasswordGrantWithInvalidPassword() ]); /** @var Client $client */ - $client = ClientFactory::new()->asPasswordClient()->create(['user_id' => $user->getKey()]); + $client = ClientFactory::new()->asPasswordClient()->for($user, 'owner')->create(); $response = $this->post( '/oauth/token', @@ -206,7 +190,7 @@ public function testGettingAccessTokenWithPasswordGrantWithInvalidClientSecret() ]); /** @var Client $client */ - $client = ClientFactory::new()->asPasswordClient()->create(['user_id' => $user->getKey()]); + $client = ClientFactory::new()->asPasswordClient()->for($user, 'owner')->create(); $response = $this->post( '/oauth/token', @@ -251,7 +235,7 @@ public function testGettingCustomResponseType() ]); /** @var Client $client */ - $client = ClientFactory::new()->asClientCredentials()->create(['user_id' => $user->getKey()]); + $client = ClientFactory::new()->asClientCredentials()->for($user, 'owner')->create(); $response = $this->post( '/oauth/token', @@ -271,25 +255,17 @@ public function testGettingCustomResponseType() } } -class IdTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerTokenResponse +class IdTokenResponse extends BearerTokenResponse { - /** - * @var string Id token. - */ - protected $idToken; - - /** - * @param string $idToken - */ - public function __construct($idToken) - { - $this->idToken = $idToken; + public function __construct( + protected string $idToken + ) { } /** * {@inheritdoc} */ - protected function getExtraParams(\League\OAuth2\Server\Entities\AccessTokenEntityInterface $accessToken): array + protected function getExtraParams(AccessTokenEntityInterface $accessToken): array { return [ 'id_token' => $this->idToken, diff --git a/tests/Feature/AuthorizationCodeGrantTest.php b/tests/Feature/AuthorizationCodeGrantTest.php index d59d9f01f..7a9cb31ba 100644 --- a/tests/Feature/AuthorizationCodeGrantTest.php +++ b/tests/Feature/AuthorizationCodeGrantTest.php @@ -78,7 +78,7 @@ public function testIssueAccessToken() $this->assertSame('Bearer', $json['token_type']); $this->assertSame(31536000, $json['expires_in']); - Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + Route::get('/foo', fn (Request $request) => $request->user()->currentAccessToken()->toJson()) ->middleware('auth:api'); $json = $this->withToken($json['access_token'], $json['token_type'])->get('/foo')->json(); @@ -199,7 +199,7 @@ public function testValidateScopes() parse_str(parse_url($location, PHP_URL_QUERY), $params); $this->assertStringStartsWith($redirect.'?', $location); - // $this->assertSame($state, $params['state']); + $this->assertSame($state, $params['state']); $this->assertSame('invalid_scope', $params['error']); $this->assertArrayHasKey('error_description', $params); } diff --git a/tests/Feature/AuthorizationCodeGrantWithPkceTest.php b/tests/Feature/AuthorizationCodeGrantWithPkceTest.php index 16352f426..5a7f10395 100644 --- a/tests/Feature/AuthorizationCodeGrantWithPkceTest.php +++ b/tests/Feature/AuthorizationCodeGrantWithPkceTest.php @@ -16,7 +16,7 @@ class AuthorizationCodeGrantWithPkceTest extends PassportTestCase protected function setUp(): void { - PassportTestCase::setUp(); + parent::setUp(); Passport::tokensCan([ 'create' => 'Create', @@ -85,7 +85,7 @@ public function testIssueAccessToken() $refreshToken = $json['refresh_token']; - Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + Route::get('/foo', fn (Request $request) => $request->user()->currentAccessToken()->toJson()) ->middleware('auth:api'); $json = $this->withToken($json['access_token'], $json['token_type'])->get('/foo')->json(); @@ -113,19 +113,76 @@ public function testRequireCodeChallenge() $query = http_build_query([ 'client_id' => $client->getKey(), - 'redirect_uri' => $client->redirect_uris[0], + 'redirect_uri' => $redirect = $client->redirect_uris[0], 'response_type' => 'code', + 'state' => $state = Str::random(40), ]); $user = UserFactory::new()->create(); $this->actingAs($user, 'web'); $response = $this->get('/oauth/authorize?'.$query); + // $response->assertRedirect(); $response->assertStatus(400); - $json = $response->json(); - $this->assertSame('invalid_request', $json['error']); - $this->assertSame('Code challenge must be provided for public clients', $json['hint']); - $this->assertArrayHasKey('error_description', $json); + // $location = $response->headers->get('Location'); + // parse_str(parse_url($location, PHP_URL_QUERY), $params); + $params = $response->json(); + + // $this->assertStringStartsWith($redirect.'?', $location); + // $this->assertSame($state, $params['state']); + $this->assertSame('invalid_request', $params['error']); + $this->assertSame('Code challenge must be provided for public clients', $params['hint']); + $this->assertArrayHasKey('error_description', $params); + + $query .= '&code_challenge=foo'; + $response = $this->get('/oauth/authorize?'.$query); + + // $response->assertRedirect(); + $response->assertStatus(400); + + // $location = $response->headers->get('Location'); + // parse_str(parse_url($location, PHP_URL_QUERY), $params); + $params = $response->json(); + + // $this->assertStringStartsWith($redirect.'?', $location); + // $this->assertSame($state, $params['state']); + $this->assertSame('invalid_request', $params['error']); + $this->assertSame('Code challenge must follow the specifications of RFC-7636.', $params['hint']); + $this->assertArrayHasKey('error_description', $params); + } + + public function testInvalidCodeChallengeMethod() + { + $client = ClientFactory::new()->asPublic()->create(); + + $codeVerifier = Str::random(128); + $codeChallenge = strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_'); + + $query = http_build_query([ + 'client_id' => $client->getKey(), + 'redirect_uri' => $redirect = $client->redirect_uris[0], + 'response_type' => 'code', + 'state' => $state = Str::random(40), + 'code_challenge' => $codeChallenge, + 'code_challenge_method' => 'foo', + ]); + + $user = UserFactory::new()->create(); + $this->actingAs($user, 'web'); + $response = $this->get('/oauth/authorize?'.$query); + + // $response->assertRedirect(); + $response->assertStatus(400); + + // $location = $response->headers->get('Location'); + // parse_str(parse_url($location, PHP_URL_QUERY), $params); + $params = $response->json(); + + // $this->assertStringStartsWith($redirect.'?', $location); + // $this->assertSame($state, $params['state']); + $this->assertSame('invalid_request', $params['error']); + $this->assertStringStartsWith('Code challenge method must be', $params['hint']); + $this->assertArrayHasKey('error_description', $params); } } diff --git a/tests/Feature/ClientCredentialsGrantTest.php b/tests/Feature/ClientCredentialsGrantTest.php index 03876d511..c6eb9fa73 100644 --- a/tests/Feature/ClientCredentialsGrantTest.php +++ b/tests/Feature/ClientCredentialsGrantTest.php @@ -16,7 +16,7 @@ class ClientCredentialsGrantTest extends PassportTestCase protected function setUp(): void { - PassportTestCase::setUp(); + parent::setUp(); Passport::tokensCan([ 'create' => 'Create', @@ -38,6 +38,7 @@ public function testIssueAccessToken() ])->assertOk()->json(); $this->assertArrayHasKey('access_token', $json); + $this->assertArrayNotHasKey('refresh_token', $json); $this->assertSame('Bearer', $json['token_type']); $this->assertSame(31536000, $json['expires_in']); @@ -55,6 +56,35 @@ public function testIssueAccessToken() $response->assertForbidden(); } + public function testIssueAccessTokenWithAllScopes() + { + $client = ClientFactory::new()->asClientCredentials()->create(); + + $json = $this->post('/oauth/token', [ + 'grant_type' => 'client_credentials', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'scope' => '*', + ])->assertOk()->json(); + + $this->assertArrayHasKey('access_token', $json); + $this->assertArrayNotHasKey('refresh_token', $json); + $this->assertSame('Bearer', $json['token_type']); + $this->assertSame(31536000, $json['expires_in']); + + Route::get('/foo', fn (Request $request) => response('response')) + ->middleware([EnsureClientIsResourceOwner::using(['create', 'delete'])]); + + $response = $this->withToken($json['access_token'], $json['token_type'])->get('/foo'); + $response->assertOk(); + + Route::get('/bar', fn (Request $request) => response('response')) + ->middleware(CheckToken::using(['create', 'delete'])); + + $response = $this->withToken($json['access_token'], $json['token_type'])->get('/bar'); + $response->assertOk(); + } + public function testPublicClient() { $client = ClientFactory::new()->asClientCredentials()->asPublic()->create(); @@ -62,7 +92,6 @@ public function testPublicClient() $json = $this->post('/oauth/token', [ 'grant_type' => 'client_credentials', 'client_id' => $client->getKey(), - 'client_secret' => $client->plainSecret, ])->assertUnauthorized()->json(); $this->assertSame('invalid_client', $json['error']); diff --git a/tests/Feature/Console/PurgeCommand.php b/tests/Feature/Console/PurgeCommand.php index ff5cdbd04..d454eca2c 100644 --- a/tests/Feature/Console/PurgeCommand.php +++ b/tests/Feature/Console/PurgeCommand.php @@ -1,6 +1,6 @@ assertSame('Bearer', $json['token_type']); $this->assertSame(31536000, $json['expires_in']); - Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + Route::get('/foo', fn (Request $request) => $request->user()->currentAccessToken()->toJson()) ->middleware('auth:api'); $json = $this->withToken($json['access_token'], $json['token_type'])->get('/foo')->json(); diff --git a/tests/Feature/HasApiTokensTest.php b/tests/Feature/HasApiTokensTest.php index 94d5e18ad..e9b4b149f 100644 --- a/tests/Feature/HasApiTokensTest.php +++ b/tests/Feature/HasApiTokensTest.php @@ -3,6 +3,7 @@ namespace Laravel\Passport\Tests\Feature; use Illuminate\Foundation\Auth\User as Authenticatable; +use Laravel\Passport\Contracts\OAuthenticatable; use Laravel\Passport\HasApiTokens; use Orchestra\Testbench\Concerns\WithLaravelMigrations; use Workbench\Database\Factories\UserFactory; @@ -20,18 +21,18 @@ public function testGetProvider() 'auth.guards.api-customers' => ['driver' => 'passport', 'provider' => 'customers'], ]); - $this->assertSame('users', UserFactory::new()->create()->getProvider()); - $this->assertSame('admins', (new AdminHasApiTokensStub)->getProvider()); - $this->assertSame('customers', (new CustomerHasApiTokensStub)->getProvider()); + $this->assertSame('users', UserFactory::new()->create()->getProviderName()); + $this->assertSame('admins', (new AdminHasApiTokensStub)->getProviderName()); + $this->assertSame('customers', (new CustomerHasApiTokensStub)->getProviderName()); } } -class AdminHasApiTokensStub extends Authenticatable +class AdminHasApiTokensStub extends Authenticatable implements OAuthenticatable { use HasApiTokens; } -class CustomerHasApiTokensStub extends Authenticatable +class CustomerHasApiTokensStub extends Authenticatable implements OAuthenticatable { use HasApiTokens; } diff --git a/tests/Feature/ImplicitGrantTest.php b/tests/Feature/ImplicitGrantTest.php index 658f7c27b..7768d19f5 100644 --- a/tests/Feature/ImplicitGrantTest.php +++ b/tests/Feature/ImplicitGrantTest.php @@ -16,7 +16,7 @@ class ImplicitGrantTest extends PassportTestCase protected function setUp(): void { - PassportTestCase::setUp(); + parent::setUp(); Passport::enableImplicitGrant(); @@ -67,7 +67,7 @@ public function testIssueAccessToken() $this->assertSame('Bearer', $params['token_type']); $this->assertSame('31536000', $params['expires_in']); - Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + Route::get('/foo', fn (Request $request) => $request->user()->currentAccessToken()->toJson()) ->middleware('auth:api'); $json = $this->withToken($params['access_token'], $params['token_type'])->get('/foo')->json(); @@ -100,8 +100,8 @@ public function testDenyAuthorization() $location = $response->headers->get('Location'); parse_str(parse_url($location, PHP_URL_FRAGMENT), $params); - // $this->assertStringStartsWith($redirect.'#', $location); - // $this->assertSame($state, $params['state']); + $this->assertStringStartsWith($redirect.'#', $location); + $this->assertSame($state, $params['state']); $this->assertSame('access_denied', $params['error']); $this->assertArrayHasKey('error_description', $params); } @@ -182,7 +182,7 @@ public function testValidateScopes() parse_str(parse_url($location, PHP_URL_FRAGMENT), $params); $this->assertStringStartsWith($redirect.'#', $location); - // $this->assertSame($state, $params['state']); + $this->assertSame($state, $params['state']); $this->assertSame('invalid_scope', $params['error']); $this->assertArrayHasKey('error_description', $params); } diff --git a/tests/Feature/PassportServiceProviderTest.php b/tests/Feature/PassportServiceProviderTest.php index 37ccab418..3e70ca3fd 100644 --- a/tests/Feature/PassportServiceProviderTest.php +++ b/tests/Feature/PassportServiceProviderTest.php @@ -7,11 +7,6 @@ class PassportServiceProviderTest extends PassportTestCase { - protected function tearDown(): void - { - @unlink(__DIR__.'/../keys/oauth-private.key'); - } - public function test_can_use_crypto_keys_from_config() { $privateKey = openssl_pkey_new(); diff --git a/tests/Feature/PasswordGrantTest.php b/tests/Feature/PasswordGrantTest.php index 0daa149c4..6b2f225c5 100644 --- a/tests/Feature/PasswordGrantTest.php +++ b/tests/Feature/PasswordGrantTest.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Schema; +use Laravel\Passport\Contracts\OAuthenticatable; use Laravel\Passport\Database\Factories\ClientFactory; use Laravel\Passport\HasApiTokens; use Laravel\Passport\Passport; @@ -49,7 +50,7 @@ public function testIssueToken() $this->assertSame('Bearer', $json['token_type']); $this->assertSame(31536000, $json['expires_in']); - Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + Route::get('/foo', fn (Request $request) => $request->user()->currentAccessToken()->toJson()) ->middleware('auth:api'); $json = $this->withToken($json['access_token'], $json['token_type'])->get('/foo')->json(); @@ -78,7 +79,7 @@ public function testIssueTokenWithAllScopes() $this->assertSame('Bearer', $json['token_type']); $this->assertSame(31536000, $json['expires_in']); - Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + Route::get('/foo', fn (Request $request) => $request->user()->currentAccessToken()->toJson()) ->middleware('auth:api'); $json = $this->withToken($json['access_token'], $json['token_type'])->get('/foo')->json(); @@ -120,7 +121,7 @@ public function testIssueTokenWithDifferentProviders() Route::get('/foo', fn (Request $request) => response()->json([ 'user' => $request->user(), - 'token' => $request->user()->token(), + 'token' => $request->user()->currentAccessToken(), ]))->middleware('auth:api'); $json = $this->withToken($json['access_token'], $json['token_type'])->get('/foo')->json(); @@ -140,7 +141,7 @@ public function testIssueTokenWithDifferentProviders() Route::get('/bar', fn (Request $request) => response()->json([ 'user' => $request->user(), - 'token' => $request->user()->token(), + 'token' => $request->user()->currentAccessToken(), ]))->middleware('auth:api-admins'); $json = $this->withToken($json['access_token'], $json['token_type'])->get('/bar')->json(); @@ -190,7 +191,7 @@ public function testUnauthorizedClient() } } -class AdminProviderPasswordStub extends Authenticatable +class AdminProviderPasswordStub extends Authenticatable implements OAuthenticatable { use HasApiTokens; diff --git a/tests/Feature/PersonalAccessGrantTest.php b/tests/Feature/PersonalAccessGrantTest.php index 3a9a0fe05..9d3d613b0 100644 --- a/tests/Feature/PersonalAccessGrantTest.php +++ b/tests/Feature/PersonalAccessGrantTest.php @@ -3,8 +3,11 @@ namespace Laravel\Passport\Tests\Feature; use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Route; use Laravel\Passport\Client; +use Laravel\Passport\Contracts\OAuthenticatable; use Laravel\Passport\Database\Factories\ClientFactory; use Laravel\Passport\HasApiTokens; use Laravel\Passport\Passport; @@ -29,20 +32,53 @@ public function testIssueToken() ]); $result = $user->createToken('test', ['bar']); + $token = $result->getToken(); $this->assertInstanceOf(PersonalAccessTokenResult::class, $result); - $this->assertSame($client->getKey(), $result->token->client_id); - $this->assertSame($user->getAuthIdentifier(), $result->token->user_id); - $this->assertSame(['bar'], $result->token->scopes); + $this->assertArrayHasKey('accessToken', $result->toArray()); + $this->assertSame($token->getKey(), $result->accessTokenId); + $this->assertSame('Bearer', $result->tokenType); + $this->assertSame(31536000, $result->expiresIn); + $this->assertSame($client->getKey(), $token->client_id); + $this->assertSame($user->getAuthIdentifier(), $token->user_id); + $this->assertSame(['bar'], $token->scopes); + $this->assertSame('test', $token->name); $this->assertDatabaseHas('oauth_access_tokens', [ - 'id' => $result->token->id, - 'user_id' => $result->token->user_id, - 'client_id' => $result->token->client_id, - 'name' => $result->token->name, + 'id' => $token->id, + 'user_id' => $token->user_id, + 'client_id' => $token->client_id, + 'name' => $token->name, ]); } + public function testIssueTokenWithAllScopes() + { + $user = UserFactory::new()->create(); + + /** @var Client $client */ + $client = ClientFactory::new()->asPersonalAccessTokenClient()->create(); + + $result = $user->createToken('test', ['*']); + $token = $result->getToken(); + + $this->assertInstanceOf(PersonalAccessTokenResult::class, $result); + $this->assertSame($client->getKey(), $token->client_id); + $this->assertSame($user->getAuthIdentifier(), $token->user_id); + $this->assertSame(['*'], $token->scopes); + $this->assertSame('test', $token->name); + + Route::get('/foo', fn (Request $request) => $request->user()->currentAccessToken()->toJson()) + ->middleware('auth:api'); + + $json = $this->withToken($result->accessToken)->get('/foo')->json(); + + $this->assertSame($token->getKey(), $json['oauth_access_token_id']); + $this->assertSame($client->getKey(), $json['oauth_client_id']); + $this->assertEquals($user->getAuthIdentifier(), $json['oauth_user_id']); + $this->assertSame(['*'], $json['oauth_scopes']); + } + public function testIssueTokenWithDifferentProviders() { $client = ClientFactory::new()->asPersonalAccessTokenClient()->create(); @@ -58,24 +94,30 @@ public function testIssueTokenWithDifferentProviders() $user = UserFactory::new()->create(); $userToken = $user->createToken('test user'); + $userTokenRecord = $userToken->getToken(); $admin = new AdminProviderStub; $adminToken = $admin->createToken('test admin'); + $adminTokenRecord = $adminToken->getToken(); $customer = new CustomerProviderStub; $customerToken = $customer->createToken('test customer'); + $customerTokenRecord = $customerToken->getToken(); $this->assertInstanceOf(PersonalAccessTokenResult::class, $userToken); - $this->assertSame($client->getKey(), $userToken->token->client_id); - $this->assertSame($user->getAuthIdentifier(), $userToken->token->user_id); + $this->assertSame($client->getKey(), $userTokenRecord->client_id); + $this->assertSame($user->getAuthIdentifier(), $userTokenRecord->user_id); + $this->assertSame('test user', $userTokenRecord->name); $this->assertInstanceOf(PersonalAccessTokenResult::class, $adminToken); - $this->assertSame($adminClient->getKey(), $adminToken->token->client_id); - $this->assertSame($admin->getAuthIdentifier(), $adminToken->token->user_id); + $this->assertSame($adminClient->getKey(), $adminTokenRecord->client_id); + $this->assertSame($admin->getAuthIdentifier(), $adminTokenRecord->user_id); + $this->assertSame('test admin', $adminTokenRecord->name); $this->assertInstanceOf(PersonalAccessTokenResult::class, $customerToken); - $this->assertSame($customerClient->getKey(), $customerToken->token->client_id); - $this->assertSame($customer->getAuthIdentifier(), $customerToken->token->user_id); + $this->assertSame($customerClient->getKey(), $customerTokenRecord->client_id); + $this->assertSame($customer->getAuthIdentifier(), $customerTokenRecord->user_id); + $this->assertSame('test customer', $customerTokenRecord->name); DB::enableQueryLog(); $userTokens = $user->tokens()->pluck('id')->all(); @@ -88,9 +130,9 @@ public function testIssueTokenWithDifferentProviders() $this->assertStringContainsString('and ("provider" = \'admins\')', $queries[1]['raw_query']); $this->assertStringContainsString('and ("provider" = \'customers\')', $queries[2]['raw_query']); - $this->assertEquals([$userToken->token->id], $userTokens); - $this->assertEquals([$adminToken->token->id], $adminTokens); - $this->assertEquals([$customerToken->token->id], $customerTokens); + $this->assertEquals([$userToken->accessTokenId], $userTokens); + $this->assertEquals([$adminToken->accessTokenId], $adminTokens); + $this->assertEquals([$customerToken->accessTokenId], $customerTokens); } public function testPersonalAccessTokenRequestIsDisabled() @@ -100,7 +142,7 @@ public function testPersonalAccessTokenRequestIsDisabled() $response = $this->post('/oauth/token', [ 'grant_type' => 'personal_access', - 'provider' => $user->getProvider(), + 'provider' => $user->getProviderName(), 'user_id' => $user->getKey(), 'scope' => '', ]); @@ -116,14 +158,14 @@ public function testPersonalAccessTokenRequestIsDisabled() } } -class AdminProviderStub extends Authenticatable +class AdminProviderStub extends Authenticatable implements OAuthenticatable { use HasApiTokens; protected $attributes = ['id' => 1]; } -class CustomerProviderStub extends Authenticatable +class CustomerProviderStub extends Authenticatable implements OAuthenticatable { use HasApiTokens; diff --git a/tests/Feature/RefreshTokenGrantTest.php b/tests/Feature/RefreshTokenGrantTest.php index cdc053ef5..8c718ffe2 100644 --- a/tests/Feature/RefreshTokenGrantTest.php +++ b/tests/Feature/RefreshTokenGrantTest.php @@ -49,7 +49,7 @@ public function testRefreshingToken() $this->assertSame(31536000, $newToken['expires_in']); $this->assertSame('Bearer', $newToken['token_type']); - Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + Route::get('/foo', fn (Request $request) => $request->user()->currentAccessToken()->toJson()) ->middleware('auth:api'); $this->getJson('/foo', [ @@ -95,7 +95,7 @@ public function testRefreshingTokenWithoutRevoking() $this->assertSame(31536000, $newToken['expires_in']); $this->assertSame('Bearer', $newToken['token_type']); - Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + Route::get('/foo', fn (Request $request) => $request->user()->currentAccessToken()->toJson()) ->middleware('auth:api'); $this->getJson('/foo', [ diff --git a/tests/Feature/RevokedTest.php b/tests/Feature/RevokedTest.php index 62e2b1980..59f3f3c6c 100644 --- a/tests/Feature/RevokedTest.php +++ b/tests/Feature/RevokedTest.php @@ -1,5 +1,7 @@ shouldReceive('getParsedBody')->once()->andReturn([]); $response = m::type(ResponseInterface::class); - $psrResponse = new Response(); + $psrResponse = (new PsrHttpFactory)->createResponse(new Response); $psrResponse->getBody()->write(json_encode(['access_token' => 'access-token'])); $server = m::mock(AuthorizationServer::class); @@ -40,9 +40,8 @@ public function test_a_token_can_be_issued() public function test_exceptions_are_handled() { $request = m::mock(ServerRequestInterface::class); - $request->shouldReceive('getParsedBody')->once()->andReturn([]); - app()->instance(ResponseInterface::class, new Response); + app()->instance(ResponseInterface::class, (new PsrHttpFactory)->createResponse(new Response)); $server = m::mock(AuthorizationServer::class); $server->shouldReceive('respondToAccessTokenRequest')->with( diff --git a/tests/Unit/AccessTokenTest.php b/tests/Unit/AccessTokenTest.php index db2ea1de6..28ac0ee92 100644 --- a/tests/Unit/AccessTokenTest.php +++ b/tests/Unit/AccessTokenTest.php @@ -5,7 +5,6 @@ use Laravel\Passport\AccessToken; use Laravel\Passport\Passport; use PHPUnit\Framework\TestCase; -use ReflectionObject; class AccessTokenTest extends TestCase { @@ -87,10 +86,7 @@ public function test_token_resolves_inherited_scopes() { $token = new AccessToken; - $reflector = new ReflectionObject($token); - $method = $reflector->getMethod('resolveInheritedScopes'); - $method->setAccessible(true); - $inheritedScopes = $method->invoke($token, 'admin:webhooks:read'); + $inheritedScopes = (fn () => $this->resolveInheritedScopes('admin:webhooks:read'))->call($token); $this->assertSame([ 'admin', diff --git a/tests/Unit/ApproveAuthorizationControllerTest.php b/tests/Unit/ApproveAuthorizationControllerTest.php index 903ea270a..3a3604507 100644 --- a/tests/Unit/ApproveAuthorizationControllerTest.php +++ b/tests/Unit/ApproveAuthorizationControllerTest.php @@ -8,9 +8,10 @@ use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery as m; -use Nyholm\Psr7\Response; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; +use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; +use Symfony\Component\HttpFoundation\Response; class ApproveAuthorizationControllerTest extends TestCase { @@ -38,7 +39,7 @@ public function test_complete_authorization_request() $authRequest->shouldReceive('getGrantTypeId')->once()->andReturn('authorization_code'); $authRequest->shouldReceive('setAuthorizationApproved')->once()->with(true); - $psrResponse = new Response(); + $psrResponse = (new PsrHttpFactory)->createResponse(new Response); $psrResponse->getBody()->write('response'); $server->shouldReceive('completeAuthorizationRequest') diff --git a/tests/Unit/AuthorizationControllerTest.php b/tests/Unit/AuthorizationControllerTest.php index 4d1e7ec84..31ba4750f 100644 --- a/tests/Unit/AuthorizationControllerTest.php +++ b/tests/Unit/AuthorizationControllerTest.php @@ -19,10 +19,11 @@ use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery as m; -use Nyholm\Psr7\Response; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; +use Symfony\Component\HttpFoundation\Response; class AuthorizationControllerTest extends TestCase { @@ -92,7 +93,7 @@ public function test_authorization_exceptions_are_handled() $psrRequest->shouldReceive('getQueryParams')->andReturn([]); $psrResponse = m::mock(ResponseInterface::class); - app()->instance(ResponseInterface::class, new Response); + app()->instance(ResponseInterface::class, (new PsrHttpFactory)->createResponse(new Response)); $request = m::mock(Request::class); @@ -117,7 +118,7 @@ public function test_request_is_approved_if_valid_token_exists() $guard->shouldReceive('guest')->andReturn(false); $guard->shouldReceive('user')->andReturn($user = m::mock(Authenticatable::class)); - $psrResponse = new Response(); + $psrResponse = (new PsrHttpFactory)->createResponse(new Response); $psrResponse->getBody()->write('approved'); $server->shouldReceive('validateAuthorizationRequest') ->andReturn($authRequest = m::mock(AuthorizationRequest::class)); @@ -165,7 +166,7 @@ public function test_request_is_approved_if_client_can_skip_authorization() $guard->shouldReceive('guest')->andReturn(false); $guard->shouldReceive('user')->andReturn($user = m::mock(Authenticatable::class)); - $psrResponse = new Response(); + $psrResponse = (new PsrHttpFactory)->createResponse(new Response); $psrResponse->getBody()->write('approved'); $server->shouldReceive('validateAuthorizationRequest') ->andReturn($authRequest = m::mock(AuthorizationRequest::class)); @@ -268,7 +269,7 @@ public function test_authorization_denied_if_request_has_prompt_equals_to_none() $psrRequest->shouldReceive('getQueryParams')->andReturn([]); $psrResponse = m::mock(ResponseInterface::class); - app()->instance(ResponseInterface::class, new Response); + app()->instance(ResponseInterface::class, (new PsrHttpFactory)->createResponse(new Response)); $request = m::mock(Request::class); $request->shouldReceive('session')->andReturn($session = m::mock()); @@ -321,7 +322,7 @@ public function test_authorization_denied_if_unauthenticated_and_request_has_pro $psrRequest->shouldReceive('getQueryParams')->andReturn([]); $psrResponse = m::mock(ResponseInterface::class); - app()->instance(ResponseInterface::class, new Response); + app()->instance(ResponseInterface::class, (new PsrHttpFactory)->createResponse(new Response)); $request = m::mock(Request::class); $request->shouldNotReceive('user'); diff --git a/tests/Unit/AuthorizedAccessTokenControllerTest.php b/tests/Unit/AuthorizedAccessTokenControllerTest.php index 042c316ab..f53449aa2 100644 --- a/tests/Unit/AuthorizedAccessTokenControllerTest.php +++ b/tests/Unit/AuthorizedAccessTokenControllerTest.php @@ -99,16 +99,13 @@ public function test_tokens_can_be_deleted() public function test_not_found_response_is_returned_if_user_doesnt_have_token() { - $request = Request::create('/', 'GET'); - - $this->tokenRepository->shouldReceive('findForUser')->with(3, 1)->andReturnNull(); + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthIdentifier')->andReturn(1); - $request->setUserResolver(function () { - $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthIdentifier')->andReturn(1); + $request = Request::create('/', 'GET'); + $request->setUserResolver(fn () => $user); - return $user; - }); + $this->tokenRepository->shouldReceive('findForUser')->with(3, $user)->andReturnNull(); $this->assertSame(404, $this->controller->destroy($request, 3)->status()); } diff --git a/tests/Unit/CheckTokenForAnyScopeTest.php b/tests/Unit/CheckTokenForAnyScopeTest.php index 22fe874cd..9788abe38 100644 --- a/tests/Unit/CheckTokenForAnyScopeTest.php +++ b/tests/Unit/CheckTokenForAnyScopeTest.php @@ -112,7 +112,7 @@ public function test_request_is_passed_along_if_scopes_are_present_on_token() $middleware = new CheckTokenForAnyScope($resourceServer); $request = m::mock(Request::class); $request->shouldReceive('user')->andReturn($user = m::mock()); - $user->shouldReceive('token')->andReturn($token = m::mock(AccessToken::class)); + $user->shouldReceive('currentAccessToken')->andReturn($token = m::mock(AccessToken::class)); $token->shouldReceive('can')->with('foo')->andReturn(true); $token->shouldReceive('can')->with('bar')->andReturn(false); @@ -131,7 +131,7 @@ public function test_exception_is_thrown_if_token_doesnt_have_scope() $middleware = new CheckTokenForAnyScope($resourceServer); $request = m::mock(Request::class); $request->shouldReceive('user')->andReturn($user = m::mock()); - $user->shouldReceive('token')->andReturn($token = m::mock(AccessToken::class)); + $user->shouldReceive('currentAccessToken')->andReturn($token = m::mock(AccessToken::class)); $token->shouldReceive('can')->with('foo')->andReturn(false); $token->shouldReceive('can')->with('bar')->andReturn(false); diff --git a/tests/Unit/CheckTokenTest.php b/tests/Unit/CheckTokenTest.php index ce234c202..0b3ea06a5 100644 --- a/tests/Unit/CheckTokenTest.php +++ b/tests/Unit/CheckTokenTest.php @@ -112,7 +112,7 @@ public function test_request_is_passed_along_if_scopes_are_present_on_token() $middleware = new CheckToken($resourceServer); $request = m::mock(Request::class); $request->shouldReceive('user')->andReturn($user = m::mock()); - $user->shouldReceive('token')->andReturn($token = m::mock(AccessToken::class)); + $user->shouldReceive('currentAccessToken')->andReturn($token = m::mock(AccessToken::class)); $token->shouldReceive('cant')->with('foo')->andReturn(false); $token->shouldReceive('cant')->with('bar')->andReturn(false); @@ -131,7 +131,7 @@ public function test_exception_is_thrown_if_token_doesnt_have_scope() $middleware = new CheckToken($resourceServer); $request = m::mock(Request::class); $request->shouldReceive('user')->andReturn($user = m::mock()); - $user->shouldReceive('token')->andReturn($token = m::mock(AccessToken::class)); + $user->shouldReceive('currentAccessToken')->andReturn($token = m::mock(AccessToken::class)); $token->shouldReceive('cant')->with('foo')->andReturn(true); $middleware->handle($request, function () { diff --git a/tests/Unit/ClientControllerTest.php b/tests/Unit/ClientControllerTest.php index 55a5f5eb5..3e0a80422 100644 --- a/tests/Unit/ClientControllerTest.php +++ b/tests/Unit/ClientControllerTest.php @@ -20,13 +20,13 @@ class ClientControllerTest extends TestCase public function test_all_the_clients_for_the_current_user_can_be_retrieved() { - $clientRepository = m::mock(ClientRepository::class); - $clientRepository->shouldReceive('forUser')->once()->with(1) - ->andReturn($clients = (new Client)->newCollection()); - $user = m::mock(Authenticatable::class); $user->shouldReceive('getAuthIdentifier')->andReturn(1); + $clientRepository = m::mock(ClientRepository::class); + $clientRepository->shouldReceive('forUser')->once()->with($user) + ->andReturn($clients = (new Client)->newCollection()); + $request = Request::create('/', 'GET'); $request->setUserResolver(fn () => $user); @@ -114,18 +114,15 @@ public function test_public_clients_can_be_stored() public function test_clients_can_be_updated() { + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthIdentifier')->andReturn(1); + $clients = m::mock(ClientRepository::class); $client = m::mock(Client::class); - $clients->shouldReceive('findForUser')->with(1, 1)->andReturn($client); + $clients->shouldReceive('findForUser')->with(1, $user)->andReturn($client); $request = Request::create('/', 'GET', ['name' => 'client name', 'redirect' => 'http://localhost']); - - $request->setUserResolver(function () { - $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthIdentifier')->andReturn(1); - - return $user; - }); + $request->setUserResolver(fn () => $user); $clients->shouldReceive('update')->once()->with( $client, 'client name', ['http://localhost'] @@ -152,17 +149,14 @@ public function test_clients_can_be_updated() public function test_404_response_if_client_doesnt_belong_to_user() { + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthIdentifier')->andReturn(1); + $clients = m::mock(ClientRepository::class); - $clients->shouldReceive('findForUser')->with(1, 1)->andReturnNull(); + $clients->shouldReceive('findForUser')->with(1, $user)->andReturnNull(); $request = Request::create('/', 'GET', ['name' => 'client name', 'redirect' => 'http://localhost']); - - $request->setUserResolver(function () { - $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthIdentifier')->andReturn(1); - - return $user; - }); + $request->setUserResolver(fn () => $user); $clients->shouldReceive('update')->never(); @@ -177,18 +171,15 @@ public function test_404_response_if_client_doesnt_belong_to_user() public function test_clients_can_be_deleted() { + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthIdentifier')->andReturn(1); + $clients = m::mock(ClientRepository::class); $client = m::mock(Client::class); - $clients->shouldReceive('findForUser')->with(1, 1)->andReturn($client); + $clients->shouldReceive('findForUser')->with(1, $user)->andReturn($client); $request = Request::create('/', 'GET', ['name' => 'client name', 'redirect' => 'http://localhost']); - - $request->setUserResolver(function () { - $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthIdentifier')->andReturn(1); - - return $user; - }); + $request->setUserResolver(fn () => $user); $clients->shouldReceive('delete')->once()->with( m::type(Client::class) @@ -207,17 +198,14 @@ public function test_clients_can_be_deleted() public function test_404_response_if_client_doesnt_belong_to_user_on_delete() { + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthIdentifier')->andReturn(1); + $clients = m::mock(ClientRepository::class); - $clients->shouldReceive('findForUser')->with(1, 1)->andReturnNull(); + $clients->shouldReceive('findForUser')->with(1, $user)->andReturnNull(); $request = Request::create('/', 'GET', ['name' => 'client name', 'redirect' => 'http://localhost']); - - $request->setUserResolver(function () { - $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthIdentifier')->andReturn(1); - - return $user; - }); + $request->setUserResolver(fn () => $user); $clients->shouldReceive('delete')->never(); diff --git a/tests/Unit/DenyAuthorizationControllerTest.php b/tests/Unit/DenyAuthorizationControllerTest.php index 6208749b6..ee9d6d28e 100644 --- a/tests/Unit/DenyAuthorizationControllerTest.php +++ b/tests/Unit/DenyAuthorizationControllerTest.php @@ -8,9 +8,10 @@ use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery as m; -use Nyholm\Psr7\Response; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; +use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; +use Symfony\Component\HttpFoundation\Response; class DenyAuthorizationControllerTest extends TestCase { @@ -41,7 +42,7 @@ public function test_authorization_can_be_denied() $authRequest->shouldReceive('setAuthorizationApproved')->once()->with(false); $psrResponse = m::mock(ResponseInterface::class); - app()->instance(ResponseInterface::class, new Response); + app()->instance(ResponseInterface::class, (new PsrHttpFactory)->createResponse(new Response)); $server->shouldReceive('completeAuthorizationRequest') ->with($authRequest, m::type(ResponseInterface::class)) diff --git a/tests/Unit/HandlesOAuthErrorsTest.php b/tests/Unit/HandlesOAuthErrorsTest.php index ca64a5c62..d0b5db4a6 100644 --- a/tests/Unit/HandlesOAuthErrorsTest.php +++ b/tests/Unit/HandlesOAuthErrorsTest.php @@ -7,7 +7,9 @@ use Laravel\Passport\Http\Controllers\HandlesOAuthErrors; use League\OAuth2\Server\Exception\OAuthServerException as LeagueException; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; use RuntimeException; +use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; class HandlesOAuthErrorsTest extends TestCase { @@ -27,16 +29,16 @@ public function testShouldHandleOAuthServerException() { $controller = new HandlesOAuthErrorsStubController; - $exception = new LeagueException('Error', 1, 'fatal'); + app()->instance(ResponseInterface::class, (new PsrHttpFactory)->createResponse(new Response)); $e = null; try { - $controller->test(function () use ($exception) { - throw $exception; + $controller->test(function () { + throw new LeagueException('Error', 1, 'fatal'); }); - } catch (OAuthServerException $e) { - $e = $e; + } catch (OAuthServerException $exception) { + $e = $exception; } $this->assertInstanceOf(OAuthServerException::class, $e); diff --git a/tests/Unit/HasApiTokensTest.php b/tests/Unit/HasApiTokensTest.php index 8c819c8ac..3b4afc146 100644 --- a/tests/Unit/HasApiTokensTest.php +++ b/tests/Unit/HasApiTokensTest.php @@ -3,7 +3,9 @@ namespace Laravel\Passport\Tests\Unit; use Illuminate\Container\Container; +use Illuminate\Foundation\Auth\User as Authenticatable; use Laravel\Passport\AccessToken; +use Laravel\Passport\Contracts\OAuthenticatable; use Laravel\Passport\HasApiTokens; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery as m; @@ -30,7 +32,7 @@ public function test_token_can_indicates_if_token_has_given_scope() } } -class HasApiTokensTestStub +class HasApiTokensTestStub extends Authenticatable implements OAuthenticatable { use HasApiTokens; diff --git a/tests/Unit/PersonalAccessTokenControllerTest.php b/tests/Unit/PersonalAccessTokenControllerTest.php index 8f57c8a54..e89c0967b 100644 --- a/tests/Unit/PersonalAccessTokenControllerTest.php +++ b/tests/Unit/PersonalAccessTokenControllerTest.php @@ -115,17 +115,14 @@ public function test_tokens_can_be_deleted() public function test_not_found_response_is_returned_if_user_doesnt_have_token() { - $request = Request::create('/', 'GET'); + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthIdentifier')->andReturn(1); $tokenRepository = m::mock(TokenRepository::class); - $tokenRepository->shouldReceive('findForUser')->with(3, 1)->andReturnNull(); - - $request->setUserResolver(function () { - $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthIdentifier')->andReturn(1); + $tokenRepository->shouldReceive('findForUser')->with(3, $user)->andReturnNull(); - return $user; - }); + $request = Request::create('/', 'GET'); + $request->setUserResolver(fn () => $user); $validator = m::mock(Factory::class); $controller = new PersonalAccessTokenController($tokenRepository, $validator); diff --git a/tests/Unit/TokenGuardTest.php b/tests/Unit/TokenGuardTest.php index 5427f07cd..3b96e52cb 100644 --- a/tests/Unit/TokenGuardTest.php +++ b/tests/Unit/TokenGuardTest.php @@ -62,7 +62,7 @@ public function test_user_can_be_pulled_via_bearer_token() $user = $guard->user(); $this->assertInstanceOf(TokenGuardTestUser::class, $user); - $this->assertEquals(AccessToken::fromPsrRequest($psr), $user->token()); + $this->assertEquals(AccessToken::fromPsrRequest($psr), $user->currentAccessToken()); } public function test_user_is_resolved_only_once() @@ -98,7 +98,7 @@ public function test_user_is_resolved_only_once() $user2 = $guard->user(); $this->assertInstanceOf(TokenGuardTestUser::class, $user); - $this->assertEquals(AccessToken::fromPsrRequest($psr), $user->token()); + $this->assertEquals(AccessToken::fromPsrRequest($psr), $user->currentAccessToken()); $this->assertSame($user, $user2); } @@ -172,7 +172,7 @@ public function test_users_may_be_retrieved_from_cookies_with_csrf_token_header( 'sub' => 1, 'aud' => 1, 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256'), false) ); @@ -204,7 +204,7 @@ public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header( 'sub' => 1, 'aud' => 1, 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256'), false) ); @@ -232,7 +232,7 @@ public function test_cookie_xsrf_is_verified_against_csrf_token_header() 'sub' => 1, 'aud' => 1, 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256')) ); @@ -257,7 +257,7 @@ public function test_cookie_xsrf_is_verified_against_xsrf_token_header() 'sub' => 1, 'aud' => 1, 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256')) ); @@ -290,7 +290,7 @@ public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header_ 'sub' => 1, 'aud' => 1, 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], Passport::tokenEncryptionKey($encrypter), 'HS256'), false) ); @@ -330,7 +330,7 @@ public function test_users_may_be_retrieved_from_cookies_without_encryption() 'sub' => 1, 'aud' => 1, 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], Passport::tokenEncryptionKey($encrypter), 'HS256') ); @@ -362,7 +362,7 @@ public function test_xsrf_token_cookie_without_a_token_header_is_not_accepted() 'sub' => 1, 'aud' => 1, 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256')) ); @@ -387,7 +387,7 @@ public function test_expired_cookies_may_not_be_used() 'sub' => 1, 'aud' => 1, 'csrf' => 'token', - 'expiry' => Carbon::now()->subMinutes(10)->getTimestamp(), + 'exp' => Carbon::now()->subMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256')) ); @@ -416,7 +416,7 @@ public function test_csrf_check_can_be_disabled() $encrypter->encrypt(CookieValuePrefix::create('laravel_token', $encrypter->getKey()).JWT::encode([ 'sub' => 1, 'aud' => 1, - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256'), false) ); @@ -537,7 +537,7 @@ public function test_clients_may_be_retrieved_from_cookies() 'sub' => 1, 'aud' => 1, 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256'), false) ); diff --git a/workbench/app/Models/User.php b/workbench/app/Models/User.php index 6987bfb38..a9bf21d65 100644 --- a/workbench/app/Models/User.php +++ b/workbench/app/Models/User.php @@ -4,9 +4,10 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Passport\Contracts\OAuthenticatable; use Laravel\Passport\HasApiTokens; -class User extends Authenticatable +class User extends Authenticatable implements OAuthenticatable { use HasApiTokens, Notifiable; diff --git a/workbench/database/factories/UserFactory.php b/workbench/database/factories/UserFactory.php index db6dfa3df..b96af9a4f 100644 --- a/workbench/database/factories/UserFactory.php +++ b/workbench/database/factories/UserFactory.php @@ -14,7 +14,7 @@ class UserFactory extends \Orchestra\Testbench\Factories\UserFactory /** * The name of the factory's corresponding model. * - * @var class-string<\TModel> + * @var class-string */ protected $model = User::class; }