From 851277f422908a443126988cd87135c9c2bd1ef6 Mon Sep 17 00:00:00 2001 From: mocode Date: Fri, 23 Apr 2021 15:30:21 +0800 Subject: [PATCH] first commit --- .editorconfig | 15 ++ .gitattributes | 17 ++ .github/CODE_OF_CONDUCT.md | 3 + .github/CONTRIBUTING.md | 3 + .github/ISSUE_TEMPLATE/1_Bug_report.md | 17 ++ .github/ISSUE_TEMPLATE/2_Feature_request.md | 4 + .github/ISSUE_TEMPLATE/config.yml | 8 + .github/PULL_REQUEST_TEMPLATE.md | 9 + .github/SECURITY.md | 92 +++++++ .github/SUPPORT.md | 3 + .github/workflows/tests.yml | 42 +++ .gitignore | 15 ++ .styleci.yml | 4 + CHANGELOG.md | 0 LICENSE.md | 21 ++ README.md | 37 +++ composer.json | 50 ++++ config/sanctum.php | 50 ++++ ...01_create_personal_access_tokens_table.php | 37 +++ phpunit.xml.dist | 21 ++ src/Contracts/HasAbilities.php | 22 ++ src/Contracts/HasApiTokens.php | 45 ++++ src/Guard.php | 110 ++++++++ src/HasApiTokens.php | 77 ++++++ src/Http/Controllers/CsrfCookieController.php | 25 ++ .../EnsureFrontendRequestsAreStateful.php | 74 ++++++ src/NewAccessToken.php | 60 +++++ src/PersonalAccessToken.php | 99 +++++++ src/Sanctum.php | 98 +++++++ src/SanctumServiceProvider.php | 132 ++++++++++ src/TransientToken.php | 30 +++ tests/ActingAsTest.php | 103 ++++++++ tests/DefaultConfigContainsAppUrlTest.php | 37 +++ .../EnsureFrontendRequestsAreStatefulTest.php | 81 ++++++ tests/GuardTest.php | 242 ++++++++++++++++++ tests/HasApiTokensTest.php | 55 ++++ tests/PersonalAccessTokenTest.php | 30 +++ tests/TransientTokenTest.php | 19 ++ 38 files changed, 1787 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/1_Bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/2_Feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/SECURITY.md create mode 100644 .github/SUPPORT.md create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .styleci.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 config/sanctum.php create mode 100644 database/migrations/2021_04_23_000001_create_personal_access_tokens_table.php create mode 100644 phpunit.xml.dist create mode 100644 src/Contracts/HasAbilities.php create mode 100644 src/Contracts/HasApiTokens.php create mode 100644 src/Guard.php create mode 100644 src/HasApiTokens.php create mode 100644 src/Http/Controllers/CsrfCookieController.php create mode 100644 src/Http/Middleware/EnsureFrontendRequestsAreStateful.php create mode 100644 src/NewAccessToken.php create mode 100644 src/PersonalAccessToken.php create mode 100644 src/Sanctum.php create mode 100644 src/SanctumServiceProvider.php create mode 100644 src/TransientToken.php create mode 100644 tests/ActingAsTest.php create mode 100644 tests/DefaultConfigContainsAppUrlTest.php create mode 100644 tests/EnsureFrontendRequestsAreStatefulTest.php create mode 100644 tests/GuardTest.php create mode 100644 tests/HasApiTokensTest.php create mode 100644 tests/PersonalAccessTokenTest.php create mode 100644 tests/TransientTokenTest.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6537ca4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5fe92d4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +* text=auto + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +/tests export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.styleci.yml export-ignore +CHANGELOG.md export-ignore +phpunit.xml.dist export-ignore +UPGRADE.md diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..92b5bf5 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +The Laravel Code of Conduct can be found in the [Laravel documentation](https://laravel.com/docs/contributions#code-of-conduct). diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..38ca9f8 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contribution Guide + +The Laravel contributing guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md new file mode 100644 index 0000000..c798807 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.md @@ -0,0 +1,17 @@ +--- +name: "Bug report" +about: "Report something that's broken. Please ensure your Laravel version is still supported: https://laravel.com/docs/releases#support-policy" +--- + + + + +- Sanctum Version: #.#.# +- Laravel Version: #.#.# +- PHP Version: #.#.# +- Database Driver & Version: + +### Description: + + +### Steps To Reproduce: diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md new file mode 100644 index 0000000..e530d76 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_Feature_request.md @@ -0,0 +1,4 @@ +--- +name: "Feature request" +about: 'For ideas or feature requests, please make a pull request, or open an issue' +--- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6253bb2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Support Questions & Other + url: https://laravel.com/docs/contributions#support-questions + about: 'This repository is only for reporting bugs. If you have a question or need help using the library, click:' + - name: Documentation issue + url: https://github.com/laravel/docs + about: For documentation issues, open a pull request at the laravel/docs repository diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0378693 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ + diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..dd673d4 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,92 @@ +# Security Policy + +**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).** + +## Supported Versions + +Only the latest major version receives security fixes. + +## Reporting a Vulnerability + +If you discover a security vulnerability within Laravel, please send an email to Taylor Otwell at taylor@laravel.com. All security vulnerabilities will be promptly addressed. + +### Public PGP Key + +``` +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: OpenPGP v2.0.8 +Comment: https://sela.io/pgp/ + +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 +WBGOG/qJGDlNiqBYYt2xNqzHCJoC +=zXOv +-----END PGP PUBLIC KEY BLOCK----- +``` diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..f0877fc --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,3 @@ +# Support Questions + +The Laravel support guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions#support-questions). diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..cbd650c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,42 @@ +name: tests + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + tests: + runs-on: ubuntu-20.04 + + strategy: + fail-fast: true + matrix: + php: [7.2, 7.3, 7.4, 8.0] + laravel: [^6.0, ^7.0, ^8.0] + exclude: + - php: 7.2 + laravel: ^8.0 + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip + tools: composer:v2 + coverage: none + + - name: Install dependencies + run: | + composer require "illuminate/contracts=${{ matrix.laravel }}" --no-update + composer update --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c213e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +/vendor +composer.lock +/phpunit.xml +.phpunit.result.cache + +.DS_Store + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw* diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..215fbcf --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,4 @@ +php: + preset: laravel +js: true +css: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..ea20dd3 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Mocode + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a383910 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +## Introduction + +Laravel Sanctum MongoDB provides a featherweight authentication system for SPAs and simple APIs. + +## Installation + +You may install Laravel Sanctum MongoDB via the Composer package manager: +``` +composer require mocode/sanctum-mongodb +``` + +Next, you should publish the Sanctum configuration and migration files using the `vendor:publish` Artisan command. The `sanctum` configuration file will be placed in your application's `config` directory: +``` +php artisan vendor:publish --provider="Mocode\Sanctum\SanctumServiceProvider" +``` + +Finally, you should run your database migrations. Sanctum will create one database table in which to store API tokens: +``` +php artisan migrate +``` + +Next, if you plan to utilize Sanctum to authenticate an SPA, you should add Sanctum's middleware to your `api` middleware group within your application's `app/Http/Kernel.php` file: +``` +'api' => [ + \Mocode\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + 'throttle:api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, +], +``` + +## Official Documentation + +Documentation for Sanctum can be found on the [Laravel website](https://laravel.com/docs/sanctum). + +## License + +Laravel Sanctum MongoDB is open-sourced software licensed under the [MIT license](LICENSE.md). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..93f69f2 --- /dev/null +++ b/composer.json @@ -0,0 +1,50 @@ +{ + "name": "mocode/sanctum-mongodb", + "description": "Laravel Sanctum Mongodb provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": ["laravel", "sanctum", "mongodb", "auth"], + "license": "MIT", + "authors": [ + { + "name": "mocode", + "email": "mocode@163.com" + } + ], + "require": { + "php": "^7.2|^8.0", + "ext-json": "*", + "illuminate/contracts": "^6.9|^7.0|^8.0", + "illuminate/database": "^6.9|^7.0|^8.0", + "illuminate/support": "^6.9|^7.0|^8.0", + "jenssegers/mongodb": "^3.8" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.0|^5.0|^6.0", + "phpunit/phpunit": "^8.0|^9.3" + }, + "autoload": { + "psr-4": { + "Mocode\\Sanctum\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Mocode\\Sanctum\\Tests\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + }, + "laravel": { + "providers": [ + "Mocode\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..3ccc3ca --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,50 @@ + explode(',', env( + 'SANCTUM_STATEFUL_DOMAINS', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1,'.parse_url(env('APP_URL'), PHP_URL_HOST) + )), + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. If this value is null, personal access tokens do + | not expire. This won't tweak the lifetime of first-party sessions. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, + 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, + ], + +]; diff --git a/database/migrations/2021_04_23_000001_create_personal_access_tokens_table.php b/database/migrations/2021_04_23_000001_create_personal_access_tokens_table.php new file mode 100644 index 0000000..883989f --- /dev/null +++ b/database/migrations/2021_04_23_000001_create_personal_access_tokens_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $collection->morphs('tokenable'); + $collection->string('name'); + $collection->string('token', 64); + $collection->text('abilities')->nullable(); + $collection->timestamp('last_used_at')->nullable(); + $collection->timestamps(); + + $collection->unique('token'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('personal_access_tokens'); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..eae7043 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + + + + + ./tests/ + + + diff --git a/src/Contracts/HasAbilities.php b/src/Contracts/HasAbilities.php new file mode 100644 index 0000000..b274d31 --- /dev/null +++ b/src/Contracts/HasAbilities.php @@ -0,0 +1,22 @@ +auth = $auth; + $this->expiration = $expiration; + $this->provider = $provider; + } + + /** + * Retrieve the authenticated user for the incoming request. + * + * @param \Illuminate\Http\Request $request + * @return mixed + */ + public function __invoke(Request $request) + { + foreach (Arr::wrap(config('sanctum.guard', 'web')) as $guard) { + if ($user = $this->auth->guard($guard)->user()) { + return $this->supportsTokens($user) + ? $user->withAccessToken(new TransientToken) + : $user; + } + } + + if ($token = $request->bearerToken()) { + $model = Sanctum::$personalAccessTokenModel; + + $accessToken = $model::findToken($token); + + if (! $accessToken || + ($this->expiration && + $accessToken->created_at->lte(now()->subMinutes($this->expiration))) || + ! $this->hasValidProvider($accessToken->tokenable)) { + return; + } + + return $this->supportsTokens($accessToken->tokenable) ? $accessToken->tokenable->withAccessToken( + tap($accessToken->forceFill(['last_used_at' => now()]))->save() + ) : null; + } + } + + /** + * Determine if the tokenable model supports API tokens. + * + * @param mixed $tokenable + * @return bool + */ + protected function supportsTokens($tokenable = null) + { + return $tokenable && in_array(HasApiTokens::class, class_uses_recursive( + get_class($tokenable) + )); + } + + /** + * Determine if the tokenable model matches the provider's model type. + * + * @param \Illuminate\Database\Eloquent\Model $tokenable + * @return bool + */ + protected function hasValidProvider($tokenable) + { + if (is_null($this->provider)) { + return true; + } + + $model = config("auth.providers.{$this->provider}.model"); + + return $tokenable instanceof $model; + } +} diff --git a/src/HasApiTokens.php b/src/HasApiTokens.php new file mode 100644 index 0000000..736eb56 --- /dev/null +++ b/src/HasApiTokens.php @@ -0,0 +1,77 @@ +morphMany(Sanctum::$personalAccessTokenModel, 'tokenable'); + } + + /** + * Determine if the current API token has a given scope. + * + * @param string $ability + * @return bool + */ + public function tokenCan(string $ability) + { + return $this->accessToken ? $this->accessToken->can($ability) : false; + } + + /** + * Create a new personal access token for the user. + * + * @param string $name + * @param array $abilities + * @return \Mocode\Sanctum\NewAccessToken + */ + public function createToken(string $name, array $abilities = ['*']) + { + $token = $this->tokens()->create([ + 'name' => $name, + 'token' => hash('sha256', $plainTextToken = Str::random(40)), + 'abilities' => $abilities, + ]); + + return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken); + } + + /** + * Get the access token currently associated with the user. + * + * @return \Mocode\Sanctum\Contracts\HasAbilities + */ + public function currentAccessToken() + { + return $this->accessToken; + } + + /** + * Set the current access token for the user. + * + * @param \Mocode\Sanctum\Contracts\HasAbilities $accessToken + * @return $this + */ + public function withAccessToken($accessToken) + { + $this->accessToken = $accessToken; + + return $this; + } +} diff --git a/src/Http/Controllers/CsrfCookieController.php b/src/Http/Controllers/CsrfCookieController.php new file mode 100644 index 0000000..39cd8d7 --- /dev/null +++ b/src/Http/Controllers/CsrfCookieController.php @@ -0,0 +1,25 @@ +expectsJson()) { + return new JsonResponse(null, 204); + } + + return new Response('', 204); + } +} diff --git a/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php b/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php new file mode 100644 index 0000000..b19b57f --- /dev/null +++ b/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php @@ -0,0 +1,74 @@ +configureSecureCookieSessions(); + + return (new Pipeline(app()))->send($request)->through(static::fromFrontend($request) ? [ + function ($request, $next) { + $request->attributes->set('sanctum', true); + + return $next($request); + }, + config('sanctum.middleware.encrypt_cookies', \Illuminate\Cookie\Middleware\EncryptCookies::class), + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + config('sanctum.middleware.verify_csrf_token', \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class), + ] : [])->then(function ($request) use ($next) { + return $next($request); + }); + } + + /** + * Configure secure cookie sessions. + * + * @return void + */ + protected function configureSecureCookieSessions() + { + config([ + 'session.http_only' => true, + 'session.same_site' => 'lax', + ]); + } + + /** + * Determine if the given request is from the first-party application frontend. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + public static function fromFrontend($request) + { + $domain = $request->headers->get('referer') ?: $request->headers->get('origin'); + + if (is_null($domain)) { + return false; + } + + $domain = Str::replaceFirst('https://', '', $domain); + $domain = Str::replaceFirst('http://', '', $domain); + $domain = Str::endsWith($domain, '/') ? $domain : "{$domain}/"; + + $stateful = array_filter(config('sanctum.stateful', [])); + + return Str::is(Collection::make($stateful)->map(function ($uri) { + return trim($uri).'/*'; + })->all(), $domain); + } +} diff --git a/src/NewAccessToken.php b/src/NewAccessToken.php new file mode 100644 index 0000000..5c73ef4 --- /dev/null +++ b/src/NewAccessToken.php @@ -0,0 +1,60 @@ +accessToken = $accessToken; + $this->plainTextToken = $plainTextToken; + } + + /** + * Get the instance as an array. + * + * @return array + */ + public function toArray() + { + return [ + 'accessToken' => $this->accessToken, + 'plainTextToken' => $this->plainTextToken, + ]; + } + + /** + * Convert the object to its JSON representation. + * + * @param int $options + * @return string + */ + public function toJson($options = 0) + { + return json_encode($this->toArray(), $options); + } +} diff --git a/src/PersonalAccessToken.php b/src/PersonalAccessToken.php new file mode 100644 index 0000000..d6124eb --- /dev/null +++ b/src/PersonalAccessToken.php @@ -0,0 +1,99 @@ + 'json', + 'last_used_at' => 'datetime', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + 'token', + 'abilities', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + 'token', + ]; + + /** + * Get the tokenable model that the access token belongs to. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphTo + */ + public function tokenable() + { + return $this->morphTo('tokenable'); + } + + /** + * Find the token instance matching the given token. + * + * @param string $token + * @return static|null + */ + public static function findToken($token) + { + if (strpos($token, '|') === false) { + return static::where('token', hash('sha256', $token))->first(); + } + + [$id, $token] = explode('|', $token, 2); + + if ($instance = static::find($id)) { + return hash_equals($instance->token, hash('sha256', $token)) ? $instance : null; + } + } + + /** + * Determine if the token has a given ability. + * + * @param string $ability + * @return bool + */ + public function can($ability) + { + return in_array('*', $this->abilities) || + array_key_exists($ability, array_flip($this->abilities)); + } + + /** + * Determine if the token is missing a given ability. + * + * @param string $ability + * @return bool + */ + public function cant($ability) + { + return ! $this->can($ability); + } +} diff --git a/src/Sanctum.php b/src/Sanctum.php new file mode 100644 index 0000000..2051bcc --- /dev/null +++ b/src/Sanctum.php @@ -0,0 +1,98 @@ +shouldIgnoreMissing(false); + + if (in_array('*', $abilities)) { + $token->shouldReceive('can')->withAnyArgs()->andReturn(true); + } else { + foreach ($abilities as $ability) { + $token->shouldReceive('can')->with($ability)->andReturn(true); + } + } + + $user->withAccessToken($token); + + if (isset($user->wasRecentlyCreated) && $user->wasRecentlyCreated) { + $user->wasRecentlyCreated = false; + } + + app('auth')->guard($guard)->setUser($user); + + app('auth')->shouldUse($guard); + + return $user; + } + + /** + * Set the personal access token model name. + * + * @param string $model + * @return void + */ + public static function usePersonalAccessTokenModel($model) + { + static::$personalAccessTokenModel = $model; + } + + /** + * Determine if Sanctum's migrations should be run. + * + * @return bool + */ + public static function shouldRunMigrations() + { + return static::$runsMigrations; + } + + /** + * Configure Sanctum to not register its migrations. + * + * @return static + */ + public static function ignoreMigrations() + { + static::$runsMigrations = false; + + return new static; + } + + /** + * Get the token model class name. + * + * @return string + */ + public static function personalAccessTokenModel() + { + return static::$personalAccessTokenModel; + } +} diff --git a/src/SanctumServiceProvider.php b/src/SanctumServiceProvider.php new file mode 100644 index 0000000..6e3fa14 --- /dev/null +++ b/src/SanctumServiceProvider.php @@ -0,0 +1,132 @@ + array_merge([ + 'driver' => 'sanctum', + 'provider' => null, + ], config('auth.guards.sanctum', [])), + ]); + + if (! $this->app->configurationIsCached()) { + $this->mergeConfigFrom(__DIR__.'/../config/sanctum.php', 'sanctum'); + } + } + + /** + * Bootstrap any application services. + * + * @return void + */ + public function boot() + { + if ($this->app->runningInConsole()) { + $this->registerMigrations(); + + $this->publishes([ + __DIR__.'/../database/migrations' => database_path('migrations'), + ], 'sanctum-migrations'); + + $this->publishes([ + __DIR__.'/../config/sanctum.php' => config_path('sanctum.php'), + ], 'sanctum-config'); + } + + $this->defineRoutes(); + $this->configureGuard(); + $this->configureMiddleware(); + } + + /** + * Register Sanctum's migration files. + * + * @return void + */ + protected function registerMigrations() + { + if (Sanctum::shouldRunMigrations()) { + return $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); + } + } + + /** + * Define the Sanctum routes. + * + * @return void + */ + protected function defineRoutes() + { + if ($this->app->routesAreCached() || config('sanctum.routes') === false) { + return; + } + + Route::group(['prefix' => config('sanctum.prefix', 'sanctum')], function () { + Route::get( + '/csrf-cookie', + CsrfCookieController::class.'@show' + )->middleware('web'); + }); + } + + /** + * Configure the Sanctum authentication guard. + * + * @return void + */ + protected function configureGuard() + { + Auth::resolved(function ($auth) { + $auth->extend('sanctum', function ($app, $name, array $config) use ($auth) { + return tap($this->createGuard($auth, $config), function ($guard) { + app()->refresh('request', $guard, 'setRequest'); + }); + }); + }); + } + + /** + * Register the guard. + * + * @param \Illuminate\Contracts\Auth\Factory $auth + * @param array $config + * @return RequestGuard + */ + protected function createGuard($auth, $config) + { + return new RequestGuard( + new Guard($auth, config('sanctum.expiration'), $config['provider']), + $this->app['request'], + $auth->createUserProvider($config['provider'] ?? null) + ); + } + + /** + * Configure the Sanctum middleware and priority. + * + * @return void + */ + protected function configureMiddleware() + { + $kernel = $this->app->make(Kernel::class); + + $kernel->prependToMiddlewarePriority(EnsureFrontendRequestsAreStateful::class); + } +} diff --git a/src/TransientToken.php b/src/TransientToken.php new file mode 100644 index 0000000..fa3ce48 --- /dev/null +++ b/src/TransientToken.php @@ -0,0 +1,30 @@ +set('database.default', 'testbench'); + + $app['config']->set('database.connections.testbench', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } + + public function testActingAsWhenTheRouteIsProtectedByAuthMiddlware() + { + $this->withoutExceptionHandling(); + + Route::get('/foo', function () { + return 'bar'; + })->middleware('auth:sanctum'); + + Sanctum::actingAs($user = new SanctumUser); + $user->id = 1; + + $response = $this->get('/foo'); + + $response->assertStatus(200); + $response->assertSee('bar'); + } + + public function testActingAsWhenTheRouteIsProtectedUsingAbilities() + { + $this->artisan('migrate', ['--database' => 'testbench'])->run(); + + $this->withoutExceptionHandling(); + + Route::get('/foo', function () { + if (Auth::user()->tokenCan('baz')) { + return 'bar'; + } + + return response(403); + })->middleware('auth:sanctum'); + + $user = new SanctumUser; + $user->id = 1; + + Sanctum::actingAs($user, ['baz']); + + $response = $this->get('/foo'); + + $response->assertStatus(200); + $response->assertSee('bar'); + } + + public function testActingAsWhenKeyHasAnyAbility() + { + $this->artisan('migrate', ['--database' => 'testbench'])->run(); + + $this->withoutExceptionHandling(); + + Route::get('/foo', function () { + if (Auth::user()->tokenCan('baz')) { + return 'bar'; + } + + return response(403); + })->middleware('auth:sanctum'); + + $user = new SanctumUser; + $user->id = 1; + + Sanctum::actingAs($user, ['*']); + + $response = $this->get('/foo'); + + $response->assertStatus(200); + $response->assertSee('bar'); + } + + protected function getPackageProviders($app) + { + return [SanctumServiceProvider::class]; + } +} + +class SanctumUser extends User implements HasApiTokensContract +{ + use HasApiTokens; +} diff --git a/tests/DefaultConfigContainsAppUrlTest.php b/tests/DefaultConfigContainsAppUrlTest.php new file mode 100644 index 0000000..c0b9b34 --- /dev/null +++ b/tests/DefaultConfigContainsAppUrlTest.php @@ -0,0 +1,37 @@ +config->set('sanctum.stateful', $config['stateful']); + } + + public function test_default_config_contains_app_url() + { + $config = require __DIR__.'/../config/sanctum.php'; + + $app_host = parse_url(env('APP_URL'), PHP_URL_HOST); + + $this->assertContains($app_host, $config['stateful']); + } + + /** + * @environment-setup useDefaultStatefulConfiguration + */ + public function test_request_from_app_url_is_stateful_with_default_config() + { + $request = Request::create('/'); + $request->headers->set('referer', env('APP_URL')); + + $this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request)); + } +} diff --git a/tests/EnsureFrontendRequestsAreStatefulTest.php b/tests/EnsureFrontendRequestsAreStatefulTest.php new file mode 100644 index 0000000..d7367f1 --- /dev/null +++ b/tests/EnsureFrontendRequestsAreStatefulTest.php @@ -0,0 +1,81 @@ +set('sanctum.stateful', ['test.com', '*.test.com']); + } + + public function test_request_referer_is_parsed_against_configuration() + { + $request = Request::create('/'); + $request->headers->set('referer', 'https://test.com'); + + $this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request)); + + $request = Request::create('/'); + $request->headers->set('referer', 'https://wrong.com'); + + $this->assertFalse(EnsureFrontendRequestsAreStateful::fromFrontend($request)); + + $request = Request::create('/'); + $request->headers->set('referer', 'https://test.com.x'); + + $this->assertFalse(EnsureFrontendRequestsAreStateful::fromFrontend($request)); + + $request = Request::create('/'); + $request->headers->set('referer', 'https://foobar.test.com/'); + + $this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request)); + } + + public function test_request_origin_fallback() + { + $request = Request::create('/'); + $request->headers->set('origin', 'test.com'); + + $this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request)); + + $request = Request::create('/'); + $request->headers->set('referer', null); + $request->headers->set('origin', 'test.com'); + + $this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request)); + + $request = Request::create('/'); + $request->headers->set('referer', ''); + $request->headers->set('origin', 'test.com'); + + $this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request)); + } + + public function test_wildcard_matching() + { + $request = Request::create('/'); + $request->headers->set('referer', 'https://foo.test.com'); + + $this->assertTrue(EnsureFrontendRequestsAreStateful::fromFrontend($request)); + } + + public function test_requests_are_not_stateful_without_referer() + { + $this->app['config']->set('sanctum.stateful', ['']); + + $request = Request::create('/'); + + $this->assertFalse(EnsureFrontendRequestsAreStateful::fromFrontend($request)); + } + + protected function getPackageProviders($app) + { + return [SanctumServiceProvider::class]; + } +} diff --git a/tests/GuardTest.php b/tests/GuardTest.php new file mode 100644 index 0000000..634d5c3 --- /dev/null +++ b/tests/GuardTest.php @@ -0,0 +1,242 @@ +set('database.default', 'testbench'); + + $app['config']->set('database.connections.testbench', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } + + public function tearDown(): void + { + parent::tearDown(); + + Mockery::close(); + } + + public function test_authentication_is_attempted_with_web_middleware() + { + $factory = Mockery::mock(AuthFactory::class); + + $guard = new Guard($factory, null, 'users'); + + $webGuard = Mockery::mock(stdClass::class); + + $factory->shouldReceive('guard') + ->with('web') + ->andReturn($webGuard); + + $webGuard->shouldReceive('user')->once()->andReturn($fakeUser = new User); + + $user = $guard->__invoke(Request::create('/', 'GET')); + + $this->assertSame($user, $fakeUser); + $this->assertTrue($user->tokenCan('foo')); + } + + public function test_authentication_is_attempted_with_token_if_no_session_present() + { + $this->artisan('migrate', ['--database' => 'testbench'])->run(); + + $factory = Mockery::mock(AuthFactory::class); + + $guard = new Guard($factory, null, 'users'); + + $webGuard = Mockery::mock(stdClass::class); + + $factory->shouldReceive('guard') + ->with('web') + ->andReturn($webGuard); + + $webGuard->shouldReceive('user')->once()->andReturn(null); + + $request = Request::create('/', 'GET'); + $request->headers->set('Authorization', 'Bearer test'); + + $user = $guard->__invoke($request); + + $this->assertNull($user); + } + + public function test_authentication_with_token_fails_if_expired() + { + $this->loadLaravelMigrations(['--database' => 'testbench']); + $this->artisan('migrate', ['--database' => 'testbench'])->run(); + + $factory = Mockery::mock(AuthFactory::class); + + $guard = new Guard($factory, 1, 'users'); + + $webGuard = Mockery::mock(stdClass::class); + + $factory->shouldReceive('guard') + ->with('web') + ->andReturn($webGuard); + + $webGuard->shouldReceive('user')->once()->andReturn(null); + + $request = Request::create('/', 'GET'); + $request->headers->set('Authorization', 'Bearer test'); + + $user = User::forceCreate([ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + 'remember_token' => Str::random(10), + ]); + + $token = PersonalAccessToken::forceCreate([ + 'tokenable_id' => $user->id, + 'tokenable_type' => get_class($user), + 'name' => 'Test', + 'token' => hash('sha256', 'test'), + 'created_at' => now()->subMinutes(60), + ]); + + $user = $guard->__invoke($request); + + $this->assertNull($user); + } + + public function test_authentication_is_successful_with_token_if_no_session_present() + { + $this->loadLaravelMigrations(['--database' => 'testbench']); + $this->artisan('migrate', ['--database' => 'testbench'])->run(); + + $factory = Mockery::mock(AuthFactory::class); + + $guard = new Guard($factory, null); + + $webGuard = Mockery::mock(stdClass::class); + + $factory->shouldReceive('guard') + ->with('web') + ->andReturn($webGuard); + + $webGuard->shouldReceive('user')->once()->andReturn(null); + + $request = Request::create('/', 'GET'); + $request->headers->set('Authorization', 'Bearer test'); + + $user = User::forceCreate([ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + 'remember_token' => Str::random(10), + ]); + + $token = PersonalAccessToken::forceCreate([ + 'tokenable_id' => $user->id, + 'tokenable_type' => get_class($user), + 'name' => 'Test', + 'token' => hash('sha256', 'test'), + ]); + + $returnedUser = $guard->__invoke($request); + + $this->assertEquals($user->id, $returnedUser->id); + $this->assertEquals($token->id, $returnedUser->currentAccessToken()->id); + $this->assertInstanceOf(DateTimeInterface::class, $returnedUser->currentAccessToken()->last_used_at); + } + + public function test_authentication_with_token_fails_if_user_provider_is_invalid() + { + $this->loadLaravelMigrations(['--database' => 'testbench']); + $this->artisan('migrate', ['--database' => 'testbench'])->run(); + + config(['auth.guards.sanctum.provider' => 'users']); + config(['auth.providers.users.model' => 'App\Models\User']); + + $factory = $this->app->make(AuthFactory::class); + $requestGuard = $factory->guard('sanctum'); + + $request = Request::create('/', 'GET'); + $request->headers->set('Authorization', 'Bearer test'); + + $user = User::forceCreate([ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + 'remember_token' => Str::random(10), + ]); + + $token = PersonalAccessToken::forceCreate([ + 'tokenable_id' => $user->id, + 'tokenable_type' => get_class($user), + 'name' => 'Test', + 'token' => hash('sha256', 'test'), + ]); + + $returnedUser = $requestGuard->setRequest($request)->user(); + + $this->assertNull($returnedUser); + $this->assertInstanceOf(EloquentUserProvider::class, $requestGuard->getProvider()); + } + + public function test_authentication_is_successful_with_token_if_user_provider_is_valid() + { + $this->loadLaravelMigrations(['--database' => 'testbench']); + $this->artisan('migrate', ['--database' => 'testbench'])->run(); + + config(['auth.guards.sanctum.provider' => 'users']); + config(['auth.providers.users.model' => User::class]); + + $factory = $this->app->make(AuthFactory::class); + $requestGuard = $factory->guard('sanctum'); + + $request = Request::create('/', 'GET'); + $request->headers->set('Authorization', 'Bearer test'); + + $user = User::forceCreate([ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + 'remember_token' => Str::random(10), + ]); + + $token = PersonalAccessToken::forceCreate([ + 'tokenable_id' => $user->id, + 'tokenable_type' => get_class($user), + 'name' => 'Test', + 'token' => hash('sha256', 'test'), + ]); + + $returnedUser = $requestGuard->setRequest($request)->user(); + + $this->assertEquals($user->id, $returnedUser->id); + $this->assertInstanceOf(EloquentUserProvider::class, $requestGuard->getProvider()); + } + + protected function getPackageProviders($app) + { + return [SanctumServiceProvider::class]; + } +} + +class User extends Model implements HasApiTokensContract +{ + use HasApiTokens; +} diff --git a/tests/HasApiTokensTest.php b/tests/HasApiTokensTest.php new file mode 100644 index 0000000..a56bab7 --- /dev/null +++ b/tests/HasApiTokensTest.php @@ -0,0 +1,55 @@ +createToken('test', ['foo']); + + [$id, $token] = explode('|', $newToken->plainTextToken); + + $this->assertEquals( + $newToken->accessToken->token, + hash('sha256', $token) + ); + + $this->assertEquals( + $newToken->accessToken->id, + $id + ); + } + + public function test_can_check_token_abilities() + { + $class = new ClassThatHasApiTokens; + + $class->withAccessToken(new TransientToken); + + $this->assertTrue($class->tokenCan('foo')); + } +} + +class ClassThatHasApiTokens implements HasApiTokensContract +{ + use HasApiTokens; + + public function tokens() + { + return new class { + public function create(array $attributes) + { + return new PersonalAccessToken($attributes); + } + }; + } +} diff --git a/tests/PersonalAccessTokenTest.php b/tests/PersonalAccessTokenTest.php new file mode 100644 index 0000000..68e34f7 --- /dev/null +++ b/tests/PersonalAccessTokenTest.php @@ -0,0 +1,30 @@ +abilities = []; + + $this->assertFalse($token->can('foo')); + + $token->abilities = ['foo']; + + $this->assertTrue($token->can('foo')); + $this->assertFalse($token->can('bar')); + $this->assertTrue($token->cant('bar')); + $this->assertFalse($token->cant('foo')); + + $token->abilities = ['foo', '*']; + + $this->assertTrue($token->can('foo')); + $this->assertTrue($token->can('bar')); + } +} diff --git a/tests/TransientTokenTest.php b/tests/TransientTokenTest.php new file mode 100644 index 0000000..26948e5 --- /dev/null +++ b/tests/TransientTokenTest.php @@ -0,0 +1,19 @@ +assertTrue($token->can('foo')); + $this->assertTrue($token->can('bar')); + $this->assertFalse($token->cant('foo')); + $this->assertFalse($token->cant('bar')); + } +}