diff --git a/.gitattributes b/.gitattributes index efd7a3a8e..6c56d0f01 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,5 +14,6 @@ .gitignore export-ignore .styleci.yml export-ignore CHANGELOG.md export-ignore +phpstan.neon.dist export-ignore phpunit.xml.dist export-ignore UPGRADE.md diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md deleted file mode 100644 index 2d7b68a23..000000000 --- a/.github/ISSUE_TEMPLATE/1_Bug_report.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: "Bug report" -about: "Report something that's broken. Please ensure your Laravel version is still supported: https://laravel.com/docs/releases#support-policy" ---- - - - - -- Passport Version: #.#.# -- Laravel Version: #.#.# -- PHP Version: #.#.# -- Database Driver & Version: - -### Description: - - -### Steps To Reproduce: - - - diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yml b/.github/ISSUE_TEMPLATE/1_Bug_report.yml new file mode 100644 index 000000000..bc02d3a13 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yml @@ -0,0 +1,47 @@ +name: Bug Report +description: "Report a general library issue." +body: + - type: markdown + attributes: + value: "Before submitting your report, [please ensure your Laravel version is still supported](https://laravel.com/docs/releases#support-policy)." + - type: input + attributes: + label: Passport Version + description: Provide the Passport version that you are using. + placeholder: 10.0.1 + validations: + required: true + - type: input + attributes: + label: Laravel Version + description: Provide the Laravel version that you are using. + placeholder: 10.4.1 + validations: + required: true + - type: input + attributes: + label: PHP Version + description: Provide the PHP version that you are using. + placeholder: 8.1.4 + validations: + required: true + - type: input + attributes: + label: Database Driver & Version + description: If applicable, provide the database driver and version you are using. + placeholder: "MySQL 8.0.31 for macOS 13.0 on arm64 (Homebrew)" + validations: + required: false + - type: textarea + attributes: + label: Description + description: Provide a detailed description of the issue you are facing. + validations: + required: true + - type: textarea + attributes: + label: Steps To Reproduce + description: Provide detailed steps to reproduce your issue. If necessary, please provide a GitHub repository to demonstrate your issue using `laravel new bug-report --github="--public"`. + validations: + required: true + diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml new file mode 100644 index 000000000..9634a0edb --- /dev/null +++ b/.github/workflows/issues.yml @@ -0,0 +1,12 @@ +name: issues + +on: + issues: + types: [labeled] + +permissions: + issues: write + +jobs: + help-wanted: + uses: laravel/.github/.github/workflows/issues.yml@main diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 000000000..efea805e1 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,41 @@ +name: static analysis + +on: + push: + branches: + - master + - '*.x' + pull_request: + +permissions: + contents: read + +jobs: + tests: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: true + + name: Static Analysis + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + tools: composer:v2 + coverage: none + + - name: Install dependencies + uses: nick-fields/retry@v2 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Execute type checking + run: vendor/bin/phpstan diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 64514f889..3ae685569 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,30 +2,33 @@ name: tests on: push: + branches: + - master + - '*.x' pull_request: schedule: - cron: '0 0 * * *' jobs: tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: true matrix: - php: [7.3, 7.4, '8.0', 8.1] - laravel: [8, 9] + php: ['8.0', 8.1, 8.2, 8.3] + laravel: [9, 10] exclude: - - php: 7.3 - laravel: 9 - - php: 7.4 + - php: '8.0' + laravel: 10 + - php: 8.3 laravel: 9 name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 543b8f20b..c2a573833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,177 @@ # Release Notes -## [Unreleased](https://github.com/laravel/passport/compare/v10.4.1...10.x) +## [Unreleased](https://github.com/laravel/passport/compare/v11.10.0...11.x) + +## [v11.10.0](https://github.com/laravel/passport/compare/v11.9.2...v11.10.0) - 2023-11-02 + +- [11.x] Named static methods for middleware by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/passport/pull/1695 +- Simplify Conditional Statement by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/passport/pull/1696 + +## [v11.9.2](https://github.com/laravel/passport/compare/v11.9.1...v11.9.2) - 2023-10-16 + +- Add return to revokeRefreshTokensByAccessTokenId method by [@aminkhoshzahmat](https://github.com/aminkhoshzahmat) in https://github.com/laravel/passport/pull/1693 + +## [v11.9.1](https://github.com/laravel/passport/compare/v11.9.0...v11.9.1) - 2023-09-01 + +- [11.x] Allow scope repository to be constructed without parameters by [@axlon](https://github.com/axlon) in https://github.com/laravel/passport/pull/1686 + +## [v11.9.0](https://github.com/laravel/passport/compare/v11.8.8...v11.9.0) - 2023-08-29 + +- [11.x] Add the ability to limit scopes by client by [@axlon](https://github.com/axlon) in https://github.com/laravel/passport/pull/1682 +- [11.x] Add support for inherited scopes when limiting scopes on clients by [@axlon](https://github.com/axlon) in https://github.com/laravel/passport/pull/1683 + +## [v11.8.8](https://github.com/laravel/passport/compare/v11.8.7...v11.8.8) - 2023-07-07 + +- Add generics to client factory by [@axlon](https://github.com/axlon) in https://github.com/laravel/passport/pull/1669 +- Update composer.json by [@Smoggert](https://github.com/Smoggert) in https://github.com/laravel/passport/pull/1674 +- Update composer.json by [@drhoussem](https://github.com/drhoussem) in https://github.com/laravel/passport/pull/1677 + +## [v11.8.7](https://github.com/laravel/passport/compare/v11.8.6...v11.8.7) - 2023-04-28 + +- Revert "[11.x] Add Provider Guard to ClientRepository for Personal Access Clients" by @driesvints in https://github.com/laravel/passport/pull/1658 + +## [v11.8.6](https://github.com/laravel/passport/compare/v11.8.5...v11.8.6) - 2023-04-24 + +- Add Provider Guard to ClientRepository for Personal Access Clients by @michaelnabil230 in https://github.com/laravel/passport/pull/1655 + +## [v11.8.5](https://github.com/laravel/passport/compare/v11.8.4...v11.8.5) - 2023-04-04 + +- Allow `lcobucci/jwt` v5 and cleaned up version constraints by @GrahamCampbell in https://github.com/laravel/passport/pull/1649 +- Pass user identifier through to finalize scopes in personal access grant by @GrahamCampbell in https://github.com/laravel/passport/pull/1650 + +## [v11.8.4](https://github.com/laravel/passport/compare/v11.8.3...v11.8.4) - 2023-03-18 + +- Removed deprecated `dates` property from `RefreshToken` model by @siarheipashkevich in https://github.com/laravel/passport/pull/1645 +- Removed deprecated `dates` property from `AuthCode` model by @siarheipashkevich in https://github.com/laravel/passport/pull/1644 +- Fix doc block types by @hafezdivandari in https://github.com/laravel/passport/pull/1647 + +## [v11.8.3](https://github.com/laravel/passport/compare/v11.8.2...v11.8.3) - 2023-03-01 + +- Allow overriding the `AccessToken` class by @hafezdivandari in https://github.com/laravel/passport/pull/1638 +- Make `$userId` nullable in `ClientRepository->createPersonalAccessClient` by @bram-pkg in https://github.com/laravel/passport/pull/1642 + +## [v11.8.2](https://github.com/laravel/passport/compare/v11.8.1...v11.8.2) - 2023-02-20 + +- Re-apply "Added AuthenticationException to extend the behaviour of Laravel's default exception handler" by @driesvints in https://github.com/laravel/passport/commit/67c3e336af163f6eba5dbca8e5db46275ff0e433 + +## [v11.8.1](https://github.com/laravel/passport/compare/v11.8.0...v11.8.1) - 2023-02-20 + +- Revert "Move AuthenticationException into the scope of Laravel Passport" by @driesvints in https://github.com/laravel/passport/commit/db543b0cc13ed3f56f1bffda04707fbe2a8c7ab5 + +## [v11.8.0](https://github.com/laravel/passport/compare/v11.7.0...v11.8.0) - 2023-02-17 + +- Move AuthenticationException into the scope of Laravel Passport by @chrispage1 in https://github.com/laravel/passport/pull/1633 +- Custom authorization view response by @JonErickson in https://github.com/laravel/passport/pull/1629 +- Fix deprecated $dates property by @TonyWong9527 in https://github.com/laravel/passport/pull/1636 + +## [v11.7.0](https://github.com/laravel/passport/compare/v11.6.1...v11.7.0) - 2023-02-08 + +### Added + +- Add support for `EncryptCookies` middleware by @axlon in https://github.com/laravel/passport/pull/1628 + +## [v11.6.1](https://github.com/laravel/passport/compare/v11.6.0...v11.6.1) - 2023-02-03 + +### Changed + +- Indicate current token can be `TransientToken` by @axlon in https://github.com/laravel/passport/pull/1627 + +## [v11.6.0](https://github.com/laravel/passport/compare/v11.5.1...v11.6.0) - 2023-01-31 + +### Changed + +- Update ClientCommand.php's user_id description by @Smoggert in https://github.com/laravel/passport/pull/1619 +- Get model PK instead of forcibly id column by @lucaspanik in https://github.com/laravel/passport/pull/1626 + +### Fixed + +- Fix doc block for `withAccessToken()` by @axlon in https://github.com/laravel/passport/pull/1620 + +## [v11.5.1](https://github.com/laravel/passport/compare/v11.5.0...v11.5.1) - 2023-01-16 + +### Fixed + +- Get authenticated user from the guard by @hafezdivandari in https://github.com/laravel/passport/pull/1617 + +## [v11.5.0](https://github.com/laravel/passport/compare/v11.4.0...v11.5.0) - 2023-01-09 + +### Added + +- Laravel v10 Support by @driesvints in https://github.com/laravel/passport/pull/1615 + +## [v11.4.0](https://github.com/laravel/passport/compare/v11.3.1...v11.4.0) - 2023-01-03 + +### Changed + +- Uses PHP Native Type Declarations 🐘 by @nunomaduro in https://github.com/laravel/passport/pull/1594 + +## [v11.3.1](https://github.com/laravel/passport/compare/v11.3.0...v11.3.1) - 2022-12-02 + +### Changed + +- Add auth guard to routes by @hafezdivandari in https://github.com/laravel/passport/pull/1603 + +## [v11.3.0](https://github.com/laravel/passport/compare/v11.2.1...v11.3.0) - 2022-10-22 + +### Added + +- Support prompting login when redirecting for authorization by @hafezdivandari in https://github.com/laravel/passport/pull/1577 + +### Changed + +- Update PurgeCommand.php by @fatoskurtishi in https://github.com/laravel/passport/pull/1586 +- Fix ClientRepository doc blocks by @axlon in https://github.com/laravel/passport/pull/1587 +- Update docblock by @mnabialek in https://github.com/laravel/passport/pull/1588 + +## [v11.2.1](https://github.com/laravel/passport/compare/v11.2.0...v11.2.1) - 2022-09-29 + +### Fixed + +- Improve token guard return type by @axlon in https://github.com/laravel/passport/pull/1579 + +## [v11.2.0](https://github.com/laravel/passport/compare/v11.1.0...v11.2.0) - 2022-09-07 + +### Changed + +- Let OAuth2 server handle the denying response by @hafezdivandari in https://github.com/laravel/passport/pull/1572 + +## [v11.1.0](https://github.com/laravel/passport/compare/v11.0.1...v11.1.0) - 2022-09-05 + +### Added + +- Support prompting re-consent when redirecting for authorization by @hafezdivandari in https://github.com/laravel/passport/pull/1567 +- Support disabling prompt when redirecting for authorization by @hafezdivandari in https://github.com/laravel/passport/pull/1569 + +## [v11.0.1](https://github.com/laravel/passport/compare/v11.0.0...v11.0.1) - 2022-08-29 + +### Changed + +- Custom days and hours to passport purge command by @rubengg86 in https://github.com/laravel/passport/pull/1563 +- Allow for bootstrapping without loading routes by @axlon in https://github.com/laravel/passport/pull/1564 + +## [v11.0.0](https://github.com/laravel/passport/compare/v10.4.1...v11.0.0) - 2022-08-19 + +### Added + +- Allow authenticated client to be retrieved from the guard by @axlon in https://github.com/laravel/passport/pull/1508 + +### Changed + +- Revert model DB connection customization by @driesvints in https://github.com/laravel/passport/pull/1412 +- Allow timestamps on Token model by @driesvints in https://github.com/laravel/passport/pull/1425 +- Improve authenticateViaBearerToken() performance by @alecpl in https://github.com/laravel/passport/pull/1447 +- Refactor routes to dedicated file by @driesvints in https://github.com/laravel/passport/pull/1464 + +### Fixed + +- Stub client on guard when calling Passport::actingAsClient() by @axlon in https://github.com/laravel/passport/pull/1519 +- Fix scope inheritance when using Passport::actingAs() by @axlon in https://github.com/laravel/passport/pull/1551 + +### Removed + +- Drop PHP 7.x and Laravel v8 by @driesvints in https://github.com/laravel/passport/pull/1558 +- Remove deprecated properties by @driesvints in https://github.com/laravel/passport/pull/1560 +- Remove deprecated functionality and simplify some feature tests by @driesvints in https://github.com/laravel/passport/pull/1559 ## [v10.4.1](https://github.com/laravel/passport/compare/v10.4.0...v10.4.1) - 2022-04-16 diff --git a/UPGRADE.md b/UPGRADE.md index 4df35ceb5..643f7640d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -2,6 +2,50 @@ ## General Notes +## Upgrading To 11.0 From 10.x + +### Minimum PHP Version + +PHP 8.0 is now the minimum required version. + +### Minimum Laravel Version + +Laravel 9.0 is now the minimum required version. + +### Reverting Model DB Connection Customization + +PR: https://github.com/laravel/passport/pull/1412 + +Customizing model database connections through the migration files has been reverted. This was first introduced in [this PR](https://github.com/laravel/passport/pull/1255). + +If you need to customize the database connection for a model you should override the models [as explained in the documentation](https://laravel.com/docs/9.x/passport#overriding-default-models). + +### Allow Timestamps On Token model + +PR: https://github.com/laravel/passport/pull/1425 + +Timestamps are now allowed on the `Token` model. If you specifically didn't want these model's timestamps to be updated then you may override the `Token` model [as explained in the documentation](https://laravel.com/docs/9.x/passport#overriding-default-models). + +### Refactor Routes To Dedicated File + +PR: https://github.com/laravel/passport/pull/1464 + +Passport's routes have been moved to a dedicated route file. You can remove the `Passport::routes()` call from your application's service provider. + +If you previously relied on overwriting routes using `routes($callback = null, array $options = [])` you may now achieve the same behavior by simply overwriting the routes in your application's own `web.php` route file. + +### Stubbing Client In Tests + +PR: https://github.com/laravel/passport/pull/1519 + +Previously, a stubbed client created via `Passport::actingAsClient(...)` wasn't retrieved when calling the `->client()` method on the API guard. This has been fixed in Passport v11 to reflect real-world situations and you may need to accommodate for this behavior in your tests. + +### Scope Inheritance In Tests + +PR: https://github.com/laravel/passport/pull/1551 + +Previously, scopes weren't inherited when using `Passport::actingAs(...)`. This has been fixed in Passport v11 to reflect real-world situations and you may need to accommodate for this behavior in your tests. + ## Upgrading To 10.0 From 9.x ### Minimum PHP Version diff --git a/composer.json b/composer.json index ccc2f768f..16ec45c82 100644 --- a/composer.json +++ b/composer.json @@ -14,27 +14,28 @@ } ], "require": { - "php": "^7.3|^8.0", + "php": "^8.0", "ext-json": "*", - "firebase/php-jwt": "^6.0", - "illuminate/auth": "^8.37|^9.0", - "illuminate/console": "^8.37|^9.0", - "illuminate/container": "^8.37|^9.0", - "illuminate/contracts": "^8.37|^9.0", - "illuminate/cookie": "^8.37|^9.0", - "illuminate/database": "^8.37|^9.0", - "illuminate/encryption": "^8.37|^9.0", - "illuminate/http": "^8.37|^9.0", - "illuminate/support": "^8.37|^9.0", - "lcobucci/jwt": "^3.4|^4.0", + "firebase/php-jwt": "^6.4", + "illuminate/auth": "^9.0|^10.0", + "illuminate/console": "^9.0|^10.0", + "illuminate/container": "^9.0|^10.0", + "illuminate/contracts": "^9.0|^10.0", + "illuminate/cookie": "^9.0|^10.0", + "illuminate/database": "^9.0|^10.0", + "illuminate/encryption": "^9.0|^10.0", + "illuminate/http": "^9.0|^10.0", + "illuminate/support": "^9.0|^10.0", + "lcobucci/jwt": "^4.3|^5.0", "league/oauth2-server": "dev-device-flow-grant-update2", - "nyholm/psr7": "^1.3", + "nyholm/psr7": "^1.5", "phpseclib/phpseclib": "^2.0|^3.0", - "symfony/psr-http-message-bridge": "^2.0" + "symfony/psr-http-message-bridge": "^2.1" }, "require-dev": { "mockery/mockery": "^1.0", - "orchestra/testbench": "^6.0|^7.0", + "orchestra/testbench": "^7.31|^8.11", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.3" }, "autoload": { @@ -45,12 +46,14 @@ }, "autoload-dev": { "psr-4": { - "Laravel\\Passport\\Tests\\": "tests/" + "Laravel\\Passport\\Tests\\": "tests/", + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/" } }, "extra": { "branch-alias": { - "dev-master": "10.x-dev" + "dev-master": "11.x-dev" }, "laravel": { "providers": [ @@ -61,6 +64,10 @@ "config": { "sort-packages": true }, + "scripts": { + "post-autoload-dump": "@prepare", + "prepare": "@php vendor/bin/testbench package:discover --ansi" + }, "minimum-stability": "dev", "prefer-stable": true, "repositories": [ diff --git a/config/passport.php b/config/passport.php index 64ba5f082..06053cd12 100644 --- a/config/passport.php +++ b/config/passport.php @@ -2,6 +2,19 @@ return [ + /* + |-------------------------------------------------------------------------- + | Passport Guard + |-------------------------------------------------------------------------- + | + | Here you may specify which authentication guard Passport will use when + | authenticating users. This value should correspond with one of your + | guards that is already present in your "auth" configuration file. + | + */ + + 'guard' => 'web', + /* |-------------------------------------------------------------------------- | Encryption Keys @@ -46,20 +59,4 @@ 'secret' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET'), ], - /* - |-------------------------------------------------------------------------- - | Passport Storage Driver - |-------------------------------------------------------------------------- - | - | This configuration value allows you to customize the storage options - | for Passport, such as the database connection that should be used - | by Passport's internal database models which store tokens, etc. - | - */ - - 'storage' => [ - 'database' => [ - 'connection' => env('DB_CONNECTION', 'mysql'), - ], - ], ]; diff --git a/database/factories/ClientFactory.php b/database/factories/ClientFactory.php index 07796191b..eec50f041 100644 --- a/database/factories/ClientFactory.php +++ b/database/factories/ClientFactory.php @@ -7,6 +7,9 @@ use Laravel\Passport\Client; use Laravel\Passport\Passport; +/** + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\Laravel\Passport\Client> + */ class ClientFactory extends Factory { /** diff --git a/database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php b/database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php index 195685f7a..7b93b406a 100644 --- a/database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php +++ b/database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php @@ -6,31 +6,12 @@ return new class extends Migration { - /** - * The database schema. - * - * @var \Illuminate\Database\Schema\Builder - */ - protected $schema; - - /** - * Create a new migration instance. - * - * @return void - */ - public function __construct() - { - $this->schema = Schema::connection($this->getConnection()); - } - /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { - $this->schema->create('oauth_auth_codes', function (Blueprint $table) { + Schema::create('oauth_auth_codes', function (Blueprint $table) { $table->string('id', 100)->primary(); $table->unsignedBigInteger('user_id')->index(); $table->unsignedBigInteger('client_id'); @@ -42,21 +23,9 @@ public function up() /** * Reverse the migrations. - * - * @return void - */ - public function down() - { - $this->schema->dropIfExists('oauth_auth_codes'); - } - - /** - * Get the migration connection name. - * - * @return string|null */ - public function getConnection() + public function down(): void { - return config('passport.storage.database.connection'); + Schema::dropIfExists('oauth_auth_codes'); } }; diff --git a/database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php b/database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php index c8ecd7227..598798eef 100644 --- a/database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php +++ b/database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php @@ -6,31 +6,12 @@ return new class extends Migration { - /** - * The database schema. - * - * @var \Illuminate\Database\Schema\Builder - */ - protected $schema; - - /** - * Create a new migration instance. - * - * @return void - */ - public function __construct() - { - $this->schema = Schema::connection($this->getConnection()); - } - /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { - $this->schema->create('oauth_access_tokens', function (Blueprint $table) { + Schema::create('oauth_access_tokens', function (Blueprint $table) { $table->string('id', 100)->primary(); $table->unsignedBigInteger('user_id')->nullable()->index(); $table->unsignedBigInteger('client_id'); @@ -44,21 +25,9 @@ public function up() /** * Reverse the migrations. - * - * @return void - */ - public function down() - { - $this->schema->dropIfExists('oauth_access_tokens'); - } - - /** - * Get the migration connection name. - * - * @return string|null */ - public function getConnection() + public function down(): void { - return config('passport.storage.database.connection'); + Schema::dropIfExists('oauth_access_tokens'); } }; diff --git a/database/migrations/2016_06_01_000003_create_oauth_refresh_tokens_table.php b/database/migrations/2016_06_01_000003_create_oauth_refresh_tokens_table.php index 998b63158..b007904ce 100644 --- a/database/migrations/2016_06_01_000003_create_oauth_refresh_tokens_table.php +++ b/database/migrations/2016_06_01_000003_create_oauth_refresh_tokens_table.php @@ -6,31 +6,12 @@ return new class extends Migration { - /** - * The database schema. - * - * @var \Illuminate\Database\Schema\Builder - */ - protected $schema; - - /** - * Create a new migration instance. - * - * @return void - */ - public function __construct() - { - $this->schema = Schema::connection($this->getConnection()); - } - /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { - $this->schema->create('oauth_refresh_tokens', function (Blueprint $table) { + Schema::create('oauth_refresh_tokens', function (Blueprint $table) { $table->string('id', 100)->primary(); $table->string('access_token_id', 100)->index(); $table->boolean('revoked'); @@ -40,21 +21,9 @@ public function up() /** * Reverse the migrations. - * - * @return void - */ - public function down() - { - $this->schema->dropIfExists('oauth_refresh_tokens'); - } - - /** - * Get the migration connection name. - * - * @return string|null */ - public function getConnection() + public function down(): void { - return config('passport.storage.database.connection'); + Schema::dropIfExists('oauth_refresh_tokens'); } }; 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 48b316dd7..fdfdb488f 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 @@ -4,43 +4,13 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ - /** - * The database schema. - * - * @var \Illuminate\Database\Schema\Builder - */ - protected $schema; - - /** - * Create a new migration instance. - * - * @return void - */ - public function __construct() - { - $this->schema = Schema::connection($this->getConnection()); - } - - /** - * Get the migration connection name. - * - * @return string|null - */ - public function getConnection() - { - return config('passport.storage.database.connection'); - } - +return new class extends Migration { /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { - $this->schema->create('oauth_clients', function (Blueprint $table) { + Schema::create('oauth_clients', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('user_id')->nullable()->index(); $table->string('name'); @@ -51,18 +21,15 @@ public function up() $table->boolean('password_client'); $table->boolean('device_client'); $table->boolean('revoked'); - $table->string('user_provider_name')->nullable()->default(null); $table->timestamps(); }); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - $this->schema->dropIfExists('oauth_clients'); + Schema::dropIfExists('oauth_clients'); } }; diff --git a/database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php b/database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php index e12920ab7..7c9d1e8f1 100644 --- a/database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php +++ b/database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php @@ -6,31 +6,12 @@ return new class extends Migration { - /** - * The database schema. - * - * @var \Illuminate\Database\Schema\Builder - */ - protected $schema; - - /** - * Create a new migration instance. - * - * @return void - */ - public function __construct() - { - $this->schema = Schema::connection($this->getConnection()); - } - /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { - $this->schema->create('oauth_personal_access_clients', function (Blueprint $table) { + Schema::create('oauth_personal_access_clients', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('client_id'); $table->timestamps(); @@ -39,21 +20,9 @@ public function up() /** * Reverse the migrations. - * - * @return void - */ - public function down() - { - $this->schema->dropIfExists('oauth_personal_access_clients'); - } - - /** - * Get the migration connection name. - * - * @return string|null */ - public function getConnection() + public function down(): void { - return config('passport.storage.database.connection'); + Schema::dropIfExists('oauth_personal_access_clients'); } }; diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 000000000..649776a4c --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,11 @@ +parameters: + paths: + - config + - database + - routes + - src + + level: 0 + + ignoreErrors: + - "#Unsafe usage of new static\\(\\)#" diff --git a/resources/views/authorize.blade.php b/resources/views/authorize.blade.php index ecbdcaa58..d0a4a991c 100644 --- a/resources/views/authorize.blade.php +++ b/resources/views/authorize.blade.php @@ -68,7 +68,7 @@ @csrf - + @@ -79,7 +79,7 @@ @method('DELETE') - + diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 000000000..c69d4ddb1 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,90 @@ + 'AccessTokenController@issueToken', + 'as' => 'token', + 'middleware' => 'throttle', +]); + +Route::get('/authorize', [ + 'uses' => 'AuthorizationController@authorize', + 'as' => 'authorizations.authorize', + 'middleware' => 'web', +]); + +Route::post('/device_authorization', [ + 'uses' => 'DeviceAuthorizationController@authorize', + 'as' => 'authorizations.authorize_device', + 'middleware' => 'throttle', +]); + +$guard = config('passport.guard', null); + +Route::middleware(['web', $guard ? 'auth:'.$guard : 'auth'])->group(function () { + Route::post('/token/refresh', [ + 'uses' => 'TransientTokenController@refresh', + 'as' => 'token.refresh', + ]); + + Route::post('/authorize', [ + 'uses' => 'ApproveAuthorizationController@approve', + 'as' => 'authorizations.approve', + ]); + + Route::delete('/authorize', [ + 'uses' => 'DenyAuthorizationController@deny', + 'as' => 'authorizations.deny', + ]); + + Route::get('/tokens', [ + 'uses' => 'AuthorizedAccessTokenController@forUser', + 'as' => 'tokens.index', + ]); + + Route::delete('/tokens/{token_id}', [ + 'uses' => 'AuthorizedAccessTokenController@destroy', + 'as' => 'tokens.destroy', + ]); + + Route::get('/clients', [ + 'uses' => 'ClientController@forUser', + 'as' => 'clients.index', + ]); + + Route::post('/clients', [ + 'uses' => 'ClientController@store', + 'as' => 'clients.store', + ]); + + Route::put('/clients/{client_id}', [ + 'uses' => 'ClientController@update', + 'as' => 'clients.update', + ]); + + Route::delete('/clients/{client_id}', [ + 'uses' => 'ClientController@destroy', + 'as' => 'clients.destroy', + ]); + + Route::get('/scopes', [ + 'uses' => 'ScopeController@all', + 'as' => 'scopes.index', + ]); + + Route::get('/personal-access-tokens', [ + 'uses' => 'PersonalAccessTokenController@forUser', + 'as' => 'personal.tokens.index', + ]); + + Route::post('/personal-access-tokens', [ + 'uses' => 'PersonalAccessTokenController@store', + 'as' => 'personal.tokens.store', + ]); + + Route::delete('/personal-access-tokens/{token_id}', [ + 'uses' => 'PersonalAccessTokenController@destroy', + 'as' => 'personal.tokens.destroy', + ]); +}); diff --git a/src/AuthCode.php b/src/AuthCode.php index 94441b5f7..c3b194ad7 100644 --- a/src/AuthCode.php +++ b/src/AuthCode.php @@ -34,15 +34,7 @@ class AuthCode extends Model */ protected $casts = [ 'revoked' => 'bool', - ]; - - /** - * The attributes that should be mutated to dates. - * - * @var array - */ - protected $dates = [ - 'expires_at', + 'expires_at' => 'datetime', ]; /** diff --git a/src/Bridge/AccessTokenRepository.php b/src/Bridge/AccessTokenRepository.php index 0fe2be49b..23572d760 100644 --- a/src/Bridge/AccessTokenRepository.php +++ b/src/Bridge/AccessTokenRepository.php @@ -5,6 +5,7 @@ use DateTime; use Illuminate\Contracts\Events\Dispatcher; use Laravel\Passport\Events\AccessTokenCreated; +use Laravel\Passport\Passport; use Laravel\Passport\TokenRepository; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; @@ -46,7 +47,7 @@ public function __construct(TokenRepository $tokenRepository, Dispatcher $events */ public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null) { - return new AccessToken($userIdentifier, $scopes, $clientEntity); + return new Passport::$accessTokenEntity($userIdentifier, $scopes, $clientEntity); } /** diff --git a/src/Bridge/PersonalAccessGrant.php b/src/Bridge/PersonalAccessGrant.php index 289a0257a..4eb5f869c 100644 --- a/src/Bridge/PersonalAccessGrant.php +++ b/src/Bridge/PersonalAccessGrant.php @@ -20,14 +20,22 @@ public function respondToAccessTokenRequest( // Validate request $client = $this->validateClient($request); $scopes = $this->validateScopes($this->getRequestParameter('scope', $request)); + $userIdentifier = $this->getRequestParameter('user_id', $request); // Finalize the requested scopes - $scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client); + $scopes = $this->scopeRepository->finalizeScopes( + $scopes, + $this->getIdentifier(), + $client, + $userIdentifier + ); // Issue and persist access token $accessToken = $this->issueAccessToken( - $accessTokenTTL, $client, - $this->getRequestParameter('user_id', $request), $scopes + $accessTokenTTL, + $client, + $userIdentifier, + $scopes ); // Inject access token into response type diff --git a/src/Bridge/ScopeRepository.php b/src/Bridge/ScopeRepository.php index 4e7e7ec12..05e0ac9a1 100644 --- a/src/Bridge/ScopeRepository.php +++ b/src/Bridge/ScopeRepository.php @@ -2,12 +2,31 @@ namespace Laravel\Passport\Bridge; +use Laravel\Passport\ClientRepository; use Laravel\Passport\Passport; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; class ScopeRepository implements ScopeRepositoryInterface { + /** + * The client repository. + * + * @var \Laravel\Passport\ClientRepository|null + */ + protected ?ClientRepository $clients; + + /** + * Create a new scope repository. + * + * @param \Laravel\Passport\ClientRepository|null $clients + * @return void + */ + public function __construct(?ClientRepository $clients = null) + { + $this->clients = $clients; + } + /** * {@inheritdoc} */ @@ -31,8 +50,12 @@ public function finalizeScopes( })->values()->all(); } + $client = $this->clients?->findActive($clientEntity->getIdentifier()); + return collect($scopes)->filter(function ($scope) { return Passport::hasScope($scope->getIdentifier()); + })->when($client, function ($scopes, $client) { + return $scopes->filter(fn ($scope) => $client->hasScope($scope->getIdentifier())); })->values()->all(); } } diff --git a/src/Client.php b/src/Client.php index f75fb7b97..9a8b9e202 100644 --- a/src/Client.php +++ b/src/Client.php @@ -10,6 +10,7 @@ class Client extends Model { use HasFactory; + use ResolvesInheritedScopes; /** * The database table used by the model. @@ -41,8 +42,10 @@ class Client extends Model */ protected $casts = [ 'grant_types' => 'array', + 'scopes' => 'array', 'personal_access_client' => 'bool', 'password_client' => 'bool', + 'device_client' => 'bool', 'revoked' => 'bool', ]; @@ -154,6 +157,31 @@ public function skipsAuthorization() return false; } + /** + * Determine whether the client has the given scope. + * + * @param string $scope + * @return bool + */ + public function hasScope($scope) + { + if (! is_array($this->scopes)) { + return true; + } + + $scopes = Passport::$withInheritedScopes + ? $this->resolveInheritedScopes($scope) + : [$scope]; + + foreach ($scopes as $scope) { + if (in_array($scope, $this->scopes)) { + return true; + } + } + + return false; + } + /** * Determine if the client is a confidential client. * @@ -184,16 +212,6 @@ public function getIncrementing() return Passport::clientUuids() ? false : $this->incrementing; } - /** - * Get the current connection name for the model. - * - * @return string|null - */ - public function getConnectionName() - { - return config('passport.storage.database.connection') ?? $this->connection; - } - /** * Create a new factory instance for the model. * diff --git a/src/ClientRepository.php b/src/ClientRepository.php index f3e617e80..e2c5cc5e1 100644 --- a/src/ClientRepository.php +++ b/src/ClientRepository.php @@ -37,7 +37,7 @@ public function __construct($personalAccessClientId = null, $personalAccessClien /** * Get a client by the given ID. * - * @param int $id + * @param int|string $id * @return \Laravel\Passport\Client|null */ public function find($id) @@ -50,7 +50,7 @@ public function find($id) /** * Get an active client by the given ID. * - * @param int $id + * @param int|string $id * @return \Laravel\Passport\Client|null */ public function findActive($id) @@ -63,7 +63,7 @@ public function findActive($id) /** * Get a client instance for the given ID and user ID. * - * @param int $clientId + * @param int|string $clientId * @param mixed $userId * @return \Laravel\Passport\Client|null */ @@ -128,7 +128,7 @@ public function personalAccessClient() /** * Store a new client. * - * @param int $userId + * @param int|null $userId * @param string $name * @param string $redirect * @param string|null $provider @@ -161,7 +161,7 @@ public function create($userId, $name, $redirect, $provider = null, $personalAcc /** * Store a new personal access token client. * - * @param int $userId + * @param int|null $userId * @param string $name * @param string $redirect * @return \Laravel\Passport\Client @@ -170,7 +170,7 @@ public function createPersonalAccessClient($userId, $name, $redirect) { return tap($this->create($userId, $name, $redirect, null, true), function ($client) { $accessClient = Passport::personalAccessClient(); - $accessClient->client_id = $client->id; + $accessClient->client_id = $client->getKey(); $accessClient->save(); }); } @@ -178,10 +178,10 @@ public function createPersonalAccessClient($userId, $name, $redirect) /** * Store a new password grant client. * - * @param int $userId + * @param int|null $userId * @param string $name * @param string $redirect - * @param string|null $provider + * @param string|null $provider * @return \Laravel\Passport\Client */ public function createPasswordGrantClient($userId, $name, $redirect, $provider = null) @@ -195,6 +195,7 @@ public function createPasswordGrantClient($userId, $name, $redirect, $provider = * @param int $userId * @param string $name * @param string $redirect + * @param string|null $provider * @return \Laravel\Passport\Client */ public function createDeviceCodeGrantClient($userId, $name, $redirect, $provider = null) @@ -237,7 +238,7 @@ public function regenerateSecret(Client $client) /** * Determine if the given client is revoked. * - * @param int $id + * @param int|string $id * @return bool */ public function revoked($id) diff --git a/src/Console/ClientCommand.php b/src/Console/ClientCommand.php index f92a4f846..d5172d3d8 100644 --- a/src/Console/ClientCommand.php +++ b/src/Console/ClientCommand.php @@ -104,6 +104,14 @@ protected function createPasswordClient(ClientRepository $clients) in_array('users', $providers) ? 'users' : null ); + $providers = array_keys(config('auth.providers')); + + $provider = $this->option('provider') ?: $this->choice( + 'Which user provider should this client use to retrieve users?', + $providers, + in_array('users', $providers) ? 'users' : null + ); + $client = $clients->createPasswordGrantClient( null, $name, 'http://localhost', $provider ); @@ -144,7 +152,7 @@ protected function createClientCredentialsClient(ClientRepository $clients) protected function createAuthCodeClient(ClientRepository $clients) { $userId = $this->option('user_id') ?: $this->ask( - 'Which user ID should the client be assigned to?' + 'Which user ID should the client be assigned to? (Optional)' ); $name = $this->option('name') ?: $this->ask( @@ -208,7 +216,7 @@ protected function outputClientDetails(Client $client) $this->line(''); } - $this->line('Client ID: '.$client->id); + $this->line('Client ID: '.$client->getKey()); $this->line('Client secret: '.$client->plainSecret); } } diff --git a/src/Console/PurgeCommand.php b/src/Console/PurgeCommand.php index 7854c6ab2..9d9e66bcf 100644 --- a/src/Console/PurgeCommand.php +++ b/src/Console/PurgeCommand.php @@ -15,7 +15,8 @@ class PurgeCommand extends Command */ protected $signature = 'passport:purge {--revoked : Only purge revoked tokens and authentication codes} - {--expired : Only purge expired tokens and authentication codes}'; + {--expired : Only purge expired tokens and authentication codes} + {--hours= : The number of hours to retain expired tokens}'; /** * The console command description. @@ -29,7 +30,9 @@ class PurgeCommand extends Command */ public function handle() { - $expired = Carbon::now()->subDays(7); + $expired = $this->option('hours') + ? Carbon::now()->subHours($this->option('hours')) + : Carbon::now()->subDays(7); if (($this->option('revoked') && $this->option('expired')) || (! $this->option('revoked') && ! $this->option('expired'))) { @@ -37,7 +40,9 @@ public function handle() Passport::authCode()->where('revoked', 1)->orWhereDate('expires_at', '<', $expired)->delete(); Passport::refreshToken()->where('revoked', 1)->orWhereDate('expires_at', '<', $expired)->delete(); - $this->info('Purged revoked items and items expired for more than seven days.'); + $this->option('hours') + ? $this->info('Purged revoked items and items expired for more than '.$this->option('hours').' hours.') + : $this->info('Purged revoked items and items expired for more than seven days.'); } elseif ($this->option('revoked')) { Passport::token()->where('revoked', 1)->delete(); Passport::authCode()->where('revoked', 1)->delete(); @@ -49,7 +54,9 @@ public function handle() Passport::authCode()->whereDate('expires_at', '<', $expired)->delete(); Passport::refreshToken()->whereDate('expires_at', '<', $expired)->delete(); - $this->info('Purged items expired for more than seven days.'); + $this->option('hours') + ? $this->info('Purged items expired for more than '.$this->option('hours').' hours.') + : $this->info('Purged items expired for more than seven days.'); } } } diff --git a/src/Contracts/AuthorizationViewResponse.php b/src/Contracts/AuthorizationViewResponse.php new file mode 100644 index 000000000..6594c6624 --- /dev/null +++ b/src/Contracts/AuthorizationViewResponse.php @@ -0,0 +1,16 @@ +server = $server; $this->tokens = $tokens; $this->clients = $clients; $this->provider = $provider; $this->encrypter = $encrypter; + $this->request = $request; } /** - * Determine if the requested provider matches the client's provider. + * Get the user for the incoming request. * - * @param \Illuminate\Http\Request $request - * @return bool + * @return mixed */ - protected function hasValidProvider(Request $request) + public function user() { - $client = $this->client($request); - - if ($client && ! $client->provider) { - return true; + if (! is_null($this->user)) { + return $this->user; } - return $client && $client->provider === $this->provider->getProviderName(); + if ($this->request->bearerToken()) { + return $this->user = $this->authenticateViaBearerToken($this->request); + } elseif ($this->request->cookie(Passport::cookie())) { + return $this->user = $this->authenticateViaCookie($this->request); + } } /** - * Get the user for the incoming request. + * Validate a user's credentials. * - * @param \Illuminate\Http\Request $request - * @return mixed + * @param array $credentials + * @return bool */ - public function user(Request $request) + public function validate(array $credentials = []) { - if ($request->bearerToken()) { - return $this->authenticateViaBearerToken($request); - } elseif ($request->cookie(Passport::cookie())) { - return $this->authenticateViaCookie($request); - } + return ! is_null((new static( + $this->server, + $this->provider, + $this->tokens, + $this->clients, + $this->encrypter, + $credentials['request'], + ))->user()); } /** * Get the client for the incoming request. * - * @param \Illuminate\Http\Request $request - * @return mixed + * @return \Laravel\Passport\Client|null */ - public function client(Request $request) + public function client() { - if ($request->bearerToken()) { - if (! $psr = $this->getPsrRequestViaBearerToken($request)) { + if (! is_null($this->client)) { + return $this->client; + } + + if ($this->request->bearerToken()) { + if (! $psr = $this->getPsrRequestViaBearerToken($this->request)) { return; } - return $this->clients->findActive( + return $this->client = $this->clients->findActive( $psr->getAttribute('oauth_client_id') ); - } elseif ($request->cookie(Passport::cookie())) { - if ($token = $this->getTokenViaCookie($request)) { - return $this->clients->findActive($token['aud']); + } elseif ($this->request->cookie(Passport::cookie())) { + if ($token = $this->getTokenViaCookie($this->request)) { + return $this->client = $this->clients->findActive($token['aud']); } } } @@ -149,7 +179,13 @@ protected function authenticateViaBearerToken($request) return; } - if (! $this->hasValidProvider($request)) { + $client = $this->clients->findActive( + $psr->getAttribute('oauth_client_id') + ); + + if (! $client || + ($client->provider && + $client->provider !== $this->provider->getProviderName())) { return; } @@ -171,15 +207,6 @@ protected function authenticateViaBearerToken($request) $psr->getAttribute('oauth_access_token_id') ); - $clientId = $psr->getAttribute('oauth_client_id'); - - // Finally, we will verify if the client that issued this token is still valid and - // its tokens may still be used. If not, we will bail out since we don't want a - // user to be able to send access tokens for deleted or revoked applications. - if ($this->clients->revoked($clientId)) { - return; - } - return $token ? $user->withAccessToken($token) : null; } @@ -187,7 +214,7 @@ protected function authenticateViaBearerToken($request) * Authenticate and get the incoming PSR-7 request via the Bearer token. * * @param \Illuminate\Http\Request $request - * @return \Psr\Http\Message\ServerRequestInterface + * @return \Psr\Http\Message\ServerRequestInterface|null */ protected function getPsrRequestViaBearerToken($request) { @@ -268,8 +295,12 @@ protected function getTokenViaCookie($request) */ protected function decodeJwtTokenCookie($request) { + $jwt = $request->cookie(Passport::cookie()); + return (array) JWT::decode( - CookieValuePrefix::remove($this->encrypter->decrypt($request->cookie(Passport::cookie()), Passport::$unserializesCookies)), + Passport::$decryptsCookies + ? CookieValuePrefix::remove($this->encrypter->decrypt($jwt, Passport::$unserializesCookies)) + : $jwt, new Key(Passport::tokenEncryptionKey($this->encrypter), 'HS256') ); } @@ -305,6 +336,19 @@ protected function getTokenFromRequest($request) return $token; } + /** + * Set the current request instance. + * + * @param \Illuminate\Http\Request $request + * @return $this + */ + public function setRequest(Request $request) + { + $this->request = $request; + + return $this; + } + /** * Determine if the cookie contents should be serialized. * @@ -314,4 +358,17 @@ public static function serialized() { return EncryptCookies::serialized('XSRF-TOKEN'); } + + /** + * Set the client for the current request. + * + * @param \Laravel\Passport\Client $client + * @return $this + */ + public function setClient(Client $client) + { + $this->client = $client; + + return $this; + } } diff --git a/src/HasApiTokens.php b/src/HasApiTokens.php index 198575483..e1479cc2c 100644 --- a/src/HasApiTokens.php +++ b/src/HasApiTokens.php @@ -9,7 +9,7 @@ trait HasApiTokens /** * The current access token for the authentication user. * - * @var \Laravel\Passport\Token + * @var \Laravel\Passport\Token|\Laravel\Passport\TransientToken|null */ protected $accessToken; @@ -36,7 +36,7 @@ public function tokens() /** * Get the current access token being used by the user. * - * @return \Laravel\Passport\Token|null + * @return \Laravel\Passport\Token|\Laravel\Passport\TransientToken|null */ public function token() { @@ -46,7 +46,7 @@ public function token() /** * Determine if the current API token has a given scope. * - * @param string $scope + * @param string $scope * @return bool */ public function tokenCan($scope) @@ -57,8 +57,8 @@ public function tokenCan($scope) /** * Create a new personal access token for the user. * - * @param string $name - * @param array $scopes + * @param string $name + * @param array $scopes * @return \Laravel\Passport\PersonalAccessTokenResult */ public function createToken($name, array $scopes = []) @@ -71,7 +71,7 @@ public function createToken($name, array $scopes = []) /** * Set the current access token for the user. * - * @param \Laravel\Passport\Token $accessToken + * @param \Laravel\Passport\Token|\Laravel\Passport\TransientToken|null $accessToken * @return $this */ public function withAccessToken($accessToken) diff --git a/src/Http/Controllers/AccessTokenController.php b/src/Http/Controllers/AccessTokenController.php index 3b77a8477..7c3b395bc 100644 --- a/src/Http/Controllers/AccessTokenController.php +++ b/src/Http/Controllers/AccessTokenController.php @@ -3,7 +3,6 @@ namespace Laravel\Passport\Http\Controllers; use Laravel\Passport\TokenRepository; -use Lcobucci\JWT\Parser as JwtParser; use League\OAuth2\Server\AuthorizationServer; use Nyholm\Psr7\Response as Psr7Response; use Psr\Http\Message\ServerRequestInterface; @@ -26,29 +25,16 @@ class AccessTokenController */ protected $tokens; - /** - * The JWT parser instance. - * - * @var \Lcobucci\JWT\Parser - * - * @deprecated This property will be removed in a future Passport version. - */ - protected $jwt; - /** * Create a new controller instance. * - * @param \League\OAuth2\Server\AuthorizationServer $server - * @param \Laravel\Passport\TokenRepository $tokens - * @param \Lcobucci\JWT\Parser $jwt + * @param \League\OAuth2\Server\AuthorizationServer $server + * @param \Laravel\Passport\TokenRepository $tokens * @return void */ - public function __construct( - AuthorizationServer $server, - TokenRepository $tokens, - JwtParser $jwt - ) { - $this->jwt = $jwt; + public function __construct(AuthorizationServer $server, + TokenRepository $tokens) + { $this->server = $server; $this->tokens = $tokens; } @@ -56,7 +42,7 @@ public function __construct( /** * Authorize a client to access the user's account. * - * @param \Psr\Http\Message\ServerRequestInterface $request + * @param \Psr\Http\Message\ServerRequestInterface $request * @return \Illuminate\Http\Response */ public function issueToken(ServerRequestInterface $request) diff --git a/src/Http/Controllers/ApproveAuthorizationController.php b/src/Http/Controllers/ApproveAuthorizationController.php index 1afcdccbf..decd754c3 100644 --- a/src/Http/Controllers/ApproveAuthorizationController.php +++ b/src/Http/Controllers/ApproveAuthorizationController.php @@ -8,7 +8,7 @@ class ApproveAuthorizationController { - use ConvertsPsrResponses, RetrievesAuthRequestFromSession; + use ConvertsPsrResponses, HandlesOAuthErrors, RetrievesAuthRequestFromSession; /** * The authorization server. @@ -40,8 +40,12 @@ public function approve(Request $request) $authRequest = $this->getAuthRequestFromSession($request); - return $this->convertResponse( - $this->server->completeAuthorizationRequest($authRequest, new Psr7Response) - ); + $authRequest->setAuthorizationApproved(true); + + return $this->withErrorHandling(function () use ($authRequest) { + return $this->convertResponse( + $this->server->completeAuthorizationRequest($authRequest, new Psr7Response) + ); + }); } } diff --git a/src/Http/Controllers/AuthorizationController.php b/src/Http/Controllers/AuthorizationController.php index 22e08ef23..38942232b 100644 --- a/src/Http/Controllers/AuthorizationController.php +++ b/src/Http/Controllers/AuthorizationController.php @@ -2,14 +2,17 @@ namespace Laravel\Passport\Http\Controllers; -use Illuminate\Contracts\Routing\ResponseFactory; +use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\Request; use Illuminate\Support\Str; use Laravel\Passport\Bridge\User; use Laravel\Passport\ClientRepository; +use Laravel\Passport\Contracts\AuthorizationViewResponse; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Passport; use Laravel\Passport\TokenRepository; use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\Exception\OAuthServerException; use Nyholm\Psr7\Response as Psr7Response; use Psr\Http\Message\ServerRequestInterface; @@ -25,9 +28,16 @@ class AuthorizationController protected $server; /** - * The response factory implementation. + * The guard implementation. * - * @var \Illuminate\Contracts\Routing\ResponseFactory + * @var \Illuminate\Contracts\Auth\StatefulGuard + */ + protected $guard; + + /** + * The authorization view response implementation. + * + * @var \Laravel\Passport\Contracts\AuthorizationViewResponse */ protected $response; @@ -35,12 +45,16 @@ class AuthorizationController * Create a new controller instance. * * @param \League\OAuth2\Server\AuthorizationServer $server - * @param \Illuminate\Contracts\Routing\ResponseFactory $response + * @param \Illuminate\Contracts\Auth\StatefulGuard $guard + * @param \Laravel\Passport\Contracts\AuthorizationViewResponse $response * @return void */ - public function __construct(AuthorizationServer $server, ResponseFactory $response) + public function __construct(AuthorizationServer $server, + StatefulGuard $guard, + AuthorizationViewResponse $response) { $this->server = $server; + $this->guard = $guard; $this->response = $response; } @@ -51,7 +65,7 @@ public function __construct(AuthorizationServer $server, ResponseFactory $respon * @param \Illuminate\Http\Request $request * @param \Laravel\Passport\ClientRepository $clients * @param \Laravel\Passport\TokenRepository $tokens - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\Response|\Laravel\Passport\Contracts\AuthorizationViewResponse */ public function authorize(ServerRequestInterface $psrRequest, Request $request, @@ -62,22 +76,40 @@ public function authorize(ServerRequestInterface $psrRequest, return $this->server->validateAuthorizationRequest($psrRequest); }); - $scopes = $this->parseScopes($authRequest); + if ($this->guard->guest()) { + return $request->get('prompt') === 'none' + ? $this->denyRequest($authRequest) + : $this->promptForLogin($request); + } - $token = $tokens->findValidToken( - $user = $request->user(), - $client = $clients->find($authRequest->getClient()->getIdentifier()) - ); + if ($request->get('prompt') === 'login' && + ! $request->session()->get('promptedForLogin', false)) { + $this->guard->logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return $this->promptForLogin($request); + } + + $request->session()->forget('promptedForLogin'); - if (($token && $token->scopes === collect($scopes)->pluck('id')->all()) || - $client->skipsAuthorization()) { + $scopes = $this->parseScopes($authRequest); + $user = $this->guard->user(); + $client = $clients->find($authRequest->getClient()->getIdentifier()); + + if ($request->get('prompt') !== 'consent' && + ($client->skipsAuthorization() || $this->hasValidToken($tokens, $user, $client, $scopes))) { return $this->approveRequest($authRequest, $user); } + if ($request->get('prompt') === 'none') { + return $this->denyRequest($authRequest, $user); + } + $request->session()->put('authToken', $authToken = Str::random()); $request->session()->put('authRequest', $authRequest); - return $this->response->view('passport::authorize', [ + return $this->response->withParameters([ 'client' => $client, 'user' => $user, 'scopes' => $scopes, @@ -101,11 +133,27 @@ protected function parseScopes($authRequest) ); } + /** + * Determine if a valid token exists for the given user, client, and scopes. + * + * @param \Laravel\Passport\TokenRepository $tokens + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param \Laravel\Passport\Client $client + * @param array $scopes + * @return bool + */ + protected function hasValidToken($tokens, $user, $client, $scopes) + { + $token = $tokens->findValidToken($user, $client); + + return $token && $token->scopes === collect($scopes)->pluck('id')->all(); + } + /** * Approve the authorization request. * * @param \League\OAuth2\Server\RequestTypes\AuthorizationRequest $authRequest - * @param \Illuminate\Database\Eloquent\Model $user + * @param \Illuminate\Contracts\Auth\Authenticatable $user * @return \Illuminate\Http\Response */ protected function approveRequest($authRequest, $user) @@ -120,4 +168,53 @@ protected function approveRequest($authRequest, $user) ); }); } + + /** + * Deny the authorization request. + * + * @param \League\OAuth2\Server\RequestTypes\AuthorizationRequest $authRequest + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * @return \Illuminate\Http\Response + */ + protected function denyRequest($authRequest, $user = null) + { + if (is_null($user)) { + $uri = $authRequest->getRedirectUri() + ?? (is_array($authRequest->getClient()->getRedirectUri()) + ? $authRequest->getClient()->getRedirectUri()[0] + : $authRequest->getClient()->getRedirectUri()); + + $separator = $authRequest->getGrantTypeId() === 'implicit' ? '#' : '?'; + + $uri = $uri.(str_contains($uri, $separator) ? '&' : $separator).'state='.$authRequest->getState(); + + return $this->withErrorHandling(function () use ($uri) { + throw OAuthServerException::accessDenied('Unauthenticated', $uri); + }); + } + + $authRequest->setUser(new User($user->getAuthIdentifier())); + + $authRequest->setAuthorizationApproved(false); + + return $this->withErrorHandling(function () use ($authRequest) { + return $this->convertResponse( + $this->server->completeAuthorizationRequest($authRequest, new Psr7Response) + ); + }); + } + + /** + * Prompt the user to login by throwing an AuthenticationException. + * + * @param \Illuminate\Http\Request $request + * + * @throws \Laravel\Passport\Exceptions\AuthenticationException + */ + protected function promptForLogin($request) + { + $request->session()->put('promptedForLogin', true); + + throw new AuthenticationException; + } } diff --git a/src/Http/Controllers/DenyAuthorizationController.php b/src/Http/Controllers/DenyAuthorizationController.php index 49411e3ca..3a46e6174 100644 --- a/src/Http/Controllers/DenyAuthorizationController.php +++ b/src/Http/Controllers/DenyAuthorizationController.php @@ -2,36 +2,36 @@ namespace Laravel\Passport\Http\Controllers; -use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Http\Request; -use Illuminate\Support\Arr; +use League\OAuth2\Server\AuthorizationServer; +use Nyholm\Psr7\Response as Psr7Response; class DenyAuthorizationController { - use RetrievesAuthRequestFromSession; + use ConvertsPsrResponses, HandlesOAuthErrors, RetrievesAuthRequestFromSession; /** - * The response factory implementation. + * The authorization server. * - * @var \Illuminate\Contracts\Routing\ResponseFactory + * @var \League\OAuth2\Server\AuthorizationServer */ - protected $response; + protected $server; /** * Create a new controller instance. * - * @param \Illuminate\Contracts\Routing\ResponseFactory $response + * @param \League\OAuth2\Server\AuthorizationServer $server * @return void */ - public function __construct(ResponseFactory $response) + public function __construct(AuthorizationServer $server) { - $this->response = $response; + $this->server = $server; } /** * Deny the authorization request. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse */ public function deny(Request $request) @@ -40,16 +40,12 @@ public function deny(Request $request) $authRequest = $this->getAuthRequestFromSession($request); - $clientUris = Arr::wrap($authRequest->getClient()->getRedirectUri()); + $authRequest->setAuthorizationApproved(false); - if (!in_array($uri = $authRequest->getRedirectUri(), $clientUris)) { - $uri = Arr::first($clientUris); - } - - $separator = $authRequest->getGrantTypeId() === 'implicit' ? '#' : (strstr($uri, '?') ? '&' : '?'); - - return $this->response->redirectTo( - $uri . $separator . 'error=access_denied&state=' . $request->input('state') - ); + return $this->withErrorHandling(function () use ($authRequest) { + return $this->convertResponse( + $this->server->completeAuthorizationRequest($authRequest, new Psr7Response) + ); + }); } } diff --git a/src/Http/Controllers/RetrievesAuthRequestFromSession.php b/src/Http/Controllers/RetrievesAuthRequestFromSession.php index b1296277c..0a23e1ec4 100644 --- a/src/Http/Controllers/RetrievesAuthRequestFromSession.php +++ b/src/Http/Controllers/RetrievesAuthRequestFromSession.php @@ -42,8 +42,6 @@ protected function getAuthRequestFromSession(Request $request) } $authRequest->setUser(new User($request->user()->getAuthIdentifier())); - - $authRequest->setAuthorizationApproved(true); }); } } diff --git a/src/Http/Middleware/CheckClientCredentials.php b/src/Http/Middleware/CheckClientCredentials.php index fc6e049e6..25644dd24 100644 --- a/src/Http/Middleware/CheckClientCredentials.php +++ b/src/Http/Middleware/CheckClientCredentials.php @@ -2,7 +2,7 @@ namespace Laravel\Passport\Http\Middleware; -use Illuminate\Auth\AuthenticationException; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Exceptions\MissingScopeException; class CheckClientCredentials extends CheckCredentials @@ -13,7 +13,7 @@ class CheckClientCredentials extends CheckCredentials * @param \Laravel\Passport\Token $token * @return void * - * @throws \Illuminate\Auth\AuthenticationException + * @throws \Laravel\Passport\Exceptions\AuthenticationException */ protected function validateCredentials($token) { diff --git a/src/Http/Middleware/CheckClientCredentialsForAnyScope.php b/src/Http/Middleware/CheckClientCredentialsForAnyScope.php index 7c63de92a..8da32bf19 100644 --- a/src/Http/Middleware/CheckClientCredentialsForAnyScope.php +++ b/src/Http/Middleware/CheckClientCredentialsForAnyScope.php @@ -2,7 +2,7 @@ namespace Laravel\Passport\Http\Middleware; -use Illuminate\Auth\AuthenticationException; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Exceptions\MissingScopeException; class CheckClientCredentialsForAnyScope extends CheckCredentials @@ -13,7 +13,7 @@ class CheckClientCredentialsForAnyScope extends CheckCredentials * @param \Laravel\Passport\Token $token * @return void * - * @throws \Illuminate\Auth\AuthenticationException + * @throws \Laravel\Passport\Exceptions\AuthenticationException */ protected function validateCredentials($token) { diff --git a/src/Http/Middleware/CheckCredentials.php b/src/Http/Middleware/CheckCredentials.php index 20a937632..5d7d9273f 100644 --- a/src/Http/Middleware/CheckCredentials.php +++ b/src/Http/Middleware/CheckCredentials.php @@ -3,7 +3,7 @@ namespace Laravel\Passport\Http\Middleware; use Closure; -use Illuminate\Auth\AuthenticationException; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\TokenRepository; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\ResourceServer; @@ -39,6 +39,21 @@ public function __construct(ResourceServer $server, TokenRepository $repository) $this->repository = $repository; } + /** + * Specify the scopes for the middleware. + * + * @param array|string $scopes + * @return string + */ + public static function using(...$scopes) + { + if (is_array($scopes[0])) { + return static::class.':'.implode(',', $scopes[0]); + } + + return static::class.':'.implode(',', $scopes); + } + /** * Handle an incoming request. * @@ -47,7 +62,7 @@ public function __construct(ResourceServer $server, TokenRepository $repository) * @param mixed ...$scopes * @return mixed * - * @throws \Illuminate\Auth\AuthenticationException + * @throws \Laravel\Passport\Exceptions\AuthenticationException */ public function handle($request, Closure $next, ...$scopes) { @@ -93,7 +108,7 @@ protected function validate($psr, $scopes) * @param \Laravel\Passport\Token $token * @return void * - * @throws \Illuminate\Auth\AuthenticationException + * @throws \Laravel\Passport\Exceptions\AuthenticationException */ abstract protected function validateCredentials($token); diff --git a/src/Http/Middleware/CheckForAnyScope.php b/src/Http/Middleware/CheckForAnyScope.php index 0bb9653f7..77d9bb1a3 100644 --- a/src/Http/Middleware/CheckForAnyScope.php +++ b/src/Http/Middleware/CheckForAnyScope.php @@ -2,11 +2,26 @@ namespace Laravel\Passport\Http\Middleware; -use Illuminate\Auth\AuthenticationException; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Exceptions\MissingScopeException; class CheckForAnyScope { + /** + * Specify the scopes for the middleware. + * + * @param array|string $scopes + * @return string + */ + public static function using(...$scopes) + { + if (is_array($scopes[0])) { + return static::class.':'.implode(',', $scopes[0]); + } + + return static::class.':'.implode(',', $scopes); + } + /** * Handle the incoming request. * @@ -15,7 +30,7 @@ class CheckForAnyScope * @param mixed ...$scopes * @return \Illuminate\Http\Response * - * @throws \Illuminate\Auth\AuthenticationException|\Laravel\Passport\Exceptions\MissingScopeException + * @throws \Laravel\Passport\Exceptions\AuthenticationException|\Laravel\Passport\Exceptions\MissingScopeException */ public function handle($request, $next, ...$scopes) { diff --git a/src/Http/Middleware/CheckScopes.php b/src/Http/Middleware/CheckScopes.php index e19f1097e..fdcead33a 100644 --- a/src/Http/Middleware/CheckScopes.php +++ b/src/Http/Middleware/CheckScopes.php @@ -2,11 +2,26 @@ namespace Laravel\Passport\Http\Middleware; -use Illuminate\Auth\AuthenticationException; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Exceptions\MissingScopeException; class CheckScopes { + /** + * Specify the scopes for the middleware. + * + * @param array|string $scopes + * @return string + */ + public static function using(...$scopes) + { + if (is_array($scopes[0])) { + return static::class.':'.implode(',', $scopes[0]); + } + + return static::class.':'.implode(',', $scopes); + } + /** * Handle the incoming request. * @@ -15,7 +30,7 @@ class CheckScopes * @param mixed ...$scopes * @return \Illuminate\Http\Response * - * @throws \Illuminate\Auth\AuthenticationException|\Laravel\Passport\Exceptions\MissingScopeException + * @throws \Laravel\Passport\Exceptions\AuthenticationException|\Laravel\Passport\Exceptions\MissingScopeException */ public function handle($request, $next, ...$scopes) { diff --git a/src/Http/Middleware/CreateFreshApiToken.php b/src/Http/Middleware/CreateFreshApiToken.php index 869da186e..0e93f6d11 100644 --- a/src/Http/Middleware/CreateFreshApiToken.php +++ b/src/Http/Middleware/CreateFreshApiToken.php @@ -35,6 +35,19 @@ public function __construct(ApiTokenCookieFactory $cookieFactory) $this->cookieFactory = $cookieFactory; } + /** + * Specify the guard for the middleware. + * + * @param string|null $guard + * @return string + */ + public static function using($guard = null) + { + $guard = is_null($guard) ? '' : ':'.$guard; + + return static::class.$guard; + } + /** * Handle an incoming request. * diff --git a/src/Http/Responses/AuthorizationViewResponse.php b/src/Http/Responses/AuthorizationViewResponse.php new file mode 100644 index 000000000..36761d486 --- /dev/null +++ b/src/Http/Responses/AuthorizationViewResponse.php @@ -0,0 +1,68 @@ +view = $view; + } + + /** + * Add parameters to response. + * + * @param array $parameters + * @return $this + */ + public function withParameters($parameters = []) + { + $this->parameters = $parameters; + + return $this; + } + + /** + * Create an HTTP response that represents the object. + * + * @param \Illuminate\Http\Request $request + * @return \Symfony\Component\HttpFoundation\Response + */ + public function toResponse($request) + { + if (! is_callable($this->view) || is_string($this->view)) { + return response()->view($this->view, $this->parameters); + } + + $response = call_user_func($this->view, $this->parameters); + + if ($response instanceof Responsable) { + return $response->toResponse($request); + } + + return $response; + } +} diff --git a/src/Passport.php b/src/Passport.php index 506377309..ae38c38ae 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -6,7 +6,8 @@ use DateInterval; use DateTimeInterface; use Illuminate\Contracts\Encryption\Encrypter; -use Illuminate\Support\Facades\Route; +use Laravel\Passport\Contracts\AuthorizationViewResponse as AuthorizationViewResponseContract; +use Laravel\Passport\Http\Responses\AuthorizationViewResponse; use League\OAuth2\Server\ResourceServer; use Mockery; use Psr\Http\Message\ServerRequestInterface; @@ -36,15 +37,6 @@ class Passport // ]; - /** - * The date when access tokens expire. - * - * @var \DateTimeInterface|null - * - * @deprecated Will be removed in the next major Passport release. - */ - public static $tokensExpireAt; - /** * The interval when access tokens expire. * @@ -52,15 +44,6 @@ class Passport */ public static $tokensExpireIn; - /** - * The date when refresh tokens expire. - * - * @var \DateTimeInterface|null - * - * @deprecated Will be removed in the next major Passport release. - */ - public static $refreshTokensExpireAt; - /** * The date when refresh tokens expire. * @@ -68,15 +51,6 @@ class Passport */ public static $refreshTokensExpireIn; - /** - * The date when personal access tokens expire. - * - * @var \DateTimeInterface|null - * - * @deprecated Will be removed in the next major Passport release. - */ - public static $personalAccessTokensExpireAt; - /** * The date when personal access tokens expire. * @@ -105,6 +79,13 @@ class Passport */ public static $keyPath; + /** + * The access token entity class name. + * + * @var string + */ + public static $accessTokenEntity = 'Laravel\Passport\Bridge\AccessToken'; + /** * The auth code model class name. * @@ -171,6 +152,13 @@ class Passport */ public static $unserializesCookies = false; + /** + * Indicates if Passport should decrypt cookies. + * + * @var bool + */ + public static $decryptsCookies = true; + /** * Indicates if client secrets will be hashed. * @@ -199,6 +187,13 @@ class Passport */ public static $authorizationServerResponseType; + /** + * Indicates if Passport routes will be registered. + * + * @var bool + */ + public static $registersRoutes = true; + /** * Enable the implicit grant type. * @@ -211,31 +206,6 @@ public static function enableImplicitGrant() return new static; } - /** - * Binds the Passport routes into the controller. - * - * @param callable|null $callback - * @param array $options - * @return void - */ - public static function routes($callback = null, array $options = []) - { - $callback = $callback ?: function ($router) { - $router->all(); - }; - - $defaultOptions = [ - 'prefix' => 'oauth', - 'namespace' => '\Laravel\Passport\Http\Controllers', - ]; - - $options = array_merge($defaultOptions, $options); - - Route::group($options, function ($router) use ($callback) { - $callback(new RouteRegistrar($router)); - }); - } - /** * Set the default scope(s). Multiple scopes may be an array or specified delimited by spaces. * @@ -318,7 +288,6 @@ public static function tokensExpireIn(DateTimeInterface $date = null) return static::$tokensExpireIn ?? new DateInterval('P1Y'); } - static::$tokensExpireAt = $date; static::$tokensExpireIn = Carbon::now()->diff($date); return new static; @@ -336,7 +305,6 @@ public static function refreshTokensExpireIn(DateTimeInterface $date = null) return static::$refreshTokensExpireIn ?? new DateInterval('P1Y'); } - static::$refreshTokensExpireAt = $date; static::$refreshTokensExpireIn = Carbon::now()->diff($date); return new static; @@ -354,7 +322,6 @@ public static function personalAccessTokensExpireIn(DateTimeInterface $date = nu return static::$personalAccessTokensExpireIn ?? new DateInterval('P1Y'); } - static::$personalAccessTokensExpireAt = $date; static::$personalAccessTokensExpireIn = Carbon::now()->diff($date); return new static; @@ -400,11 +367,9 @@ public static function ignoreCsrfToken($ignoreCsrfToken = true) */ public static function actingAs($user, $scopes = [], $guard = 'api') { - $token = Mockery::mock(self::tokenModel())->shouldIgnoreMissing(false); + $token = app(self::tokenModel()); - foreach ($scopes as $scope) { - $token->shouldReceive('can')->with($scope)->andReturn(true); - } + $token->scopes = $scopes; $user->withAccessToken($token); @@ -424,13 +389,14 @@ public static function actingAs($user, $scopes = [], $guard = 'api') * * @param \Laravel\Passport\Client $client * @param array $scopes + * @param string $guard * @return \Laravel\Passport\Client */ - public static function actingAsClient($client, $scopes = []) + public static function actingAsClient($client, $scopes = [], $guard = 'api') { $token = app(self::tokenModel()); - $token->client_id = $client->id; + $token->client_id = $client->getKey(); $token->setRelation('client', $client); $token->scopes = $scopes; @@ -450,6 +416,10 @@ public static function actingAsClient($client, $scopes = []) app()->instance(TokenRepository::class, $mock); + app('auth')->guard($guard)->setClient($client); + + app('auth')->shouldUse($guard); + return $client; } @@ -479,6 +449,17 @@ public static function keyPath($file) : storage_path($file); } + /** + * Set the access token entity class name. + * + * @param string $accessTokenEntity + * @return void + */ + public static function useAccessTokenEntity($accessTokenEntity) + { + static::$accessTokenEntity = $accessTokenEntity; + } + /** * Set the auth code model class name. * @@ -694,34 +675,28 @@ public static function tokenEncryptionKey(Encrypter $encrypter) } /** - * Set the device code model class name. + * Specify which view should be used as the authorization view. * - * @param string $deviceCodeModel + * @param callable|string $view * @return void */ - public static function useDeviceCodeModel($deviceCodeModel) + public static function authorizationView($view) { - static::$deviceCodeModel = $deviceCodeModel; + app()->singleton(AuthorizationViewResponseContract::class, function ($app) use ($view) { + return new AuthorizationViewResponse($view); + }); } /** - * Get the device code model class name. + * Configure Passport to not register its routes. * - * @return string + * @return static */ - public static function deviceCodeModel() + public static function ignoreRoutes() { - return static::$deviceCodeModel; - } + static::$registersRoutes = false; - /** - * Get a new device code model instance. - * - * @return \Laravel\Passport\DeviceCode - */ - public static function deviceCode() - { - return new static::$deviceCodeModel; + return new static; } /** @@ -759,4 +734,60 @@ public static function withoutCookieSerialization() return new static; } + + /** + * Instruct Passport to enable cookie encryption. + * + * @return static + */ + public static function withCookieEncryption() + { + static::$decryptsCookies = true; + + return new static; + } + + /** + * Instruct Passport to disable cookie encryption. + * + * @return static + */ + public static function withoutCookieEncryption() + { + static::$decryptsCookies = false; + + return new static; + } + + /** + * + * Set the device code model class name. + * + * @param string $deviceCodeModel + * @return void + */ + public static function useDeviceCodeModel($deviceCodeModel) + { + static::$deviceCodeModel = $deviceCodeModel; + } + + /** + * Get the device code model class name. + * + * @return string + */ + public static function deviceCodeModel() + { + return static::$deviceCodeModel; + } + + /** + * Get a new device code model instance. + * + * @return \Laravel\Passport\DeviceCode + */ + public static function deviceCode() + { + return new static::$deviceCodeModel; + } } diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index e87b51846..99e345e17 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -4,18 +4,21 @@ use DateInterval; use Illuminate\Auth\Events\Logout; -use Illuminate\Auth\RequestGuard; use Illuminate\Config\Repository as Config; +use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cookie; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Request; +use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; use Laravel\Passport\Bridge\PersonalAccessGrant; use Laravel\Passport\Bridge\RefreshTokenRepository; use Laravel\Passport\Guards\TokenGuard; -use Lcobucci\JWT\Configuration; -use Lcobucci\JWT\Parser; +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; @@ -34,13 +37,63 @@ class PassportServiceProvider extends ServiceProvider */ public function boot() { - $this->loadViewsFrom(__DIR__.'/../resources/views', 'passport'); + $this->registerRoutes(); + $this->registerResources(); + $this->registerMigrations(); + $this->registerPublishing(); + $this->registerCommands(); $this->deleteCookieOnLogout(); + } - if ($this->app->runningInConsole()) { - $this->registerMigrations(); + /** + * Register the Passport routes. + * + * @return void + */ + protected function registerRoutes() + { + if (Passport::$registersRoutes) { + Route::group([ + 'as' => 'passport.', + 'prefix' => config('passport.path', 'oauth'), + 'namespace' => 'Laravel\Passport\Http\Controllers', + ], function () { + $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); + }); + } + } + + /** + * Register the Passport resources. + * + * @return void + */ + protected function registerResources() + { + $this->loadViewsFrom(__DIR__.'/../resources/views', 'passport'); + } + + /** + * Register the Passport migration files. + * + * @return void + */ + protected function registerMigrations() + { + if ($this->app->runningInConsole() && Passport::$runsMigrations && ! config('passport.client_uuids')) { + $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); + } + } + /** + * Register the package's publishable resources. + * + * @return void + */ + protected function registerPublishing() + { + if ($this->app->runningInConsole()) { $this->publishes([ __DIR__.'/../database/migrations' => database_path('migrations'), ], 'passport-migrations'); @@ -52,26 +105,24 @@ public function boot() $this->publishes([ __DIR__.'/../config/passport.php' => config_path('passport.php'), ], 'passport-config'); - - $this->commands([ - Console\InstallCommand::class, - Console\ClientCommand::class, - Console\HashCommand::class, - Console\KeysCommand::class, - Console\PurgeCommand::class, - ]); } } /** - * Register Passport's migration files. + * Register the Passport Artisan commands. * * @return void */ - protected function registerMigrations() + protected function registerCommands() { - if (Passport::$runsMigrations && ! config('passport.client_uuids')) { - $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); + if ($this->app->runningInConsole()) { + $this->commands([ + Console\InstallCommand::class, + Console\ClientCommand::class, + Console\HashCommand::class, + Console\KeysCommand::class, + Console\PurgeCommand::class, + ]); } } @@ -86,11 +137,17 @@ public function register() Passport::setClientUuids($this->app->make(Config::class)->get('passport.client_uuids', false)); + $this->app->when(AuthorizationController::class) + ->needs(StatefulGuard::class) + ->give(fn () => Auth::guard(config('passport.guard', null))); + $this->registerAuthorizationServer(); $this->registerClientRepository(); $this->registerJWTParser(); $this->registerResourceServer(); $this->registerGuard(); + + Passport::authorizationView('passport::authorize'); } /** @@ -238,8 +295,8 @@ protected function registerClientRepository() */ protected function registerJWTParser() { - $this->app->singleton(Parser::class, function () { - return Configuration::forUnsecuredSigner()->parser(); + $this->app->singleton(ParserContract::class, function () { + return new Parser(new JoseEncoder); }); } @@ -295,19 +352,18 @@ protected function registerGuard() * Make an instance of the token guard. * * @param array $config - * @return \Illuminate\Auth\RequestGuard + * @return \Laravel\Passport\Guards\TokenGuard */ protected function makeGuard(array $config) { - return new RequestGuard(function ($request) use ($config) { - return (new TokenGuard( - $this->app->make(ResourceServer::class), - new PassportUserProvider(Auth::createUserProvider($config['provider']), $config['provider']), - $this->app->make(TokenRepository::class), - $this->app->make(ClientRepository::class), - $this->app->make('encrypter') - ))->user($request); - }, $this->app['request']); + return new TokenGuard( + $this->app->make(ResourceServer::class), + new PassportUserProvider(Auth::createUserProvider($config['provider']), $config['provider']), + $this->app->make(TokenRepository::class), + $this->app->make(ClientRepository::class), + $this->app->make('encrypter'), + $this->app->make('request') + ); } /** diff --git a/src/PersonalAccessTokenFactory.php b/src/PersonalAccessTokenFactory.php index b849d069f..15ac840ad 100644 --- a/src/PersonalAccessTokenFactory.php +++ b/src/PersonalAccessTokenFactory.php @@ -35,8 +35,6 @@ class PersonalAccessTokenFactory * The JWT token parser instance. * * @var \Lcobucci\JWT\Parser - * - * @deprecated This property will be removed in a future Passport version. */ protected $jwt; @@ -100,7 +98,7 @@ protected function createRequest($client, $userId, array $scopes) return (new ServerRequest('POST', 'not-important'))->withParsedBody([ 'grant_type' => 'personal_access', - 'client_id' => $client->id, + 'client_id' => $client->getKey(), 'client_secret' => $secret, 'user_id' => $userId, 'scope' => implode(' ', $scopes), @@ -126,7 +124,7 @@ protected function dispatchRequestToAuthorizationServer(ServerRequestInterface $ * @param array $response * @return \Laravel\Passport\Token */ - protected function findAccessToken(array $response) + public function findAccessToken(array $response) { return $this->tokens->find( $this->jwt->parse($response['access_token'])->claims()->get('jti') diff --git a/src/RefreshToken.php b/src/RefreshToken.php index 376a97b82..f0f3265a7 100644 --- a/src/RefreshToken.php +++ b/src/RefreshToken.php @@ -41,15 +41,7 @@ class RefreshToken extends Model */ protected $casts = [ 'revoked' => 'bool', - ]; - - /** - * The attributes that should be mutated to dates. - * - * @var array - */ - protected $dates = [ - 'expires_at', + 'expires_at' => 'datetime', ]; /** diff --git a/src/RefreshTokenRepository.php b/src/RefreshTokenRepository.php index ded3b0a85..bca0269e3 100644 --- a/src/RefreshTokenRepository.php +++ b/src/RefreshTokenRepository.php @@ -52,11 +52,11 @@ public function revokeRefreshToken($id) * Revokes refresh tokens by access token id. * * @param string $tokenId - * @return void + * @return mixed */ public function revokeRefreshTokensByAccessTokenId($tokenId) { - Passport::refreshToken()->where('access_token_id', $tokenId)->update(['revoked' => true]); + return Passport::refreshToken()->where('access_token_id', $tokenId)->update(['revoked' => true]); } /** diff --git a/src/ResolvesInheritedScopes.php b/src/ResolvesInheritedScopes.php new file mode 100644 index 000000000..e7658a165 --- /dev/null +++ b/src/ResolvesInheritedScopes.php @@ -0,0 +1,27 @@ +router = $router; - } - - /** - * Register routes for transient tokens, clients, and personal access tokens. - * - * @return void - */ - public function all() - { - $this->forAuthorization(); - $this->forAccessTokens(); - $this->forTransientTokens(); - $this->forClients(); - $this->forPersonalAccessTokens(); - $this->forDeviceAuthorization(); - } - - /** - * Register the routes needed for authorization. - * - * @return void - */ - public function forAuthorization() - { - $this->router->group(['middleware' => ['web', 'auth']], function ($router) { - $router->get('/authorize', [ - 'uses' => 'AuthorizationController@authorize', - 'as' => 'passport.authorizations.authorize', - ]); - - $router->post('/authorize', [ - 'uses' => 'ApproveAuthorizationController@approve', - 'as' => 'passport.authorizations.approve', - ]); - - $router->delete('/authorize', [ - 'uses' => 'DenyAuthorizationController@deny', - 'as' => 'passport.authorizations.deny', - ]); - }); - } - - /** - * Register the routes for retrieving and issuing access tokens. - * - * @return void - */ - public function forAccessTokens() - { - $this->router->post('/token', [ - 'uses' => 'AccessTokenController@issueToken', - 'as' => 'passport.token', - 'middleware' => 'throttle', - ]); - - $this->router->group(['middleware' => ['web', 'auth']], function ($router) { - $router->get('/tokens', [ - 'uses' => 'AuthorizedAccessTokenController@forUser', - 'as' => 'passport.tokens.index', - ]); - - $router->delete('/tokens/{token_id}', [ - 'uses' => 'AuthorizedAccessTokenController@destroy', - 'as' => 'passport.tokens.destroy', - ]); - }); - } - - /** - * Register the routes needed for refreshing transient tokens. - * - * @return void - */ - public function forTransientTokens() - { - $this->router->post('/token/refresh', [ - 'middleware' => ['web', 'auth'], - 'uses' => 'TransientTokenController@refresh', - 'as' => 'passport.token.refresh', - ]); - } - - /** - * Register the routes needed for managing clients. - * - * @return void - */ - public function forClients() - { - $this->router->group(['middleware' => ['web', 'auth']], function ($router) { - $router->get('/clients', [ - 'uses' => 'ClientController@forUser', - 'as' => 'passport.clients.index', - ]); - - $router->post('/clients', [ - 'uses' => 'ClientController@store', - 'as' => 'passport.clients.store', - ]); - - $router->put('/clients/{client_id}', [ - 'uses' => 'ClientController@update', - 'as' => 'passport.clients.update', - ]); - - $router->delete('/clients/{client_id}', [ - 'uses' => 'ClientController@destroy', - 'as' => 'passport.clients.destroy', - ]); - }); - } - - /** - * Register the routes needed for managing personal access tokens. - * - * @return void - */ - public function forPersonalAccessTokens() - { - $this->router->group(['middleware' => ['web', 'auth']], function ($router) { - $router->get('/scopes', [ - 'uses' => 'ScopeController@all', - 'as' => 'passport.scopes.index', - ]); - - $router->get('/personal-access-tokens', [ - 'uses' => 'PersonalAccessTokenController@forUser', - 'as' => 'passport.personal.tokens.index', - ]); - - $router->post('/personal-access-tokens', [ - 'uses' => 'PersonalAccessTokenController@store', - 'as' => 'passport.personal.tokens.store', - ]); - - $router->delete('/personal-access-tokens/{token_id}', [ - 'uses' => 'PersonalAccessTokenController@destroy', - 'as' => 'passport.personal.tokens.destroy', - ]); - }); - } - - /** - * Register the routes for issuing device codes - * - * @return void - */ - public function forDeviceAuthorization() - { - $this->router->post('/device_authorization', [ - 'uses' => 'DeviceAuthorizationController@authorize', - 'as' => 'passport.authorizations.authorize_device', - 'middleware' => 'throttle', - ]); - } -} diff --git a/src/Token.php b/src/Token.php index 9dfd874c6..ad3f80817 100644 --- a/src/Token.php +++ b/src/Token.php @@ -6,6 +6,8 @@ class Token extends Model { + use ResolvesInheritedScopes; + /** * The database table used by the model. * @@ -42,24 +44,9 @@ class Token extends Model protected $casts = [ 'scopes' => 'array', 'revoked' => 'bool', + 'expires_at' => 'datetime', ]; - /** - * The attributes that should be mutated to dates. - * - * @var array - */ - protected $dates = [ - 'expires_at', - ]; - - /** - * Indicates if the model should be timestamped. - * - * @var bool - */ - public $timestamps = false; - /** * Get the client that the token belongs to. * @@ -109,27 +96,6 @@ public function can($scope) return false; } - /** - * Resolve all possible scopes. - * - * @param string $scope - * @return array - */ - protected function resolveInheritedScopes($scope) - { - $parts = explode(':', $scope); - - $partsCount = count($parts); - - $scopes = []; - - for ($i = 1; $i <= $partsCount; $i++) { - $scopes[] = implode(':', array_slice($parts, 0, $i)); - } - - return $scopes; - } - /** * Determine if the token is missing a given scope. * @@ -160,14 +126,4 @@ public function transient() { return false; } - - /** - * Get the current connection name for the model. - * - * @return string|null - */ - public function getConnectionName() - { - return config('passport.storage.database.connection') ?? $this->connection; - } } diff --git a/src/TokenRepository.php b/src/TokenRepository.php index 8f992b37c..b68f339b8 100644 --- a/src/TokenRepository.php +++ b/src/TokenRepository.php @@ -54,7 +54,7 @@ public function forUser($userId) /** * Get a valid token instance for the given user and client. * - * @param \Illuminate\Database\Eloquent\Model $user + * @param \Illuminate\Contracts\Auth\Authenticatable $user * @param \Laravel\Passport\Client $client * @return \Laravel\Passport\Token|null */ @@ -107,7 +107,7 @@ public function isAccessTokenRevoked($id) /** * Find a valid token for the given user and client. * - * @param \Illuminate\Database\Eloquent\Model $user + * @param \Illuminate\Contracts\Auth\Authenticatable $user * @param \Laravel\Passport\Client $client * @return \Laravel\Passport\Token|null */ diff --git a/testbench.yaml b/testbench.yaml new file mode 100644 index 000000000..7d0c53214 --- /dev/null +++ b/testbench.yaml @@ -0,0 +1,5 @@ +providers: + - Laravel\Passport\PassportServiceProvider + +migrations: true + diff --git a/tests/DeviceAuthorizationControllerTest.php b/tests/DeviceAuthorizationControllerTest.php deleted file mode 100644 index 5cf6a5a8e..000000000 --- a/tests/DeviceAuthorizationControllerTest.php +++ /dev/null @@ -1,68 +0,0 @@ - 'description', - ]); - - $server = m::mock(AuthorizationServer::class); - - $controller = new DeviceAuthorizationController($server); - - $server->shouldReceive('validateDeviceAuthorizationRequest')->andReturn($deviceAuthRequest = m::mock(DeviceAuthorizationRequest::class)); - - $psrResponse = new \Zend\Diactoros\Response(); - $psrResponse->getBody()->write('response'); - - $server->shouldReceive('completeDeviceAuthorizationRequest') - ->with($deviceAuthRequest, m::type(ResponseInterface::class)) - ->andReturn($psrResponse); - - $this->assertEquals('response', $controller->authorize(m::mock(ServerRequestInterface::class))->getContent()); - - } - - public function test_authorization_exceptions_are_handled() - { - $server = m::mock(AuthorizationServer::class); - - $controller = new DeviceAuthorizationController($server); - - $server->shouldReceive('validateDeviceAuthorizationRequest')->andThrow(LeagueException::invalidCredentials()); - - $this->expectException(OAuthServerException::class); - - $controller->authorize(m::mock(ServerRequestInterface::class)); - } -} diff --git a/tests/Feature/AccessTokenControllerTest.php b/tests/Feature/AccessTokenControllerTest.php index 451107d30..92218333c 100644 --- a/tests/Feature/AccessTokenControllerTest.php +++ b/tests/Feature/AccessTokenControllerTest.php @@ -4,61 +4,35 @@ use Carbon\CarbonImmutable; use Illuminate\Contracts\Hashing\Hasher; -use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Laravel\Passport\Client; -use Laravel\Passport\ClientRepository; use Laravel\Passport\Database\Factories\ClientFactory; -use Laravel\Passport\HasApiTokens; use Laravel\Passport\Passport; +use Laravel\Passport\PersonalAccessTokenFactory; use Laravel\Passport\Token; -use Laravel\Passport\TokenRepository; -use Lcobucci\JWT\Configuration; +use Orchestra\Testbench\Concerns\WithLaravelMigrations; +use Workbench\Database\Factories\UserFactory; class AccessTokenControllerTest extends PassportTestCase { - protected function setUp(): void - { - parent::setUp(); - - Schema::create('users', function (Blueprint $table) { - $table->increments('id'); - $table->string('email')->unique(); - $table->string('password'); - $table->dateTime('created_at'); - $table->dateTime('updated_at'); - }); - } - - protected function tearDown(): void - { - Schema::dropIfExists('users'); - - parent::tearDown(); - } - - protected function getUserClass() - { - return User::class; - } + use WithLaravelMigrations; public function testGettingAccessTokenWithClientCredentialsGrant() { $this->withoutExceptionHandling(); - $user = new User(); - $user->email = 'foo@gmail.com'; - $user->password = $this->app->make(Hasher::class)->make('foobar123'); - $user->save(); + $user = UserFactory::new()->create([ + 'email' => 'foo@gmail.com', + 'password' => $this->app->make(Hasher::class)->make('foobar123'), + ]); /** @var Client $client */ - $client = ClientFactory::new()->asClientCredentials()->create(['user_id' => $user->id]); + $client = ClientFactory::new()->asClientCredentials()->create(['user_id' => $user->getKey()]); $response = $this->post( '/oauth/token', [ 'grant_type' => 'client_credentials', - 'client_id' => $client->id, + 'client_id' => $client->getKey(), 'client_secret' => $client->secret, ] ); @@ -75,13 +49,10 @@ public function testGettingAccessTokenWithClientCredentialsGrant() $this->assertArrayHasKey('expires_in', $decodedResponse); $this->assertArrayHasKey('access_token', $decodedResponse); $this->assertSame('Bearer', $decodedResponse['token_type']); - $expiresInSeconds = 31536000; + $expiresInSeconds = 31622400; $this->assertEqualsWithDelta($expiresInSeconds, $decodedResponse['expires_in'], 5); - $jwtAccessToken = Configuration::forUnsecuredSigner()->parser()->parse($decodedResponse['access_token']); - $this->assertTrue($this->app->make(ClientRepository::class)->findActive($jwtAccessToken->claims()->get('aud'))->is($client)); - - $token = $this->app->make(TokenRepository::class)->find($jwtAccessToken->claims()->get('jti')); + $token = $this->app->make(PersonalAccessTokenFactory::class)->findAccessToken($decodedResponse); $this->assertInstanceOf(Token::class, $token); $this->assertTrue($token->client->is($client)); $this->assertFalse($token->revoked); @@ -92,19 +63,19 @@ public function testGettingAccessTokenWithClientCredentialsGrant() public function testGettingAccessTokenWithClientCredentialsGrantInvalidClientSecret() { - $user = new User(); - $user->email = 'foo@gmail.com'; - $user->password = $this->app->make(Hasher::class)->make('foobar123'); - $user->save(); + $user = UserFactory::new()->create([ + 'email' => 'foo@gmail.com', + 'password' => $this->app->make(Hasher::class)->make('foobar123'), + ]); /** @var Client $client */ - $client = ClientFactory::new()->asClientCredentials()->create(['user_id' => $user->id]); + $client = ClientFactory::new()->asClientCredentials()->create(['user_id' => $user->getKey()]); $response = $this->post( '/oauth/token', [ 'grant_type' => 'client_credentials', - 'client_id' => $client->id, + 'client_id' => $client->getKey(), 'client_secret' => $client->secret.'foo', ] ); @@ -136,19 +107,19 @@ public function testGettingAccessTokenWithPasswordGrant() $this->withoutExceptionHandling(); $password = 'foobar123'; - $user = new User(); - $user->email = 'foo@gmail.com'; - $user->password = $this->app->make(Hasher::class)->make($password); - $user->save(); + $user = UserFactory::new()->create([ + 'email' => 'foo@gmail.com', + 'password' => $this->app->make(Hasher::class)->make($password), + ]); /** @var Client $client */ - $client = ClientFactory::new()->asPasswordClient()->create(['user_id' => $user->id]); + $client = ClientFactory::new()->asPasswordClient()->create(['user_id' => $user->getKey()]); $response = $this->post( '/oauth/token', [ 'grant_type' => 'password', - 'client_id' => $client->id, + 'client_id' => $client->getKey(), 'client_secret' => $client->secret, 'username' => $user->email, 'password' => $password, @@ -168,14 +139,10 @@ public function testGettingAccessTokenWithPasswordGrant() $this->assertArrayHasKey('access_token', $decodedResponse); $this->assertArrayHasKey('refresh_token', $decodedResponse); $this->assertSame('Bearer', $decodedResponse['token_type']); - $expiresInSeconds = 31536000; + $expiresInSeconds = 31622400; $this->assertEqualsWithDelta($expiresInSeconds, $decodedResponse['expires_in'], 5); - $jwtAccessToken = Configuration::forUnsecuredSigner()->parser()->parse($decodedResponse['access_token']); - $this->assertTrue($this->app->make(ClientRepository::class)->findActive($jwtAccessToken->claims()->get('aud'))->is($client)); - $this->assertTrue($this->app->make('auth')->createUserProvider()->retrieveById($jwtAccessToken->claims()->get('sub'))->is($user)); - - $token = $this->app->make(TokenRepository::class)->find($jwtAccessToken->claims()->get('jti')); + $token = $this->app->make(PersonalAccessTokenFactory::class)->findAccessToken($decodedResponse); $this->assertInstanceOf(Token::class, $token); $this->assertFalse($token->revoked); $this->assertTrue($token->user->is($user)); @@ -187,19 +154,19 @@ public function testGettingAccessTokenWithPasswordGrant() public function testGettingAccessTokenWithPasswordGrantWithInvalidPassword() { $password = 'foobar123'; - $user = new User(); - $user->email = 'foo@gmail.com'; - $user->password = $this->app->make(Hasher::class)->make($password); - $user->save(); + $user = UserFactory::new()->create([ + 'email' => 'foo@gmail.com', + 'password' => $this->app->make(Hasher::class)->make($password), + ]); /** @var Client $client */ - $client = ClientFactory::new()->asPasswordClient()->create(['user_id' => $user->id]); + $client = ClientFactory::new()->asPasswordClient()->create(['user_id' => $user->getKey()]); $response = $this->post( '/oauth/token', [ 'grant_type' => 'password', - 'client_id' => $client->id, + 'client_id' => $client->getKey(), 'client_secret' => $client->secret, 'username' => $user->email, 'password' => $password.'foo', @@ -230,19 +197,19 @@ public function testGettingAccessTokenWithPasswordGrantWithInvalidPassword() public function testGettingAccessTokenWithPasswordGrantWithInvalidClientSecret() { $password = 'foobar123'; - $user = new User(); - $user->email = 'foo@gmail.com'; - $user->password = $this->app->make(Hasher::class)->make($password); - $user->save(); + $user = UserFactory::new()->create([ + 'email' => 'foo@gmail.com', + 'password' => $this->app->make(Hasher::class)->make($password), + ]); /** @var Client $client */ - $client = ClientFactory::new()->asPasswordClient()->create(['user_id' => $user->id]); + $client = ClientFactory::new()->asPasswordClient()->create(['user_id' => $user->getKey()]); $response = $this->post( '/oauth/token', [ 'grant_type' => 'password', - 'client_id' => $client->id, + 'client_id' => $client->getKey(), 'client_secret' => $client->secret.'foo', 'username' => $user->email, 'password' => $password, @@ -277,19 +244,19 @@ public function testGettingCustomResponseType() $this->withoutExceptionHandling(); Passport::$authorizationServerResponseType = new IdTokenResponse('foo_bar_open_id_token'); - $user = new User(); - $user->email = 'foo@gmail.com'; - $user->password = $this->app->make(Hasher::class)->make('foobar123'); - $user->save(); + $user = UserFactory::new()->create([ + 'email' => 'foo@gmail.com', + 'password' => $this->app->make(Hasher::class)->make('foobar123'), + ]); /** @var Client $client */ - $client = ClientFactory::new()->asClientCredentials()->create(['user_id' => $user->id]); + $client = ClientFactory::new()->asClientCredentials()->create(['user_id' => $user->getKey()]); $response = $this->post( '/oauth/token', [ 'grant_type' => 'client_credentials', - 'client_id' => $client->id, + 'client_id' => $client->getKey(), 'client_secret' => $client->secret, ] ); @@ -303,11 +270,6 @@ public function testGettingCustomResponseType() } } -class User extends \Illuminate\Foundation\Auth\User -{ - use HasApiTokens; -} - class IdTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerTokenResponse { /** diff --git a/tests/Feature/ActingAsClientTest.php b/tests/Feature/ActingAsClientTest.php index 8fc99b777..f0a92c7d7 100644 --- a/tests/Feature/ActingAsClientTest.php +++ b/tests/Feature/ActingAsClientTest.php @@ -7,9 +7,8 @@ use Laravel\Passport\Http\Middleware\CheckClientCredentials; use Laravel\Passport\Http\Middleware\CheckClientCredentialsForAnyScope; use Laravel\Passport\Passport; -use Orchestra\Testbench\TestCase; -class ActingAsClientTest extends TestCase +class ActingAsClientTest extends PassportTestCase { public function testActingAsClientWhenTheRouteIsProtectedByCheckClientCredentialsMiddleware() { @@ -46,4 +45,11 @@ public function testActingAsClientWhenTheRouteIsProtectedByCheckClientCredential $response->assertSuccessful(); $response->assertSee('bar'); } + + public function testActingAsClientSetsTheClientOnTheGuard() + { + Passport::actingAsClient($client = new Client()); + + $this->assertSame($client, app('auth')->client()); + } } diff --git a/tests/Feature/ActingAsTest.php b/tests/Feature/ActingAsTest.php index aa1b3ecea..d67f93798 100644 --- a/tests/Feature/ActingAsTest.php +++ b/tests/Feature/ActingAsTest.php @@ -3,11 +3,11 @@ namespace Laravel\Passport\Tests\Feature; use Illuminate\Contracts\Routing\Registrar; -use Illuminate\Foundation\Auth\User; -use Laravel\Passport\HasApiTokens; +use Illuminate\Support\Facades\Route; use Laravel\Passport\Http\Middleware\CheckForAnyScope; use Laravel\Passport\Http\Middleware\CheckScopes; use Laravel\Passport\Passport; +use Workbench\App\Models\User; class ActingAsTest extends PassportTestCase { @@ -22,7 +22,7 @@ public function testActingAsWhenTheRouteIsProtectedByAuthMiddleware() return 'bar'; })->middleware('auth:api'); - Passport::actingAs(new PassportUser()); + Passport::actingAs(new User()); $response = $this->get('/foo'); $response->assertSuccessful(); @@ -40,13 +40,28 @@ public function testActingAsWhenTheRouteIsProtectedByCheckScopesMiddleware() return 'bar'; })->middleware(CheckScopes::class.':admin,footest'); - Passport::actingAs(new PassportUser(), ['admin', 'footest']); + Passport::actingAs(new User(), ['admin', 'footest']); $response = $this->get('/foo'); $response->assertSuccessful(); $response->assertSee('bar'); } + public function testItCanGenerateDefinitionViaStaticMethod() + { + $signature = (string) CheckScopes::using('admin'); + $this->assertSame('Laravel\Passport\Http\Middleware\CheckScopes:admin', $signature); + + $signature = (string) CheckScopes::using('admin', 'footest'); + $this->assertSame('Laravel\Passport\Http\Middleware\CheckScopes:admin,footest', $signature); + + $signature = (string) CheckForAnyScope::using('admin'); + $this->assertSame('Laravel\Passport\Http\Middleware\CheckForAnyScope:admin', $signature); + + $signature = (string) CheckForAnyScope::using('admin', 'footest'); + $this->assertSame('Laravel\Passport\Http\Middleware\CheckForAnyScope:admin,footest', $signature); + } + public function testActingAsWhenTheRouteIsProtectedByCheckForAnyScopeMiddleware() { $this->withoutExceptionHandling(); @@ -58,17 +73,44 @@ public function testActingAsWhenTheRouteIsProtectedByCheckForAnyScopeMiddleware( return 'bar'; })->middleware(CheckForAnyScope::class.':admin,footest'); - Passport::actingAs(new PassportUser(), ['footest']); + Passport::actingAs(new User(), ['footest']); $response = $this->get('/foo'); $response->assertSuccessful(); $response->assertSee('bar'); } -} -class PassportUser extends User -{ - use HasApiTokens; + public function testActingAsWhenTheRouteIsProtectedByCheckScopesMiddlewareWithInheritance() + { + Passport::$withInheritedScopes = true; + + $this->withoutExceptionHandling(); + + Route::middleware(CheckScopes::class.':foo:bar,baz:qux')->get('/foo', function () { + return 'bar'; + }); + + Passport::actingAs(new User(), ['foo', 'baz']); + + $response = $this->get('/foo'); + $response->assertSuccessful(); + $response->assertSee('bar'); + } + + public function testActingAsWhenTheRouteIsProtectedByCheckForAnyScopeMiddlewareWithInheritance() + { + Passport::$withInheritedScopes = true; + + $this->withoutExceptionHandling(); - protected $table = 'users'; + Route::middleware(CheckForAnyScope::class.':foo:baz,baz:qux')->get('/foo', function () { + return 'bar'; + }); + + Passport::actingAs(new User(), ['foo']); + + $response = $this->get('/foo'); + $response->assertSuccessful(); + $response->assertSee('bar'); + } } diff --git a/tests/Feature/KeysCommandTest.php b/tests/Feature/KeysCommandTest.php index e7d528f82..3d2f4b986 100644 --- a/tests/Feature/KeysCommandTest.php +++ b/tests/Feature/KeysCommandTest.php @@ -2,18 +2,8 @@ namespace Laravel\Passport\Tests\Feature; -use Mockery as m; - class KeysCommandTest extends PassportTestCase { - protected function tearDown(): void - { - m::close(); - - @unlink(self::PUBLIC_KEY); - @unlink(self::PRIVATE_KEY); - } - public function testPrivateAndPublicKeysAreGenerated() { $this->assertFileExists(self::PUBLIC_KEY); diff --git a/tests/Feature/PassportTestCase.php b/tests/Feature/PassportTestCase.php index 610bfef37..3ccf02bdc 100644 --- a/tests/Feature/PassportTestCase.php +++ b/tests/Feature/PassportTestCase.php @@ -3,14 +3,15 @@ namespace Laravel\Passport\Tests\Feature; use Illuminate\Contracts\Config\Repository; -use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Foundation\Testing\LazilyRefreshDatabase; use Laravel\Passport\Passport; -use Laravel\Passport\PassportServiceProvider; +use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase; +use Workbench\App\Models\User; abstract class PassportTestCase extends TestCase { - use RefreshDatabase; + use LazilyRefreshDatabase, WithWorkbench; const KEYS = __DIR__.'/../keys'; const PUBLIC_KEY = self::KEYS.'/oauth-public.key'; @@ -18,54 +19,32 @@ abstract class PassportTestCase extends TestCase protected function setUp(): void { - parent::setUp(); - - $this->artisan('migrate:fresh'); + $this->afterApplicationCreated(function () { + Passport::loadKeysFrom(self::KEYS); - Passport::routes(); + @unlink(self::PUBLIC_KEY); + @unlink(self::PRIVATE_KEY); - Passport::loadKeysFrom(self::KEYS); + $this->artisan('passport:keys'); + }); - @unlink(self::PUBLIC_KEY); - @unlink(self::PRIVATE_KEY); + $this->beforeApplicationDestroyed(function () { + @unlink(self::PUBLIC_KEY); + @unlink(self::PRIVATE_KEY); + }); - $this->artisan('passport:keys'); + parent::setUp(); } - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { $config = $app->make(Repository::class); - $config->set('auth.defaults.provider', 'users'); - - if (($userClass = $this->getUserClass()) !== null) { - $config->set('auth.providers.users.model', $userClass); - } - - $config->set('auth.guards.api', ['driver' => 'passport', 'provider' => 'users']); - - $app['config']->set('database.default', 'testbench'); - - $app['config']->set('passport.storage.database.connection', 'testbench'); - - $app['config']->set('database.connections.testbench', [ - 'driver' => 'sqlite', - 'database' => ':memory:', - 'prefix' => '', + $config->set([ + 'auth.defaults.provider' => 'users', + 'auth.providers.users.model' => User::class, + 'auth.guards.api' => ['driver' => 'passport', 'provider' => 'users'], + 'database.default' => 'testing', ]); } - - protected function getPackageProviders($app) - { - return [PassportServiceProvider::class]; - } - - /** - * Get the Eloquent user model class name. - * - * @return string|null - */ - protected function getUserClass() - { - } } diff --git a/tests/Unit/AccessTokenControllerTest.php b/tests/Unit/AccessTokenControllerTest.php index 0e451fc97..bcd72cb55 100644 --- a/tests/Unit/AccessTokenControllerTest.php +++ b/tests/Unit/AccessTokenControllerTest.php @@ -5,7 +5,6 @@ use Laravel\Passport\Exceptions\OAuthServerException; use Laravel\Passport\Http\Controllers\AccessTokenController; use Laravel\Passport\TokenRepository; -use Lcobucci\JWT\Parser; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException as LeagueException; use Mockery as m; @@ -26,7 +25,6 @@ public function test_a_token_can_be_issued() $request = m::mock(ServerRequestInterface::class); $response = m::type(ResponseInterface::class); $tokens = m::mock(TokenRepository::class); - $jwt = m::mock(Parser::class); $psrResponse = new Response(); $psrResponse->getBody()->write(json_encode(['access_token' => 'access-token'])); @@ -36,7 +34,7 @@ public function test_a_token_can_be_issued() ->with($request, $response) ->andReturn($psrResponse); - $controller = new AccessTokenController($server, $tokens, $jwt); + $controller = new AccessTokenController($server, $tokens); $this->assertSame('{"access_token":"access-token"}', $controller->issueToken($request)->getContent()); } @@ -44,14 +42,13 @@ public function test_a_token_can_be_issued() public function test_exceptions_are_handled() { $tokens = m::mock(TokenRepository::class); - $jwt = m::mock(Parser::class); $server = m::mock(AuthorizationServer::class); $server->shouldReceive('respondToAccessTokenRequest')->with( m::type(ServerRequestInterface::class), m::type(ResponseInterface::class) )->andThrow(LeagueException::invalidCredentials()); - $controller = new AccessTokenController($server, $tokens, $jwt); + $controller = new AccessTokenController($server, $tokens); $this->expectException(OAuthServerException::class); diff --git a/tests/Unit/AuthorizationControllerTest.php b/tests/Unit/AuthorizationControllerTest.php index a8672cf10..fe6e7f3ea 100644 --- a/tests/Unit/AuthorizationControllerTest.php +++ b/tests/Unit/AuthorizationControllerTest.php @@ -2,13 +2,15 @@ namespace Laravel\Passport\Tests\Unit; -use Illuminate\Contracts\Routing\ResponseFactory; +use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\Request; use Laravel\Passport\Bridge\Scope; use Laravel\Passport\Client; use Laravel\Passport\ClientRepository; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Exceptions\OAuthServerException; use Laravel\Passport\Http\Controllers\AuthorizationController; +use Laravel\Passport\Http\Responses\AuthorizationViewResponse; use Laravel\Passport\Passport; use Laravel\Passport\Token; use Laravel\Passport\TokenRepository; @@ -35,38 +37,41 @@ public function test_authorization_view_is_presented() ]); $server = m::mock(AuthorizationServer::class); - $response = m::mock(ResponseFactory::class); + $response = m::mock(AuthorizationViewResponse::class); + $guard = m::mock(StatefulGuard::class); - $controller = new AuthorizationController($server, $response); + $controller = new AuthorizationController($server, $guard, $response); + $guard->shouldReceive('guest')->andReturn(false); + $guard->shouldReceive('user')->andReturn($user = m::mock()); $server->shouldReceive('validateAuthorizationRequest')->andReturn($authRequest = m::mock()); $request = m::mock(Request::class); $request->shouldReceive('session')->andReturn($session = m::mock()); $session->shouldReceive('put')->withSomeOfArgs('authToken'); $session->shouldReceive('put')->with('authRequest', $authRequest); - $request->shouldReceive('user')->andReturn($user = m::mock()); + $session->shouldReceive('forget')->with('promptedForLogin')->once(); + $request->shouldReceive('get')->with('prompt')->andReturn(null); $authRequest->shouldReceive('getClient->getIdentifier')->andReturn(1); $authRequest->shouldReceive('getScopes')->andReturn([new Scope('scope-1')]); $clients = m::mock(ClientRepository::class); $clients->shouldReceive('find')->with(1)->andReturn($client = m::mock(Client::class)); - $client->shouldReceive('skipsAuthorization')->andReturn(false); - $response->shouldReceive('view')->once()->andReturnUsing(function ($view, $data) use ($client, $user) { - $this->assertSame('passport::authorize', $view); + $tokens = m::mock(TokenRepository::class); + $tokens->shouldReceive('findValidToken')->with($user, $client)->andReturnNull(); + + $response->shouldReceive('withParameters')->once()->andReturnUsing(function ($data) use ($client, $user, $request) { $this->assertEquals($client, $data['client']); $this->assertEquals($user, $data['user']); + $this->assertEquals($request, $data['request']); $this->assertSame('description', $data['scopes'][0]->description); return 'view'; }); - $tokens = m::mock(TokenRepository::class); - $tokens->shouldReceive('findValidToken')->with($user, $client)->andReturnNull(); - $this->assertSame('view', $controller->authorize( m::mock(ServerRequestInterface::class), $request, $clients, $tokens )); @@ -75,10 +80,12 @@ public function test_authorization_view_is_presented() public function test_authorization_exceptions_are_handled() { $server = m::mock(AuthorizationServer::class); - $response = m::mock(ResponseFactory::class); + $response = m::mock(AuthorizationViewResponse::class); + $guard = m::mock(StatefulGuard::class); - $controller = new AuthorizationController($server, $response); + $controller = new AuthorizationController($server, $guard, $response); + $guard->shouldReceive('guest')->andReturn(false); $server->shouldReceive('validateAuthorizationRequest')->andThrow(LeagueException::invalidCredentials()); $request = m::mock(Request::class); @@ -101,9 +108,13 @@ public function test_request_is_approved_if_valid_token_exists() ]); $server = m::mock(AuthorizationServer::class); - $response = m::mock(ResponseFactory::class); + $response = m::mock(AuthorizationViewResponse::class); + $guard = m::mock(StatefulGuard::class); - $controller = new AuthorizationController($server, $response); + $controller = new AuthorizationController($server, $guard, $response); + + $guard->shouldReceive('guest')->andReturn(false); + $guard->shouldReceive('user')->andReturn($user = m::mock()); $psrResponse = new Response(); $psrResponse->getBody()->write('approved'); $server->shouldReceive('validateAuthorizationRequest') @@ -113,9 +124,11 @@ public function test_request_is_approved_if_valid_token_exists() ->andReturn($psrResponse); $request = m::mock(Request::class); - $request->shouldReceive('user')->once()->andReturn($user = m::mock()); + $request->shouldReceive('session')->andReturn($session = m::mock()); + $session->shouldReceive('forget')->with('promptedForLogin')->once(); $user->shouldReceive('getAuthIdentifier')->andReturn(1); $request->shouldNotReceive('session'); + $request->shouldReceive('get')->with('prompt')->andReturn(null); $authRequest->shouldReceive('getClient->getIdentifier')->once()->andReturn(1); $authRequest->shouldReceive('getScopes')->once()->andReturn([new Scope('scope-1')]); @@ -123,11 +136,13 @@ public function test_request_is_approved_if_valid_token_exists() $authRequest->shouldReceive('setAuthorizationApproved')->once()->with(true); $clients = m::mock(ClientRepository::class); - $clients->shouldReceive('find')->with(1)->andReturn('client'); + $clients->shouldReceive('find')->with(1)->andReturn($client = m::mock(Client::class)); + + $client->shouldReceive('skipsAuthorization')->andReturn(false); $tokens = m::mock(TokenRepository::class); $tokens->shouldReceive('findValidToken') - ->with($user, 'client') + ->with($user, $client) ->andReturn($token = m::mock(Token::class)); $token->shouldReceive('getAttribute')->with('scopes')->andReturn(['scope-1']); @@ -143,9 +158,13 @@ public function test_request_is_approved_if_client_can_skip_authorization() ]); $server = m::mock(AuthorizationServer::class); - $response = m::mock(ResponseFactory::class); + $response = m::mock(AuthorizationViewResponse::class); + $guard = m::mock(StatefulGuard::class); + + $controller = new AuthorizationController($server, $guard, $response); - $controller = new AuthorizationController($server, $response); + $guard->shouldReceive('guest')->andReturn(false); + $guard->shouldReceive('user')->andReturn($user = m::mock()); $psrResponse = new Response(); $psrResponse->getBody()->write('approved'); $server->shouldReceive('validateAuthorizationRequest') @@ -155,9 +174,11 @@ public function test_request_is_approved_if_client_can_skip_authorization() ->andReturn($psrResponse); $request = m::mock(Request::class); - $request->shouldReceive('user')->once()->andReturn($user = m::mock()); + $request->shouldReceive('session')->andReturn($session = m::mock()); + $session->shouldReceive('forget')->with('promptedForLogin')->once(); $user->shouldReceive('getAuthIdentifier')->andReturn(1); $request->shouldNotReceive('session'); + $request->shouldReceive('get')->with('prompt')->andReturn(null); $authRequest->shouldReceive('getClient->getIdentifier')->once()->andReturn(1); $authRequest->shouldReceive('getScopes')->once()->andReturn([new Scope('scope-1')]); @@ -178,4 +199,198 @@ public function test_request_is_approved_if_client_can_skip_authorization() m::mock(ServerRequestInterface::class), $request, $clients, $tokens )->getContent()); } + + public function test_authorization_view_is_presented_if_request_has_prompt_equals_to_consent() + { + Passport::tokensCan([ + 'scope-1' => 'description', + ]); + + $server = m::mock(AuthorizationServer::class); + $response = m::mock(AuthorizationViewResponse::class); + $guard = m::mock(StatefulGuard::class); + + $controller = new AuthorizationController($server, $guard, $response); + + $guard->shouldReceive('guest')->andReturn(false); + $guard->shouldReceive('user')->andReturn($user = m::mock()); + $server->shouldReceive('validateAuthorizationRequest') + ->andReturn($authRequest = m::mock(AuthorizationRequest::class)); + + $request = m::mock(Request::class); + $request->shouldReceive('session')->andReturn($session = m::mock()); + $session->shouldReceive('put')->withSomeOfArgs('authToken'); + $session->shouldReceive('put')->with('authRequest', $authRequest); + $session->shouldReceive('forget')->with('promptedForLogin')->once(); + $request->shouldReceive('get')->with('prompt')->andReturn('consent'); + + $authRequest->shouldReceive('getClient->getIdentifier')->once()->andReturn(1); + $authRequest->shouldReceive('getScopes')->once()->andReturn([new Scope('scope-1')]); + + $clients = m::mock(ClientRepository::class); + $clients->shouldReceive('find')->with(1)->andReturn($client = m::mock(Client::class)); + $client->shouldReceive('skipsAuthorization')->andReturn(false); + + $tokens = m::mock(TokenRepository::class); + $tokens->shouldNotReceive('findValidToken'); + + $response->shouldReceive('withParameters')->once()->andReturnUsing(function ($data) use ($client, $user, $request) { + $this->assertEquals($client, $data['client']); + $this->assertEquals($user, $data['user']); + $this->assertEquals($request, $data['request']); + $this->assertSame('description', $data['scopes'][0]->description); + + return 'view'; + }); + + $this->assertSame('view', $controller->authorize( + m::mock(ServerRequestInterface::class), $request, $clients, $tokens + )); + } + + public function test_authorization_denied_if_request_has_prompt_equals_to_none() + { + $this->expectException('Laravel\Passport\Exceptions\OAuthServerException'); + + Passport::tokensCan([ + 'scope-1' => 'description', + ]); + + $server = m::mock(AuthorizationServer::class); + $response = m::mock(AuthorizationViewResponse::class); + $guard = m::mock(StatefulGuard::class); + + $controller = new AuthorizationController($server, $guard, $response); + + $guard->shouldReceive('guest')->andReturn(false); + $guard->shouldReceive('user')->andReturn($user = m::mock()); + $server->shouldReceive('validateAuthorizationRequest') + ->andReturn($authRequest = m::mock(AuthorizationRequest::class)); + $server->shouldReceive('completeAuthorizationRequest') + ->with($authRequest, m::type(ResponseInterface::class)) + ->once() + ->andThrow('League\OAuth2\Server\Exception\OAuthServerException'); + + $request = m::mock(Request::class); + $request->shouldReceive('session')->andReturn($session = m::mock()); + $session->shouldReceive('forget')->with('promptedForLogin')->once(); + $user->shouldReceive('getAuthIdentifier')->andReturn(1); + $request->shouldReceive('get')->with('prompt')->andReturn('none'); + + $authRequest->shouldReceive('getClient->getIdentifier')->once()->andReturn(1); + $authRequest->shouldReceive('getScopes')->once()->andReturn([new Scope('scope-1')]); + $authRequest->shouldReceive('setUser')->once()->andReturnNull(); + $authRequest->shouldReceive('setAuthorizationApproved')->once()->with(false); + + $clients = m::mock(ClientRepository::class); + $clients->shouldReceive('find')->with(1)->andReturn($client = m::mock(Client::class)); + $client->shouldReceive('skipsAuthorization')->andReturn(false); + + $tokens = m::mock(TokenRepository::class); + $tokens->shouldReceive('findValidToken') + ->with($user, $client) + ->andReturnNull(); + + $controller->authorize( + m::mock(ServerRequestInterface::class), $request, $clients, $tokens + ); + } + + public function test_authorization_denied_if_unauthenticated_and_request_has_prompt_equals_to_none() + { + $server = m::mock(AuthorizationServer::class); + $response = m::mock(AuthorizationViewResponse::class); + $guard = m::mock(StatefulGuard::class); + + $controller = new AuthorizationController($server, $guard, $response); + + $guard->shouldReceive('guest')->andReturn(true); + $server->shouldReceive('validateAuthorizationRequest') + ->andReturn($authRequest = m::mock(AuthorizationRequest::class)); + $server->shouldNotReceive('completeAuthorizationRequest'); + + $request = m::mock(Request::class); + $request->shouldNotReceive('user'); + $request->shouldReceive('get')->with('prompt')->andReturn('none'); + + $authRequest->shouldNotReceive('setUser'); + $authRequest->shouldReceive('setAuthorizationApproved')->with(false); + $authRequest->shouldReceive('getRedirectUri')->andReturn('http://localhost'); + $authRequest->shouldReceive('getClient->getRedirectUri')->andReturn('http://localhost'); + $authRequest->shouldReceive('getState')->andReturn('state'); + $authRequest->shouldReceive('getGrantTypeId')->andReturn('authorization_code'); + + $clients = m::mock(ClientRepository::class); + $tokens = m::mock(TokenRepository::class); + + try { + $controller->authorize( + m::mock(ServerRequestInterface::class), $request, $clients, $tokens + ); + } catch (\Laravel\Passport\Exceptions\OAuthServerException $e) { + $this->assertStringStartsWith( + 'http://localhost?state=state&error=access_denied&error_description=', + $e->render($request)->headers->get('location') + ); + } + } + + public function test_logout_and_prompt_login_if_request_has_prompt_equals_to_login() + { + $this->expectException(AuthenticationException::class); + + $server = m::mock(AuthorizationServer::class); + $response = m::mock(AuthorizationViewResponse::class); + $guard = m::mock(StatefulGuard::class); + + $controller = new AuthorizationController($server, $guard, $response); + + $guard->shouldReceive('guest')->andReturn(false); + $server->shouldReceive('validateAuthorizationRequest')->once(); + $guard->shouldReceive('logout')->once(); + + $request = m::mock(Request::class); + $request->shouldReceive('session')->andReturn($session = m::mock()); + $session->shouldReceive('invalidate')->once(); + $session->shouldReceive('regenerateToken')->once(); + $session->shouldReceive('get')->with('promptedForLogin', false)->once()->andReturn(false); + $session->shouldReceive('put')->with('promptedForLogin', true)->once(); + $session->shouldNotReceive('forget')->with('promptedForLogin'); + $request->shouldReceive('get')->with('prompt')->andReturn('login'); + + $clients = m::mock(ClientRepository::class); + $tokens = m::mock(TokenRepository::class); + + $controller->authorize( + m::mock(ServerRequestInterface::class), $request, $clients, $tokens + ); + } + + public function test_user_should_be_authenticated() + { + $this->expectException(AuthenticationException::class); + + $server = m::mock(AuthorizationServer::class); + $response = m::mock(AuthorizationViewResponse::class); + $guard = m::mock(StatefulGuard::class); + + $controller = new AuthorizationController($server, $guard, $response); + + $guard->shouldReceive('guest')->andReturn(true); + $server->shouldReceive('validateAuthorizationRequest')->once(); + + $request = m::mock(Request::class); + $request->shouldNotReceive('user'); + $request->shouldReceive('session')->andReturn($session = m::mock()); + $session->shouldReceive('put')->with('promptedForLogin', true)->once(); + $session->shouldNotReceive('forget')->with('promptedForLogin'); + $request->shouldReceive('get')->with('prompt')->andReturn(null); + + $clients = m::mock(ClientRepository::class); + $tokens = m::mock(TokenRepository::class); + + $controller->authorize( + m::mock(ServerRequestInterface::class), $request, $clients, $tokens + ); + } } diff --git a/tests/Unit/BridgeAccessTokenRepositoryTest.php b/tests/Unit/BridgeAccessTokenRepositoryTest.php index 0d34d9b45..aa99cd051 100644 --- a/tests/Unit/BridgeAccessTokenRepositoryTest.php +++ b/tests/Unit/BridgeAccessTokenRepositoryTest.php @@ -3,6 +3,7 @@ namespace Laravel\Passport\Tests\Unit; use Carbon\CarbonImmutable; +use DateTime; use Illuminate\Contracts\Events\Dispatcher; use Laravel\Passport\Bridge\AccessToken; use Laravel\Passport\Bridge\AccessTokenRepository; @@ -32,8 +33,8 @@ public function test_access_tokens_can_be_persisted() $this->assertSame('client-id', $array['client_id']); $this->assertEquals(['scopes'], $array['scopes']); $this->assertEquals(false, $array['revoked']); - $this->assertInstanceOf('DateTime', $array['created_at']); - $this->assertInstanceOf('DateTime', $array['updated_at']); + $this->assertInstanceOf(DateTime::class, $array['created_at']); + $this->assertInstanceOf(DateTime::class, $array['updated_at']); $this->assertEquals($expiration, $array['expires_at']); }); diff --git a/tests/Unit/BridgeScopeRepositoryTest.php b/tests/Unit/BridgeScopeRepositoryTest.php index 4dc370aca..e663bf89b 100644 --- a/tests/Unit/BridgeScopeRepositoryTest.php +++ b/tests/Unit/BridgeScopeRepositoryTest.php @@ -5,18 +5,90 @@ use Laravel\Passport\Bridge\Client; use Laravel\Passport\Bridge\Scope; use Laravel\Passport\Bridge\ScopeRepository; +use Laravel\Passport\Client as ClientModel; +use Laravel\Passport\ClientRepository; use Laravel\Passport\Passport; +use Mockery; use PHPUnit\Framework\TestCase; class BridgeScopeRepositoryTest extends TestCase { + protected function tearDown(): void + { + Passport::$withInheritedScopes = false; + } + public function test_invalid_scopes_are_removed() { Passport::tokensCan([ 'scope-1' => 'description', ]); - $repository = new ScopeRepository; + $client = Mockery::mock(ClientModel::class)->makePartial(); + + $clients = Mockery::mock(ClientRepository::class); + $clients->shouldReceive('findActive')->withAnyArgs()->andReturn($client); + + $repository = new ScopeRepository($clients); + + $scopes = $repository->finalizeScopes( + [$scope1 = new Scope('scope-1'), new Scope('scope-2')], 'client_credentials', new Client('id', 'name', 'http://localhost'), 1 + ); + + $this->assertEquals([$scope1], $scopes); + } + + public function test_invalid_scopes_are_removed_without_a_client_repository() + { + Passport::tokensCan([ + 'scope-1' => 'description', + ]); + + $repository = new ScopeRepository(); + + $scopes = $repository->finalizeScopes( + [$scope1 = new Scope('scope-1'), new Scope('scope-2')], 'client_credentials', new Client('id', 'name', 'http://localhost'), 1 + ); + + $this->assertEquals([$scope1], $scopes); + } + + public function test_clients_do_not_restrict_scopes_by_default() + { + Passport::tokensCan([ + 'scope-1' => 'description', + 'scope-2' => 'description', + ]); + + $client = Mockery::mock(ClientModel::class)->makePartial(); + $client->scopes = null; + + $clients = Mockery::mock(ClientRepository::class); + $clients->shouldReceive('findActive')->withAnyArgs()->andReturn($client); + + $repository = new ScopeRepository($clients); + + $scopes = $repository->finalizeScopes( + [$scope1 = new Scope('scope-1'), $scope2 = new Scope('scope-2')], 'client_credentials', new Client('id', 'name', 'http://localhost'), 1 + ); + + $this->assertEquals([$scope1, $scope2], $scopes); + } + + public function test_scopes_disallowed_for_client_are_removed() + { + Passport::tokensCan([ + 'scope-1' => 'description', + 'scope-2' => 'description', + ]); + + $client = Mockery::mock(ClientModel::class)->makePartial(); + $client->scopes = ['scope-1']; + + $clients = Mockery::mock(ClientRepository::class); + $clients->shouldReceive('findActive')->withAnyArgs()->andReturn($client); + + $repository = new ScopeRepository($clients); $scopes = $repository->finalizeScopes( [$scope1 = new Scope('scope-1'), new Scope('scope-2')], 'client_credentials', new Client('id', 'name', 'http://localhost'), 1 @@ -25,13 +97,58 @@ public function test_invalid_scopes_are_removed() $this->assertEquals([$scope1], $scopes); } + public function test_scopes_disallowed_for_client_are_removed_but_inherited_scopes_are_not() + { + Passport::$withInheritedScopes = true; + + Passport::tokensCan([ + 'scope-1' => 'description', + 'scope-1:limited-access' => 'description', + 'scope-2' => 'description', + ]); + + $client = Mockery::mock(ClientModel::class)->makePartial(); + $client->scopes = ['scope-1']; + + $clients = Mockery::mock(ClientRepository::class); + $clients->shouldReceive('findActive')->withAnyArgs()->andReturn($client); + + $repository = new ScopeRepository($clients); + + $scopes = $repository->finalizeScopes( + [$scope1 = new Scope('scope-1:limited-access'), new Scope('scope-2')], 'client_credentials', new Client('id', 'name', 'http://localhost'), 1 + ); + + $this->assertEquals([$scope1], $scopes); + } + public function test_superuser_scope_cant_be_applied_if_wrong_grant() { Passport::tokensCan([ 'scope-1' => 'description', ]); - $repository = new ScopeRepository; + $client = Mockery::mock(ClientModel::class)->makePartial(); + + $clients = Mockery::mock(ClientRepository::class); + $clients->shouldReceive('findActive')->withAnyArgs()->andReturn($client); + + $repository = new ScopeRepository($clients); + + $scopes = $repository->finalizeScopes( + [$scope1 = new Scope('*')], 'refresh_token', new Client('id', 'name', 'http://localhost'), 1 + ); + + $this->assertEquals([], $scopes); + } + + public function test_superuser_scope_cant_be_applied_if_wrong_grant_without_a_client_repository() + { + Passport::tokensCan([ + 'scope-1' => 'description', + ]); + + $repository = new ScopeRepository(); $scopes = $repository->finalizeScopes( [$scope1 = new Scope('*')], 'refresh_token', new Client('id', 'name', 'http://localhost'), 1 diff --git a/tests/Unit/CheckClientCredentialsForAnyScopeTest.php b/tests/Unit/CheckClientCredentialsForAnyScopeTest.php index 754d9a7de..807bf93d4 100644 --- a/tests/Unit/CheckClientCredentialsForAnyScopeTest.php +++ b/tests/Unit/CheckClientCredentialsForAnyScopeTest.php @@ -4,6 +4,7 @@ use Illuminate\Http\Request; use Laravel\Passport\Client; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Http\Middleware\CheckClientCredentialsForAnyScope; use Laravel\Passport\Token; use Laravel\Passport\TokenRepository; @@ -85,7 +86,7 @@ public function test_request_is_passed_along_if_token_has_any_required_scope() public function test_exception_is_thrown_when_oauth_throws_exception() { - $this->expectException('Illuminate\Auth\AuthenticationException'); + $this->expectException(AuthenticationException::class); $tokenRepository = m::mock(TokenRepository::class); $resourceServer = m::mock(ResourceServer::class); diff --git a/tests/Unit/CheckClientCredentialsTest.php b/tests/Unit/CheckClientCredentialsTest.php index 36e410dd5..0a30b19ea 100644 --- a/tests/Unit/CheckClientCredentialsTest.php +++ b/tests/Unit/CheckClientCredentialsTest.php @@ -4,6 +4,7 @@ use Illuminate\Http\Request; use Laravel\Passport\Client; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Http\Middleware\CheckClientCredentials; use Laravel\Passport\Token; use Laravel\Passport\TokenRepository; @@ -84,7 +85,7 @@ public function test_request_is_passed_along_if_token_and_scope_are_valid() public function test_exception_is_thrown_when_oauth_throws_exception() { - $this->expectException('Illuminate\Auth\AuthenticationException'); + $this->expectException(AuthenticationException::class); $tokenRepository = m::mock(TokenRepository::class); $resourceServer = m::mock(ResourceServer::class); diff --git a/tests/Unit/CheckForAnyScopeTest.php b/tests/Unit/CheckForAnyScopeTest.php index cf9e04c5b..e567feeb0 100644 --- a/tests/Unit/CheckForAnyScopeTest.php +++ b/tests/Unit/CheckForAnyScopeTest.php @@ -2,6 +2,7 @@ namespace Laravel\Passport\Tests\Unit; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Http\Middleware\CheckForAnyScope as CheckScopes; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -47,7 +48,7 @@ public function test_exception_is_thrown_if_token_doesnt_have_scope() public function test_exception_is_thrown_if_no_authenticated_user() { - $this->expectException('Illuminate\Auth\AuthenticationException'); + $this->expectException(AuthenticationException::class); $middleware = new CheckScopes; $request = m::mock(); @@ -60,7 +61,7 @@ public function test_exception_is_thrown_if_no_authenticated_user() public function test_exception_is_thrown_if_no_token() { - $this->expectException('Illuminate\Auth\AuthenticationException'); + $this->expectException(AuthenticationException::class); $middleware = new CheckScopes; $request = m::mock(); diff --git a/tests/Unit/CheckScopesTest.php b/tests/Unit/CheckScopesTest.php index 3eb172b9b..21e8b40bf 100644 --- a/tests/Unit/CheckScopesTest.php +++ b/tests/Unit/CheckScopesTest.php @@ -2,6 +2,7 @@ namespace Laravel\Passport\Tests\Unit; +use Laravel\Passport\Exceptions\AuthenticationException; use Laravel\Passport\Http\Middleware\CheckScopes; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -46,7 +47,7 @@ public function test_exception_is_thrown_if_token_doesnt_have_scope() public function test_exception_is_thrown_if_no_authenticated_user() { - $this->expectException('Illuminate\Auth\AuthenticationException'); + $this->expectException(AuthenticationException::class); $middleware = new CheckScopes; $request = m::mock(); @@ -59,7 +60,7 @@ public function test_exception_is_thrown_if_no_authenticated_user() public function test_exception_is_thrown_if_no_token() { - $this->expectException('Illuminate\Auth\AuthenticationException'); + $this->expectException(AuthenticationException::class); $middleware = new CheckScopes; $request = m::mock(); diff --git a/tests/Unit/DenyAuthorizationControllerTest.php b/tests/Unit/DenyAuthorizationControllerTest.php index cfb5e01cc..e2cd26f69 100644 --- a/tests/Unit/DenyAuthorizationControllerTest.php +++ b/tests/Unit/DenyAuthorizationControllerTest.php @@ -2,12 +2,13 @@ namespace Laravel\Passport\Tests\Unit; -use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Http\Request; use Laravel\Passport\Http\Controllers\DenyAuthorizationController; +use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use Mockery as m; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; class DenyAuthorizationControllerTest extends TestCase { @@ -18,130 +19,34 @@ protected function tearDown(): void public function test_authorization_can_be_denied() { - $response = m::mock(ResponseFactory::class); + $this->expectException('Laravel\Passport\Exceptions\OAuthServerException'); - $controller = new DenyAuthorizationController($response); + $server = m::mock(AuthorizationServer::class); + $controller = new DenyAuthorizationController($server); $request = m::mock(Request::class); $request->shouldReceive('session')->andReturn($session = m::mock()); $request->shouldReceive('user')->andReturn(new DenyAuthorizationControllerFakeUser); - $request->shouldReceive('input')->with('state')->andReturn('state'); $request->shouldReceive('has')->with('auth_token')->andReturn(true); $request->shouldReceive('get')->with('auth_token')->andReturn('foo'); $session->shouldReceive('get')->once()->with('authToken')->andReturn('foo'); - $session->shouldReceive('get')->once()->with('authRequest')->andReturn($authRequest = m::mock( - AuthorizationRequest::class - )); + $session->shouldReceive('get') + ->once() + ->with('authRequest') + ->andReturn($authRequest = m::mock( + AuthorizationRequest::class + )); $authRequest->shouldReceive('setUser')->once(); - $authRequest->shouldReceive('getGrantTypeId')->andReturn('authorization_code'); - $authRequest->shouldReceive('setAuthorizationApproved')->once()->with(true); - $authRequest->shouldReceive('getRedirectUri')->andReturn('http://localhost'); - $authRequest->shouldReceive('getClient->getRedirectUri')->andReturn('http://localhost'); + $authRequest->shouldReceive('setAuthorizationApproved')->once()->with(false); - $response->shouldReceive('redirectTo')->once()->andReturnUsing(function ($url) { - return $url; - }); + $server->shouldReceive('completeAuthorizationRequest') + ->with($authRequest, m::type(ResponseInterface::class)) + ->andThrow('League\OAuth2\Server\Exception\OAuthServerException'); - $this->assertSame('http://localhost?error=access_denied&state=state', $controller->deny($request)); - } - - public function test_authorization_can_be_denied_with_multiple_redirect_uris() - { - $response = m::mock(ResponseFactory::class); - - $controller = new DenyAuthorizationController($response); - - $request = m::mock(Request::class); - - $request->shouldReceive('session')->andReturn($session = m::mock()); - $request->shouldReceive('user')->andReturn(new DenyAuthorizationControllerFakeUser); - $request->shouldReceive('input')->with('state')->andReturn('state'); - $request->shouldReceive('has')->with('auth_token')->andReturn(true); - $request->shouldReceive('get')->with('auth_token')->andReturn('foo'); - - $session->shouldReceive('get')->once()->with('authRequest')->andReturn($authRequest = m::mock( - AuthorizationRequest::class - )); - - $authRequest->shouldReceive('setUser')->once(); - $authRequest->shouldReceive('getGrantTypeId')->andReturn('authorization_code'); - $authRequest->shouldReceive('setAuthorizationApproved')->once()->with(true); - $authRequest->shouldReceive('getRedirectUri')->andReturn('http://localhost'); - $authRequest->shouldReceive('getClient->getRedirectUri')->andReturn(['http://localhost.localdomain', 'http://localhost']); - - $session->shouldReceive('get')->once()->with('authToken')->andReturn('foo'); - $response->shouldReceive('redirectTo')->once()->andReturnUsing(function ($url) { - return $url; - }); - - $this->assertSame('http://localhost?error=access_denied&state=state', $controller->deny($request)); - } - - public function test_authorization_can_be_denied_implicit() - { - $response = m::mock(ResponseFactory::class); - - $controller = new DenyAuthorizationController($response); - - $request = m::mock(Request::class); - - $request->shouldReceive('session')->andReturn($session = m::mock()); - $request->shouldReceive('user')->andReturn(new DenyAuthorizationControllerFakeUser); - $request->shouldReceive('input')->with('state')->andReturn('state'); - $request->shouldReceive('has')->with('auth_token')->andReturn(true); - $request->shouldReceive('get')->with('auth_token')->andReturn('foo'); - - $session->shouldReceive('get')->once()->with('authToken')->andReturn('foo'); - $session->shouldReceive('get')->once()->with('authRequest')->andReturn($authRequest = m::mock( - AuthorizationRequest::class - )); - - $authRequest->shouldReceive('setUser')->once(); - $authRequest->shouldReceive('getGrantTypeId')->andReturn('implicit'); - $authRequest->shouldReceive('setAuthorizationApproved')->once()->with(true); - $authRequest->shouldReceive('getRedirectUri')->andReturn('http://localhost'); - $authRequest->shouldReceive('getClient->getRedirectUri')->andReturn('http://localhost'); - - $response->shouldReceive('redirectTo')->once()->andReturnUsing(function ($url) { - return $url; - }); - - $this->assertSame('http://localhost#error=access_denied&state=state', $controller->deny($request)); - } - - public function test_authorization_can_be_denied_with_existing_query_string() - { - $response = m::mock(ResponseFactory::class); - - $controller = new DenyAuthorizationController($response); - - $request = m::mock(Request::class); - - $request->shouldReceive('session')->andReturn($session = m::mock()); - $request->shouldReceive('user')->andReturn(new DenyAuthorizationControllerFakeUser); - $request->shouldReceive('input')->with('state')->andReturn('state'); - $request->shouldReceive('has')->with('auth_token')->andReturn(true); - $request->shouldReceive('get')->with('auth_token')->andReturn('foo'); - - $session->shouldReceive('get')->once()->with('authToken')->andReturn('foo'); - $session->shouldReceive('get')->once()->with('authRequest')->andReturn($authRequest = m::mock( - AuthorizationRequest::class - )); - - $authRequest->shouldReceive('setUser')->once(); - $authRequest->shouldReceive('getGrantTypeId')->andReturn('authorization_code'); - $authRequest->shouldReceive('setAuthorizationApproved')->once()->with(true); - $authRequest->shouldReceive('getRedirectUri')->andReturn('http://localhost?action=some_action'); - $authRequest->shouldReceive('getClient->getRedirectUri')->andReturn('http://localhost?action=some_action'); - - $response->shouldReceive('redirectTo')->once()->andReturnUsing(function ($url) { - return $url; - }); - - $this->assertSame('http://localhost?action=some_action&error=access_denied&state=state', $controller->deny($request)); + $controller->deny($request); } public function test_auth_request_should_exist() @@ -149,9 +54,9 @@ public function test_auth_request_should_exist() $this->expectException('Exception'); $this->expectExceptionMessage('Authorization request was not present in the session.'); - $response = m::mock(ResponseFactory::class); + $server = m::mock(AuthorizationServer::class); - $controller = new DenyAuthorizationController($response); + $controller = new DenyAuthorizationController($server); $request = m::mock(Request::class); @@ -164,7 +69,7 @@ public function test_auth_request_should_exist() $session->shouldReceive('get')->once()->with('authToken')->andReturn('foo'); $session->shouldReceive('get')->once()->with('authRequest')->andReturnNull(); - $response->shouldReceive('redirectTo')->never(); + $server->shouldReceive('completeAuthorizationRequest')->never(); $controller->deny($request); } diff --git a/tests/Unit/PersonalAccessTokenFactoryTest.php b/tests/Unit/PersonalAccessTokenFactoryTest.php index f4731bd6b..c35127104 100644 --- a/tests/Unit/PersonalAccessTokenFactoryTest.php +++ b/tests/Unit/PersonalAccessTokenFactoryTest.php @@ -2,6 +2,7 @@ namespace Laravel\Passport\Tests\Unit; +use Laravel\Passport\Client; use Laravel\Passport\ClientRepository; use Laravel\Passport\PersonalAccessTokenFactory; use Laravel\Passport\PersonalAccessTokenResult; @@ -41,7 +42,7 @@ public function test_access_token_can_be_created() $parsedToken = new PlainToken( new DataSet([], ''), new DataSet([RegisteredClaims::ID => 'token'], ''), - Signature::fromEmptyData() + new Signature('', '') ); $jwt->shouldReceive('parse')->with('foo')->andReturn($parsedToken); @@ -56,7 +57,7 @@ public function test_access_token_can_be_created() } } -class PersonalAccessTokenFactoryTestClientStub +class PersonalAccessTokenFactoryTestClientStub extends Client { public $id = 1; diff --git a/tests/Unit/TokenGuardTest.php b/tests/Unit/TokenGuardTest.php index 83608d861..614393898 100644 --- a/tests/Unit/TokenGuardTest.php +++ b/tests/Unit/TokenGuardTest.php @@ -37,11 +37,40 @@ public function test_user_can_be_pulled_via_bearer_token() $clients = m::mock(ClientRepository::class); $encrypter = m::mock(Encrypter::class); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); + $request = Request::create('/'); + $request->headers->set('Authorization', 'Bearer token'); + + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + + $resourceServer->shouldReceive('validateAuthenticatedRequest')->andReturn($psr = m::mock()); + $psr->shouldReceive('getAttribute')->with('oauth_user_id')->andReturn(1); + $psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1); + $psr->shouldReceive('getAttribute')->with('oauth_access_token_id')->andReturn('token'); + $userProvider->shouldReceive('retrieveById')->with(1)->andReturn(new TokenGuardTestUser); + $userProvider->shouldReceive('getProviderName')->andReturn(null); + $tokens->shouldReceive('find')->once()->with('token')->andReturn($token = m::mock()); + $clients->shouldReceive('revoked')->with(1)->andReturn(false); + $clients->shouldReceive('findActive')->with(1)->andReturn(new TokenGuardTestClient); + + $user = $guard->user(); + + $this->assertInstanceOf(TokenGuardTestUser::class, $user); + $this->assertEquals($token, $user->token()); + } + + public function test_user_is_resolved_only_once() + { + $resourceServer = m::mock(ResourceServer::class); + $userProvider = m::mock(PassportUserProvider::class); + $tokens = m::mock(TokenRepository::class); + $clients = m::mock(ClientRepository::class); + $encrypter = m::mock(Encrypter::class); $request = Request::create('/'); $request->headers->set('Authorization', 'Bearer token'); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $resourceServer->shouldReceive('validateAuthenticatedRequest')->andReturn($psr = m::mock()); $psr->shouldReceive('getAttribute')->with('oauth_user_id')->andReturn(1); $psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1); @@ -52,10 +81,15 @@ public function test_user_can_be_pulled_via_bearer_token() $clients->shouldReceive('revoked')->with(1)->andReturn(false); $clients->shouldReceive('findActive')->with(1)->andReturn(new TokenGuardTestClient); - $user = $guard->user($request); + $user = $guard->user(); + + $userProvider->shouldReceive('retrieveById')->never(); + + $user2 = $guard->user(); $this->assertInstanceOf(TokenGuardTestUser::class, $user); $this->assertEquals($token, $user->token()); + $this->assertSame($user, $user2); } public function test_no_user_is_returned_when_oauth_throws_exception() @@ -71,19 +105,19 @@ public function test_no_user_is_returned_when_oauth_throws_exception() $clients = m::mock(ClientRepository::class); $encrypter = m::mock(Encrypter::class); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - $request = Request::create('/'); $request->headers->set('Authorization', 'Bearer token'); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $resourceServer->shouldReceive('validateAuthenticatedRequest')->andThrow( new OAuthServerException('message', 500, 'error type') ); - $this->assertNull($guard->user($request)); + $this->assertNull($guard->user()); // Assert that `validateAuthenticatedRequest` isn't called twice on failure. - $this->assertNull($guard->user($request)); + $this->assertNull($guard->user()); } public function test_null_is_returned_if_no_user_is_found() @@ -98,18 +132,18 @@ public function test_null_is_returned_if_no_user_is_found() ->with(1) ->andReturn(new TokenGuardTestClient); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - $request = Request::create('/'); $request->headers->set('Authorization', 'Bearer token'); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $resourceServer->shouldReceive('validateAuthenticatedRequest')->andReturn($psr = m::mock()); $psr->shouldReceive('getAttribute')->with('oauth_user_id')->andReturn(1); $psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1); $userProvider->shouldReceive('retrieveById')->with(1)->andReturn(null); $userProvider->shouldReceive('getProviderName')->andReturn(null); - $this->assertNull($guard->user($request)); + $this->assertNull($guard->user()); } public function test_users_may_be_retrieved_from_cookies_with_csrf_token_header() @@ -124,8 +158,6 @@ public function test_users_may_be_retrieved_from_cookies_with_csrf_token_header( ->with(1) ->andReturn(new TokenGuardTestClient); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - $request = Request::create('/'); $request->headers->set('X-CSRF-TOKEN', 'token'); $request->cookies->set('laravel_token', @@ -137,10 +169,12 @@ public function test_users_may_be_retrieved_from_cookies_with_csrf_token_header( ], str_repeat('a', 16), 'HS256'), false) ); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $userProvider->shouldReceive('retrieveById')->with(1)->andReturn($expectedUser = new TokenGuardTestUser); $userProvider->shouldReceive('getProviderName')->andReturn(null); - $user = $guard->user($request); + $user = $guard->user(); $this->assertEquals($expectedUser, $user); } @@ -157,8 +191,6 @@ public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header( ->with(1) ->andReturn(new TokenGuardTestClient); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - $request = Request::create('/'); $request->headers->set('X-XSRF-TOKEN', $encrypter->encrypt(CookieValuePrefix::create('X-XSRF-TOKEN', $encrypter->getKey()).'token', false)); $request->cookies->set('laravel_token', @@ -170,10 +202,12 @@ public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header( ], str_repeat('a', 16), 'HS256'), false) ); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $userProvider->shouldReceive('retrieveById')->with(1)->andReturn($expectedUser = new TokenGuardTestUser); $userProvider->shouldReceive('getProviderName')->andReturn(null); - $user = $guard->user($request); + $user = $guard->user(); $this->assertEquals($expectedUser, $user); } @@ -186,8 +220,6 @@ public function test_cookie_xsrf_is_verified_against_csrf_token_header() $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - $request = Request::create('/'); $request->headers->set('X-CSRF-TOKEN', 'wrong_token'); $request->cookies->set('laravel_token', @@ -199,9 +231,11 @@ public function test_cookie_xsrf_is_verified_against_csrf_token_header() ], str_repeat('a', 16), 'HS256')) ); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $userProvider->shouldReceive('retrieveById')->never(); - $this->assertNull($guard->user($request)); + $this->assertNull($guard->user()); } public function test_cookie_xsrf_is_verified_against_xsrf_token_header() @@ -212,8 +246,6 @@ public function test_cookie_xsrf_is_verified_against_xsrf_token_header() $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - $request = Request::create('/'); $request->headers->set('X-XSRF-TOKEN', $encrypter->encrypt('wrong_token', false)); $request->cookies->set('laravel_token', @@ -225,9 +257,11 @@ public function test_cookie_xsrf_is_verified_against_xsrf_token_header() ], str_repeat('a', 16), 'HS256')) ); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $userProvider->shouldReceive('retrieveById')->never(); - $this->assertNull($guard->user($request)); + $this->assertNull($guard->user()); } public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header_when_using_a_custom_encryption_key() @@ -246,8 +280,6 @@ public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header_ ->with(1) ->andReturn(new TokenGuardTestClient); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - $request = Request::create('/'); $request->headers->set('X-XSRF-TOKEN', $encrypter->encrypt(CookieValuePrefix::create('X-XSRF-TOKEN', $encrypter->getKey()).'token', false)); $request->cookies->set('laravel_token', @@ -259,10 +291,12 @@ public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header_ ], Passport::tokenEncryptionKey($encrypter), 'HS256'), false) ); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $userProvider->shouldReceive('retrieveById')->with(1)->andReturn($expectedUser = new TokenGuardTestUser); $userProvider->shouldReceive('getProviderName')->andReturn(null); - $user = $guard->user($request); + $user = $guard->user(); $this->assertEquals($expectedUser, $user); @@ -270,15 +304,55 @@ public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header_ Passport::encryptTokensUsing(null); } - public function test_xsrf_token_cookie_without_a_token_header_is_not_accepted() + public function test_users_may_be_retrieved_from_cookies_without_encryption() { + Passport::withoutCookieEncryption(); + Passport::encryptTokensUsing(function (EncrypterContract $encrypter) { + return $encrypter->getKey().'.mykey'; + }); + $resourceServer = m::mock(ResourceServer::class); $userProvider = m::mock(PassportUserProvider::class); $tokens = m::mock(TokenRepository::class); $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); + $clients->shouldReceive('findActive') + ->with(1) + ->andReturn(new TokenGuardTestClient); + + $request = Request::create('/'); + $request->headers->set('X-XSRF-TOKEN', $encrypter->encrypt(CookieValuePrefix::create('X-XSRF-TOKEN', $encrypter->getKey()).'token', false)); + $request->cookies->set('laravel_token', + JWT::encode([ + 'sub' => 1, + 'aud' => 1, + 'csrf' => 'token', + 'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(), + ], Passport::tokenEncryptionKey($encrypter), 'HS256') + ); + + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + + $userProvider->shouldReceive('retrieveById')->with(1)->andReturn($expectedUser = new TokenGuardTestUser); + $userProvider->shouldReceive('getProviderName')->andReturn(null); + + $user = $guard->user(); + + $this->assertEquals($expectedUser, $user); + + // Revert to the default encryption method + Passport::withCookieEncryption(); + Passport::encryptTokensUsing(null); + } + + public function test_xsrf_token_cookie_without_a_token_header_is_not_accepted() + { + $resourceServer = m::mock(ResourceServer::class); + $userProvider = m::mock(PassportUserProvider::class); + $tokens = m::mock(TokenRepository::class); + $clients = m::mock(ClientRepository::class); + $encrypter = new Encrypter(str_repeat('a', 16)); $request = Request::create('/'); $request->cookies->set('XSRF-TOKEN', $encrypter->encrypt('token', false)); @@ -291,9 +365,11 @@ public function test_xsrf_token_cookie_without_a_token_header_is_not_accepted() ], str_repeat('a', 16), 'HS256')) ); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $userProvider->shouldReceive('retrieveById')->never(); - $this->assertNull($guard->user($request)); + $this->assertNull($guard->user()); } public function test_expired_cookies_may_not_be_used() @@ -304,8 +380,6 @@ public function test_expired_cookies_may_not_be_used() $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - $request = Request::create('/'); $request->headers->set('X-CSRF-TOKEN', 'token'); $request->cookies->set('laravel_token', @@ -317,9 +391,11 @@ public function test_expired_cookies_may_not_be_used() ], str_repeat('a', 16), 'HS256')) ); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $userProvider->shouldReceive('retrieveById')->never(); - $this->assertNull($guard->user($request)); + $this->assertNull($guard->user()); } public function test_csrf_check_can_be_disabled() @@ -334,8 +410,6 @@ public function test_csrf_check_can_be_disabled() ->with(1) ->andReturn(new TokenGuardTestClient); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - Passport::ignoreCsrfToken(); $request = Request::create('/'); @@ -347,10 +421,12 @@ public function test_csrf_check_can_be_disabled() ], str_repeat('a', 16), 'HS256'), false) ); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $userProvider->shouldReceive('retrieveById')->with(1)->andReturn($expectedUser = new TokenGuardTestUser); $userProvider->shouldReceive('getProviderName')->andReturn(null); - $user = $guard->user($request); + $user = $guard->user(); $this->assertEquals($expectedUser, $user); } @@ -363,18 +439,45 @@ public function test_client_can_be_pulled_via_bearer_token() $clients = m::mock(ClientRepository::class); $encrypter = m::mock(Encrypter::class); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); + $request = Request::create('/'); + $request->headers->set('Authorization', 'Bearer token'); + + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + + $resourceServer->shouldReceive('validateAuthenticatedRequest')->andReturn($psr = m::mock()); + $psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1); + $clients->shouldReceive('findActive')->with(1)->andReturn(new TokenGuardTestClient); + + $client = $guard->client(); + + $this->assertInstanceOf(TokenGuardTestClient::class, $client); + } + + public function test_client_is_resolved_only_once() + { + $resourceServer = m::mock(ResourceServer::class); + $userProvider = m::mock(PassportUserProvider::class); + $tokens = m::mock(TokenRepository::class); + $clients = m::mock(ClientRepository::class); + $encrypter = m::mock(Encrypter::class); $request = Request::create('/'); $request->headers->set('Authorization', 'Bearer token'); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $resourceServer->shouldReceive('validateAuthenticatedRequest')->andReturn($psr = m::mock()); $psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1); $clients->shouldReceive('findActive')->with(1)->andReturn(new TokenGuardTestClient); - $client = $guard->client($request); + $client = $guard->client(); + + $clients->shouldReceive('findActive')->never(); + + $client2 = $guard->client(); $this->assertInstanceOf(TokenGuardTestClient::class, $client); + $this->assertSame($client, $client2); } public function test_no_client_is_returned_when_oauth_throws_exception() @@ -390,19 +493,19 @@ public function test_no_client_is_returned_when_oauth_throws_exception() $clients = m::mock(ClientRepository::class); $encrypter = m::mock(Encrypter::class); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - $request = Request::create('/'); $request->headers->set('Authorization', 'Bearer token'); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $resourceServer->shouldReceive('validateAuthenticatedRequest')->andThrow( new OAuthServerException('message', 500, 'error type') ); - $this->assertNull($guard->client($request)); + $this->assertNull($guard->client()); // Assert that `validateAuthenticatedRequest` isn't called twice on failure. - $this->assertNull($guard->client($request)); + $this->assertNull($guard->client()); } public function test_null_is_returned_if_no_client_is_found() @@ -413,16 +516,16 @@ public function test_null_is_returned_if_no_client_is_found() $clients = m::mock(ClientRepository::class); $encrypter = m::mock(Encrypter::class); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - $request = Request::create('/'); $request->headers->set('Authorization', 'Bearer token'); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $resourceServer->shouldReceive('validateAuthenticatedRequest')->andReturn($psr = m::mock()); $psr->shouldReceive('getAttribute')->with('oauth_client_id')->andReturn(1); $clients->shouldReceive('findActive')->with(1)->andReturn(null); - $this->assertNull($guard->client($request)); + $this->assertNull($guard->client()); } public function test_clients_may_be_retrieved_from_cookies() @@ -433,8 +536,6 @@ public function test_clients_may_be_retrieved_from_cookies() $clients = m::mock(ClientRepository::class); $encrypter = new Encrypter(str_repeat('a', 16)); - $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter); - $request = Request::create('/'); $request->headers->set('X-CSRF-TOKEN', 'token'); $request->cookies->set('laravel_token', @@ -446,9 +547,11 @@ public function test_clients_may_be_retrieved_from_cookies() ], str_repeat('a', 16), 'HS256'), false) ); + $guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter, $request); + $clients->shouldReceive('findActive')->with(1)->andReturn($expectedClient = new TokenGuardTestClient); - $client = $guard->client($request); + $client = $guard->client(); $this->assertEquals($expectedClient, $client); } diff --git a/workbench/app/Models/User.php b/workbench/app/Models/User.php new file mode 100644 index 000000000..6987bfb38 --- /dev/null +++ b/workbench/app/Models/User.php @@ -0,0 +1,42 @@ + + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'email_verified_at' => 'datetime', + ]; +} diff --git a/workbench/database/factories/UserFactory.php b/workbench/database/factories/UserFactory.php new file mode 100644 index 000000000..db6dfa3df --- /dev/null +++ b/workbench/database/factories/UserFactory.php @@ -0,0 +1,20 @@ + + */ +class UserFactory extends \Orchestra\Testbench\Factories\UserFactory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string<\TModel> + */ + protected $model = User::class; +}