diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 282bccee..de23afc9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: true matrix: - php: [8.2, 8.3] + php: [8.2, 8.3, 8.4] laravel: [11] name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} @@ -29,15 +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 require "illuminate/contracts=^${{ matrix.laravel }}" --no-update - composer update --prefer-dist --no-interaction --no-progress + composer update --prefer-dist --no-interaction --no-progress --with="illuminate/contracts=^${{ matrix.laravel }}" - name: Execute tests - run: vendor/bin/phpunit + run: vendor/bin/phpunit --fail-on-deprecation --fail-on-notice --fail-on-risky --fail-on-warning diff --git a/UPGRADE.md b/UPGRADE.md index a1c3b1ba..b74252a3 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 12733823..8c266b9b 100644 --- a/composer.json +++ b/composer.json @@ -18,27 +18,27 @@ "ext-json": "*", "ext-openssl": "*", "firebase/php-jwt": "^6.4", - "illuminate/auth": "^11.14", - "illuminate/console": "^11.14", - "illuminate/container": "^11.14", - "illuminate/contracts": "^11.14", - "illuminate/cookie": "^11.14", - "illuminate/database": "^11.14", - "illuminate/encryption": "^11.14", - "illuminate/http": "^11.14", - "illuminate/support": "^11.14", - "lcobucci/jwt": "^5.0", - "league/oauth2-server": "^9.0", - "nyholm/psr7": "^1.5", + "illuminate/auth": "^11.35", + "illuminate/console": "^11.35", + "illuminate/container": "^11.35", + "illuminate/contracts": "^11.35", + "illuminate/cookie": "^11.35", + "illuminate/database": "^11.35", + "illuminate/encryption": "^11.35", + "illuminate/http": "^11.35", + "illuminate/support": "^11.35", + "league/oauth2-server": "^9.1", + "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", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5|^11.0" + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.6", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.4" }, "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/routes/web.php b/routes/web.php index a9288779..1815fa4f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -17,7 +17,7 @@ $guard = config('passport.guard', null); -Route::middleware(['web', $guard ? 'auth:'.$guard : 'auth'])->group(function () { +Route::middleware(['web', $guard ? 'auth:'.$guard : 'auth'])->group(function (): void { Route::post('/token/refresh', [ 'uses' => 'TransientTokenController@refresh', 'as' => 'token.refresh', diff --git a/src/AccessToken.php b/src/AccessToken.php index 0fdff78c..146e9196 100644 --- a/src/AccessToken.php +++ b/src/AccessToken.php @@ -9,10 +9,9 @@ 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 @@ -26,19 +25,19 @@ class AccessToken implements Arrayable, Jsonable, JsonSerializable /** * 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 +59,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 +97,7 @@ protected function getToken(): ?Token /** * Convert the access token instance to an array. * - * @return array + * @return array */ public function toArray(): array { @@ -108,7 +107,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 dfee0806..722f21cb 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(), + 'jti' => $csrfToken, + 'exp' => $expiration, ], Passport::tokenEncryptionKey($this->encrypter), 'HS256'); } } diff --git a/src/Bridge/AccessTokenRepository.php b/src/Bridge/AccessTokenRepository.php index 7eb243bc..3eab0cdf 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 036a6be1..941cb75f 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/Client.php b/src/Bridge/Client.php index df9522d1..bd73c4e8 100644 --- a/src/Bridge/Client.php +++ b/src/Bridge/Client.php @@ -21,7 +21,8 @@ public function __construct( string $name, array $redirectUri, bool $isConfidential = false, - public ?string $provider = null + public ?string $provider = null, + public array $grantTypes = [] ) { $this->setIdentifier($identifier); diff --git a/src/Bridge/ClientRepository.php b/src/Bridge/ClientRepository.php index 351c8588..1a59438a 100644 --- a/src/Bridge/ClientRepository.php +++ b/src/Bridge/ClientRepository.php @@ -82,7 +82,8 @@ protected function fromClientModel(ClientModel $model): ClientEntityInterface $model->name, $model->redirect_uris, $model->confidential(), - $model->provider + $model->provider, + $model->grant_types ); } } diff --git a/src/Bridge/FormatsScopesForStorage.php b/src/Bridge/FormatsScopesForStorage.php deleted file mode 100644 index 7abea9cc..00000000 --- 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 00000000..d4e47fe9 --- /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 2b1d8163..0b3b06a7 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 c798fb50..48beb70c 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 7bda223d..e666243b 100644 --- a/src/Client.php +++ b/src/Client.php @@ -64,14 +64,10 @@ 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 initializeHasUniqueIds(): void { - parent::__construct($attributes); - $this->usesUniqueIds = Passport::$clientUuids; } @@ -147,6 +143,30 @@ protected function redirectUris(): Attribute ); } + /** + * Get the client's grant types. + */ + protected function grantTypes(): Attribute + { + return Attribute::make( + get: function (?string $value) { + if (isset($value)) { + return $this->fromJson($value); + } + + return array_keys(array_filter([ + 'authorization_code' => ! empty($this->redirect_uris), + 'client_credentials' => $this->confidential() && $this->firstParty(), + 'implicit' => ! empty($this->redirect_uris), + 'password' => $this->password_client, + 'personal_access' => $this->personal_access_client && $this->confidential(), + 'refresh_token' => true, + 'urn:ietf:params:oauth:grant-type:device_code' => true, + ])); + }, + ); + } + /** * Determine if the client is a "first party" client. */ @@ -170,17 +190,7 @@ public function skipsAuthorization(Authenticatable $user, array $scopes): bool */ public function hasGrantType(string $grantType): bool { - if (isset($this->attributes['grant_types']) && is_array($this->grant_types)) { - return in_array($grantType, $this->grant_types); - } - - return match ($grantType) { - 'authorization_code' => ! $this->personal_access_client && ! $this->password_client, - 'personal_access' => $this->personal_access_client && $this->confidential(), - 'password' => $this->password_client, - 'client_credentials' => $this->confidential(), - default => true, - }; + return in_array($grantType, $this->grant_types); } /** @@ -188,7 +198,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); } /** @@ -214,7 +224,7 @@ public function uniqueIds(): array */ public function newUniqueId(): ?string { - return $this->usesUniqueIds ? (string) Str::orderedUuid() : null; + return $this->usesUniqueIds ? (string) Str::uuid7() : null; } /** diff --git a/src/ClientRepository.php b/src/ClientRepository.php index 94b9052f..31e5965d 100644 --- a/src/ClientRepository.php +++ b/src/ClientRepository.php @@ -64,14 +64,14 @@ public function personalAccessClient(string $provider): 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 +82,7 @@ public function personalAccessClient(string $provider): Client * * @param string[] $grantTypes * @param string[] $redirectUris - * @param \Laravel\Passport\HasApiTokens $user + * @param \Laravel\Passport\HasApiTokens|null $user */ protected function create( string $name, diff --git a/src/Console/ClientCommand.php b/src/Console/ClientCommand.php index 80be539d..03e369bb 100644 --- a/src/Console/ClientCommand.php +++ b/src/Console/ClientCommand.php @@ -37,8 +37,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') )); @@ -69,7 +69,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') @@ -85,7 +85,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') @@ -93,7 +93,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); } @@ -111,7 +111,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') ); @@ -124,14 +124,14 @@ protected function createImplicitClient(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); return $clients->createAuthorizationCodeGrantClient( $this->option('name'), explode(',', $redirect), $confidential, diff --git a/src/Console/HashCommand.php b/src/Console/HashCommand.php index 92a18e80..c21a825b 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 013201cf..32bd7a39 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 0012c5ba..37ded859 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/Guards/TokenGuard.php b/src/Guards/TokenGuard.php index 488a30cd..f91b6516 100644 --- a/src/Guards/TokenGuard.php +++ b/src/Guards/TokenGuard.php @@ -209,11 +209,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; } @@ -244,9 +250,7 @@ protected function decodeJwtTokenCookie(): array */ protected function validCsrf(array $token): bool { - return isset($token['csrf']) && hash_equals( - $token['csrf'], $this->getTokenFromRequest() - ); + return isset($token['jti']) && hash_equals($token['jti'], $this->getTokenFromRequest()); } /** diff --git a/src/HasApiTokens.php b/src/HasApiTokens.php index b5721296..3ebe66df 100644 --- a/src/HasApiTokens.php +++ b/src/HasApiTokens.php @@ -31,12 +31,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) { + ->where(function (Builder $query): void { + $query->whereHas('client', function (Builder $query): void { + $query->where(function (Builder $query): void { $provider = $this->getProvider(); - $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); }); diff --git a/src/Http/Controllers/AccessTokenController.php b/src/Http/Controllers/AccessTokenController.php index 3b488e56..70abd57e 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 150e3689..ea6fa9c3 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' ); @@ -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 b39f9723..45ac106e 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 47d59c09..867bbe83 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 a552002c..f0e3e7cd 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 06fae3a2..f517c11c 100644 --- a/src/Http/Middleware/ValidateToken.php +++ b/src/Http/Middleware/ValidateToken.php @@ -80,8 +80,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 62984641..518f93b8 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\Http\Responses\SimpleViewResponse; use League\OAuth2\Server\ResourceServer; @@ -49,17 +50,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. @@ -272,7 +273,7 @@ public static function tokensExpireIn(DateTimeInterface|DateInterval|null $date } return static::$tokensExpireIn = $date instanceof DateTimeInterface - ? Carbon::now()->diff($date) + ? Date::now()->diff($date) : $date; } @@ -286,7 +287,7 @@ public static function refreshTokensExpireIn(DateTimeInterface|DateInterval|null } return static::$refreshTokensExpireIn = $date instanceof DateTimeInterface - ? Carbon::now()->diff($date) + ? Date::now()->diff($date) : $date; } @@ -300,7 +301,7 @@ public static function personalAccessTokensExpireIn(DateTimeInterface|DateInterv } return static::$personalAccessTokensExpireIn = $date instanceof DateTimeInterface - ? Carbon::now()->diff($date) + ? Date::now()->diff($date) : $date; } @@ -327,13 +328,10 @@ 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\HasApiTokens $user * @param string[] $scopes - * @return TUserModel */ - 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(), @@ -361,11 +359,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 d2abfd45..a4594647 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -11,13 +11,11 @@ use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; +use Laravel\Passport\Bridge\PersonalAccessBearerTokenResponse; use Laravel\Passport\Bridge\PersonalAccessGrant; use Laravel\Passport\Bridge\RefreshTokenRepository; use Laravel\Passport\Guards\TokenGuard; use Laravel\Passport\Http\Controllers\AuthorizationController; -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; @@ -26,6 +24,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 { @@ -51,7 +50,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'); }); } @@ -107,7 +106,6 @@ public function register(): void $this->app->singleton(ClientRepository::class); $this->registerAuthorizationServer(); - $this->registerJWTParser(); $this->registerResourceServer(); $this->registerGuard(); } @@ -117,10 +115,16 @@ public function register(): void */ protected function registerAuthorizationServer(): void { - $this->app->singleton(AuthorizationServer::class, function () { - return tap($this->makeAuthorizationServer(), function (AuthorizationServer $server) { - $server->setDefaultScope(Passport::$defaultScope); + $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() ); @@ -135,10 +139,6 @@ protected function registerAuthorizationServer(): void ); } - $server->enableGrantType( - new PersonalAccessGrant, Passport::personalAccessTokensExpireIn() - ); - $server->enableGrantType( new ClientCredentialsGrant, Passport::tokensExpireIn() ); @@ -148,8 +148,8 @@ protected function registerAuthorizationServer(): void $this->makeImplicitGrant(), Passport::tokensExpireIn() ); } - }); - }); + }) + ); } /** @@ -157,7 +157,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()); }); } @@ -179,9 +179,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()); }); } @@ -194,7 +194,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()); }); } @@ -210,24 +210,18 @@ protected function makeImplicitGrant(): ImplicitGrant /** * 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); + }); } /** @@ -262,8 +256,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'); })); }); @@ -290,7 +284,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/PersonalAccessTokenFactory.php b/src/PersonalAccessTokenFactory.php index 31419ec0..4c65030b 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 0e92898f..e3e0a484 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/ResolvesInheritedScopes.php b/src/ResolvesInheritedScopes.php index b277cf4d..8e98ee23 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 45c6c510..08e306c0 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/tests/Feature/AccessTokenControllerTest.php b/tests/Feature/AccessTokenControllerTest.php index fde77b30..1c226f8e 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; @@ -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() @@ -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() @@ -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 + * {@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 60121372..7936e39a 100644 --- a/tests/Feature/AuthorizationCodeGrantTest.php +++ b/tests/Feature/AuthorizationCodeGrantTest.php @@ -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 559a4084..486d47c0 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', diff --git a/tests/Feature/ClientCredentialsGrantTest.php b/tests/Feature/ClientCredentialsGrantTest.php index b0711e09..ff0f1ab6 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']); @@ -54,4 +55,46 @@ public function testIssueAccessToken() $response = $this->withToken($json['access_token'], $json['token_type'])->get('/bar'); $response->assertForbidden(); } + + public function testPublicClientCredentialsFails() + { + $client = ClientFactory::new()->asClientCredentials()->asPublic()->create(); + + $json = $this->post('/oauth/token', [ + 'grant_type' => 'client_credentials', + 'client_id' => $client->getKey(), + ])->assertUnauthorized()->json(); + + $this->assertSame('invalid_client', $json['error']); + $this->assertSame('Client authentication failed', $json['error_description']); + } + + 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(); + } } diff --git a/tests/Feature/ClientTest.php b/tests/Feature/ClientTest.php index fbaac916..b58a714b 100644 --- a/tests/Feature/ClientTest.php +++ b/tests/Feature/ClientTest.php @@ -75,12 +75,19 @@ public function testGrantTypesWhenColumnDoesNotExist(): void $client = new Client(); $client->exists = true; - $this->assertTrue($client->hasGrantType('foo')); - $client->personal_access_client = false; $client->password_client = false; + $this->assertFalse($client->hasGrantType('foo')); + $this->assertFalse($client->hasGrantType('authorization_code')); + $this->assertFalse($client->hasGrantType('password')); + $this->assertFalse($client->hasGrantType('personal_access')); + $this->assertFalse($client->hasGrantType('client_credentials')); + + $client->redirect = 'http://localhost'; $this->assertTrue($client->hasGrantType('authorization_code')); + $this->assertTrue($client->hasGrantType('implicit')); + unset($client->redirect); $client->personal_access_client = false; $client->password_client = true; @@ -100,11 +107,18 @@ public function testGrantTypesWhenColumnIsNull(): void $client = new Client(['grant_types' => null]); $client->exists = true; - $this->assertTrue($client->hasGrantType('foo')); - $client->personal_access_client = false; $client->password_client = false; + $this->assertFalse($client->hasGrantType('foo')); + $this->assertFalse($client->hasGrantType('authorization_code')); + $this->assertFalse($client->hasGrantType('password')); + $this->assertFalse($client->hasGrantType('personal_access')); + $this->assertFalse($client->hasGrantType('client_credentials')); + + $client->redirect = 'http://localhost'; $this->assertTrue($client->hasGrantType('authorization_code')); + $this->assertTrue($client->hasGrantType('implicit')); + unset($client->redirect); $client->personal_access_client = false; $client->password_client = true; diff --git a/tests/Feature/Console/PurgeCommand.php b/tests/Feature/Console/PurgeCommand.php index 33adda95..71d0f28b 100644 --- a/tests/Feature/Console/PurgeCommand.php +++ b/tests/Feature/Console/PurgeCommand.php @@ -1,6 +1,6 @@ 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 37ccab41..3e70ca3f 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 new file mode 100644 index 00000000..6c78b5ae --- /dev/null +++ b/tests/Feature/PasswordGrantTest.php @@ -0,0 +1,184 @@ + 'Create', + 'read' => 'Read', + 'update' => 'Update', + 'delete' => 'Delete', + ]); + } + + public function testIssueToken() + { + $client = ClientFactory::new()->asPasswordClient()->create(); + $user = UserFactory::new()->create(); + + $json = $this->post('/oauth/token', [ + 'grant_type' => 'password', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'username' => $user->email, + 'password' => 'password', + 'scope' => 'create delete', + ])->assertOk()->json(); + + $this->assertArrayHasKey('access_token', $json); + $this->assertArrayHasKey('refresh_token', $json); + $this->assertSame('Bearer', $json['token_type']); + $this->assertSame(31536000, $json['expires_in']); + + Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + ->middleware('auth:api'); + + $json = $this->withToken($json['access_token'], $json['token_type'])->get('/foo')->json(); + + $this->assertSame($client->getKey(), $json['oauth_client_id']); + $this->assertEquals($user->getAuthIdentifier(), $json['oauth_user_id']); + $this->assertSame(['create', 'delete'], $json['oauth_scopes']); + } + + public function testIssueTokenWithAllScopes() + { + $client = ClientFactory::new()->asPasswordClient()->create(); + $user = UserFactory::new()->create(); + + $json = $this->post('/oauth/token', [ + 'grant_type' => 'password', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'username' => $user->email, + 'password' => 'password', + 'scope' => '*', + ])->assertOk()->json(); + + $this->assertArrayHasKey('access_token', $json); + $this->assertArrayHasKey('refresh_token', $json); + $this->assertSame('Bearer', $json['token_type']); + $this->assertSame(31536000, $json['expires_in']); + + Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + ->middleware('auth:api'); + + $json = $this->withToken($json['access_token'], $json['token_type'])->get('/foo')->json(); + + $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()->asPasswordClient()->create(); + $adminClient = ClientFactory::new()->asPasswordClient()->create(['provider' => 'admins']); + + config([ + 'auth.providers.admins' => ['driver' => 'eloquent', 'model' => AdminProviderPasswordStub::class], + 'auth.guards.api-admins' => ['driver' => 'passport', 'provider' => 'admins'], + ]); + + Schema::create('admin_provider_password_stubs', function ($table) { + $table->id(); + $table->string('email'); + $table->string('password'); + }); + + $user = UserFactory::new()->create(); + $admin = AdminProviderPasswordStub::query()->create([ + 'email' => 'admin@example.org', + 'password' => 'admin-password', + ]); + + $json = $this->post('/oauth/token', [ + 'grant_type' => 'password', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'username' => $user->email, + 'password' => 'password', + ])->assertOk()->json(); + + Route::get('/foo', fn (Request $request) => response()->json([ + 'user' => $request->user(), + 'token' => $request->user()->token(), + ]))->middleware('auth:api'); + + $json = $this->withToken($json['access_token'], $json['token_type'])->get('/foo')->json(); + + $this->assertSame($user->getAuthIdentifier(), $json['user']['id']); + $this->assertSame($user->email, $json['user']['email']); + $this->assertSame($client->getKey(), $json['token']['oauth_client_id']); + $this->assertEquals($user->getAuthIdentifier(), $json['token']['oauth_user_id']); + + $json = $this->post('/oauth/token', [ + 'grant_type' => 'password', + 'client_id' => $adminClient->getKey(), + 'client_secret' => $adminClient->plainSecret, + 'username' => $admin->email, + 'password' => 'admin-password', + ])->assertOk()->json(); + + Route::get('/bar', fn (Request $request) => response()->json([ + 'user' => $request->user(), + 'token' => $request->user()->token(), + ]))->middleware('auth:api-admins'); + + $json = $this->withToken($json['access_token'], $json['token_type'])->get('/bar')->json(); + + $this->assertSame($admin->getAuthIdentifier(), $json['user']['id']); + $this->assertSame($admin->email, $json['user']['email']); + $this->assertSame($adminClient->getKey(), $json['token']['oauth_client_id']); + $this->assertEquals($admin->getAuthIdentifier(), $json['token']['oauth_user_id']); + } + + public function testPublicClient() + { + $client = ClientFactory::new()->asPasswordClient()->asPublic()->create(); + $user = UserFactory::new()->create(); + + $json = $this->post('/oauth/token', [ + 'grant_type' => 'password', + 'client_id' => $client->getKey(), + 'username' => $user->email, + 'password' => 'password', + ])->assertOk()->json(); + + $this->assertArrayHasKey('access_token', $json); + $this->assertArrayHasKey('refresh_token', $json); + $this->assertSame('Bearer', $json['token_type']); + $this->assertSame(31536000, $json['expires_in']); + } +} + +class AdminProviderPasswordStub extends Authenticatable +{ + use HasApiTokens; + + public $timestamps = false; + + protected $guarded = false; + + protected $casts = [ + 'password' => 'hashed', + ]; +} diff --git a/tests/Feature/PersonalAccessGrantTest.php b/tests/Feature/PersonalAccessGrantTest.php index 3a9a0fe0..198e51dd 100644 --- a/tests/Feature/PersonalAccessGrantTest.php +++ b/tests/Feature/PersonalAccessGrantTest.php @@ -3,7 +3,9 @@ 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\Database\Factories\ClientFactory; use Laravel\Passport\HasApiTokens; @@ -29,20 +31,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()->token()->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 +93,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 +129,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() diff --git a/tests/Feature/RevokedTest.php b/tests/Feature/RevokedTest.php index 9a61f3dc..0951e4b9 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/ApproveAuthorizationControllerTest.php b/tests/Unit/ApproveAuthorizationControllerTest.php index 903ea270..3a360450 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 4d1e7ec8..31ba4750 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 042c316a..f53449aa 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/BridgeClientRepositoryTest.php b/tests/Unit/BridgeClientRepositoryTest.php index a5330d77..27813da8 100644 --- a/tests/Unit/BridgeClientRepositoryTest.php +++ b/tests/Unit/BridgeClientRepositoryTest.php @@ -173,6 +173,7 @@ public function test_without_grant_types() { $client = $this->clientModelRepository->findActive(1); $client->grant_types = null; + $client->redirect_uris = null; $this->assertTrue($this->repository->validateClient(1, 'secret', 'client_credentials')); $this->assertFalse($this->repository->validateClient(1, 'wrong-secret', 'client_credentials')); diff --git a/tests/Unit/ClientControllerTest.php b/tests/Unit/ClientControllerTest.php index 55a5f5eb..3e0a8042 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 6208749b..ee9d6d28 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 ca64a5c6..d0b5db4a 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/PersonalAccessTokenControllerTest.php b/tests/Unit/PersonalAccessTokenControllerTest.php index 8f57c8a5..e89c0967 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 5427f07c..4da13fd4 100644 --- a/tests/Unit/TokenGuardTest.php +++ b/tests/Unit/TokenGuardTest.php @@ -171,8 +171,8 @@ public function test_users_may_be_retrieved_from_cookies_with_csrf_token_header( $encrypter->encrypt(CookieValuePrefix::create('laravel_token', $encrypter->getKey()).JWT::encode([ 'sub' => 1, 'aud' => 1, - 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'jti' => 'token', + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256'), false) ); @@ -203,8 +203,8 @@ public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header( $encrypter->encrypt(CookieValuePrefix::create('laravel_token', $encrypter->getKey()).JWT::encode([ 'sub' => 1, 'aud' => 1, - 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'jti' => 'token', + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256'), false) ); @@ -231,8 +231,8 @@ public function test_cookie_xsrf_is_verified_against_csrf_token_header() $encrypter->encrypt(JWT::encode([ 'sub' => 1, 'aud' => 1, - 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'jti' => 'token', + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256')) ); @@ -256,8 +256,8 @@ public function test_cookie_xsrf_is_verified_against_xsrf_token_header() $encrypter->encrypt(JWT::encode([ 'sub' => 1, 'aud' => 1, - 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'jti' => 'token', + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256')) ); @@ -289,8 +289,8 @@ public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header_ $encrypter->encrypt(CookieValuePrefix::create('laravel_token', $encrypter->getKey()).JWT::encode([ 'sub' => 1, 'aud' => 1, - 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'jti' => 'token', + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], Passport::tokenEncryptionKey($encrypter), 'HS256'), false) ); @@ -329,8 +329,8 @@ public function test_users_may_be_retrieved_from_cookies_without_encryption() JWT::encode([ 'sub' => 1, 'aud' => 1, - 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'jti' => 'token', + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], Passport::tokenEncryptionKey($encrypter), 'HS256') ); @@ -361,8 +361,8 @@ public function test_xsrf_token_cookie_without_a_token_header_is_not_accepted() $encrypter->encrypt(JWT::encode([ 'sub' => 1, 'aud' => 1, - 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'jti' => 'token', + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256')) ); @@ -386,8 +386,8 @@ public function test_expired_cookies_may_not_be_used() $encrypter->encrypt(JWT::encode([ 'sub' => 1, 'aud' => 1, - 'csrf' => 'token', - 'expiry' => Carbon::now()->subMinutes(10)->getTimestamp(), + 'jti' => 'token', + '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) ); @@ -536,8 +536,8 @@ public function test_clients_may_be_retrieved_from_cookies() $encrypter->encrypt(CookieValuePrefix::create('laravel_token', $encrypter->getKey()).JWT::encode([ 'sub' => 1, 'aud' => 1, - 'csrf' => 'token', - 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + 'jti' => 'token', + 'exp' => Carbon::now()->addMinutes(10)->getTimestamp(), ], str_repeat('a', 16), 'HS256'), false) );