├── .php_cs.dist.php ├── LICENSE.md ├── README.md ├── composer.json ├── config └── bearer.php ├── database ├── factories │ └── TokenFactory.php └── migrations │ └── create_bearer_table.php.stub └── src ├── Bearer.php ├── BearerServiceProvider.php ├── Concerns └── InteractsWithExpiration.php ├── Facades └── Bearer.php ├── Http └── Middleware │ └── VerifyBearerToken.php └── Models └── Token.php /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'method_argument_space' => [ 30 | 'on_multiline' => 'ensure_fully_multiline', 31 | 'keep_multiple_spaces_after_comma' => true, 32 | ], 33 | 'single_trait_insert_per_statement' => true, 34 | ]) 35 | ->setFinder($finder); 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Ryan Chandler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bearer 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/ryangjchandler/bearer.svg?style=flat-square)](https://packagist.org/packages/ryangjchandler/bearer) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/ryangjchandler/bearer/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/ryangjchandler/bearer/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/ryangjchandler/bearer.svg?style=flat-square)](https://packagist.org/packages/ryangjchandler/bearer) 6 | 7 | Minimalistic token-based authorization for Laravel API endpoints. 8 | 9 | ## Installation 10 | 11 | You can install the package via Composer: 12 | 13 | ```bash 14 | composer require ryangjchandler/bearer 15 | ``` 16 | 17 | You can publish and run the migrations with: 18 | 19 | ```bash 20 | php artisan vendor:publish --provider="RyanChandler\Bearer\BearerServiceProvider" --tag="bearer-migrations" 21 | php artisan migrate 22 | ``` 23 | 24 | You can publish the config file with: 25 | ```bash 26 | php artisan vendor:publish --provider="RyanChandler\Bearer\BearerServiceProvider" --tag="bearer-config" 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Creating tokens 32 | 33 | To create a new token, you can use the `RyanChandler\Bearer\Models\Token` model. 34 | 35 | ```php 36 | use RyanChandler\Bearer\Models\Token; 37 | 38 | $token = Token::create([ 39 | 'token' => Str::random(32), 40 | ]); 41 | ``` 42 | 43 | Alternatively, you can use the `RyanChandler\Bearer\Facades\Bearer` facade to `generate` a token. 44 | 45 | ```php 46 | use RyanChandler\Bearer\Facades\Bearer; 47 | 48 | $token = Bearer::generate(domains: [], expiresAt: null, description: null); 49 | ``` 50 | 51 | By default, Bearer uses time-ordered UUIDs for token strings. You can modify this behaviour by passing a `Closure` to `Bearer::generateTokenUsing`. This function must return a string for storage to the database. 52 | 53 | ```php 54 | use RyanChandler\Bearer\Facades\Bearer; 55 | 56 | Bearer::generateTokenUsing(static function (): string { 57 | return (string) Str::orderedUuid(); 58 | }); 59 | ``` 60 | 61 | ### Retrieving a `Token` instance 62 | 63 | To retrieve a `Token` instance from the `token` string, you can use the `RyanChandler\Bearer\Facades\Bearer` facade. 64 | 65 | ```php 66 | use RyanChandler\Bearer\Facades\Bearer; 67 | 68 | $token = Bearer::find('my-token-string'); 69 | ``` 70 | 71 | ### Using a token in a request 72 | 73 | Bearer uses the `Authorization` header of a request to retreive the token instance. You should format it like so: 74 | 75 | ``` 76 | Authorization: Bearer my-token-string 77 | ``` 78 | 79 | ### Verifying tokens 80 | 81 | To verify a token, add the `RyanChandler\Bearer\Http\Middleware\VerifyBearerToken` middleware to your API route. 82 | 83 | ```php 84 | use RyanChandler\Bearer\Http\Middleware\VerifyBearerToken; 85 | 86 | Route::get('/endpoint', MyEndpointController::class)->middleware(VerifyBearerToken::class); 87 | ``` 88 | 89 | ### Token expiration 90 | 91 | If you would like a token to expire at a particular time, you can use the `expires_at` column. 92 | 93 | ```php 94 | $token = Bearer::find('my-token-string'); 95 | 96 | $token->update([ 97 | 'expires_at' => now()->addWeek(), 98 | ]); 99 | ``` 100 | 101 | Or just use the class's helper methods. 102 | 103 | ```php 104 | $token = Bearer::find('my-token-string'); 105 | 106 | $token->addWeeks(1)->save(); 107 | ``` 108 | 109 | If you try to use the token after this time, it will return an error. 110 | 111 | ### Limit tokens to a particular domain 112 | 113 | Token usage can be restricted to a particular domain. Bearer uses the scheme and host from the request to determine if the token is valid or not. 114 | 115 | ```php 116 | $token = Bearer::find('my-token-string'); 117 | 118 | $token->update([ 119 | 'domains' => [ 120 | 'https://laravel.com', 121 | ], 122 | ]); 123 | ``` 124 | 125 | If you attempt to use this token from any domain other than `https://laravel.com`, it will fail and abort. 126 | 127 | > **Note**: domain checks include the scheme so be sure to add both cases for HTTP and HTTPS if needed. 128 | 129 | ### Set a token description 130 | 131 | You can optionally set a description for the token. 132 | 133 | ```php 134 | $token = Bearer::find('my-token-string'); 135 | 136 | $token->update([ 137 | 'description' => 'Example description for the token.', 138 | ]); 139 | ``` 140 | 141 | > **Note**: The description field accepts a maximum of 255 characters. 142 | 143 | 144 | ## Testing 145 | 146 | ```bash 147 | composer test 148 | ``` 149 | 150 | ## Contributing 151 | 152 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 153 | 154 | ## Security Vulnerabilities 155 | 156 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 157 | 158 | ## Credits 159 | 160 | - [Ryan Chandler](https://github.com/ryangjchandler) 161 | - [All Contributors](../../contributors) 162 | 163 | ## License 164 | 165 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 166 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ryangjchandler/bearer", 3 | "description": "Minimalistic token-based authentication for Laravel API endpoints.", 4 | "keywords": [ 5 | "ryangjchandler", 6 | "laravel", 7 | "bearer" 8 | ], 9 | "homepage": "https://github.com/ryangjchandler/bearer", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Ryan Chandler", 14 | "email": "support@ryangjchandler.co.uk", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2", 20 | "illuminate/contracts": "^11.0|^12.0", 21 | "illuminate/database": "^11.0|^12.0", 22 | "illuminate/http": "^11.0|^12.0", 23 | "spatie/laravel-package-tools": "^1.16" 24 | }, 25 | "require-dev": { 26 | "brianium/paratest": "^7.4", 27 | "nunomaduro/collision": "^8.0", 28 | "orchestra/testbench": "^9.0|^10.0", 29 | "phpunit/phpunit": "^10.5|^11.5.3", 30 | "spatie/laravel-ray": "^1.32" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "RyanChandler\\Bearer\\": "src", 35 | "RyanChandler\\Bearer\\Database\\Factories\\": "database/factories" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "RyanChandler\\Bearer\\Tests\\": "tests" 41 | } 42 | }, 43 | "scripts": { 44 | "psalm": "vendor/bin/psalm", 45 | "test": "./vendor/bin/testbench package:test --parallel --no-coverage", 46 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 47 | }, 48 | "config": { 49 | "sort-packages": true 50 | }, 51 | "extra": { 52 | "laravel": { 53 | "providers": [ 54 | "RyanChandler\\Bearer\\BearerServiceProvider" 55 | ], 56 | "aliases": { 57 | "Bearer": "RyanChandler\\Bearer\\BearerFacade" 58 | } 59 | } 60 | }, 61 | "minimum-stability": "dev", 62 | "prefer-stable": true 63 | } 64 | -------------------------------------------------------------------------------- /config/bearer.php: -------------------------------------------------------------------------------- 1 | env('BEARER_VERIFY_DOMAINS', true), 6 | 7 | ]; 8 | -------------------------------------------------------------------------------- /database/factories/TokenFactory.php: -------------------------------------------------------------------------------- 1 | state([ 19 | 'expires_at' => $expiresAt ?? now() 20 | ]); 21 | } 22 | 23 | public function domains($domains) 24 | { 25 | $domains = Arr::wrap($domains); 26 | 27 | return $this->state([ 28 | 'domains' => $domains, 29 | ]); 30 | } 31 | 32 | public function description(string $description) 33 | { 34 | return $this->state([ 35 | 'description' => $description, 36 | ]); 37 | } 38 | 39 | public function definition() 40 | { 41 | return [ 42 | 'token' => Str::random(32), 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /database/migrations/create_bearer_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('token')->unique(); 14 | $table->string('description')->nullable(); 15 | $table->text('domains')->nullable(); 16 | $table->datetime('expires_at')->nullable(); 17 | $table->timestamps(); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/Bearer.php: -------------------------------------------------------------------------------- 1 | generateTokenCallback = function () { 17 | return (string) Str::orderedUuid(); 18 | }; 19 | } 20 | 21 | public function find(string $token): ?Token 22 | { 23 | if (Str::startsWith($token, 'Bearer ')) { 24 | $token = Str::after($token, 'Bearer '); 25 | } 26 | 27 | return Token::where('token', $token)->first(); 28 | } 29 | 30 | public function generateTokenUsing(Closure $callback) 31 | { 32 | $this->generateTokenCallback = $callback; 33 | 34 | return $this; 35 | } 36 | 37 | public function generate(array $domains = [], ?DateTimeInterface $expiresAt = null, ?string $description = null): Token 38 | { 39 | if ($description !== null && strlen($description) > 255) { 40 | throw new \InvalidArgumentException('Token descriptions must be <= 255 characters.'); 41 | } 42 | 43 | $callback = $this->generateTokenCallback; 44 | 45 | return Token::create([ 46 | 'token' => $callback(), 47 | 'domains' => $domains, 48 | 'description' => $description, 49 | 'expires_at' => $expiresAt, 50 | ]); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/BearerServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('bearer') 14 | ->hasConfigFile() 15 | ->hasMigration('create_bearer_table'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithExpiration.php: -------------------------------------------------------------------------------- 1 | expires_at ?: now(); 12 | 13 | return $this->expires($expires->addMonths($months)); 14 | } 15 | 16 | public function addWeeks(int $weeks): Token 17 | { 18 | $expires = $this->expires_at ?: now(); 19 | 20 | return $this->expires($expires->addWeeks($weeks)); 21 | } 22 | 23 | public function addDays(int $days): Token 24 | { 25 | $expires = $this->expires_at ?: now(); 26 | 27 | return $this->expires($expires->addDays($days)); 28 | } 29 | 30 | public function addHours(int $hours): Token 31 | { 32 | $expires = $this->expires_at ?: now(); 33 | 34 | return $this->expires($expires->addHours($hours)); 35 | } 36 | 37 | public function addMinutes(int $minutes): Token 38 | { 39 | $expires = $this->expires_at ?: now(); 40 | 41 | return $this->expires($expires->addMinutes($minutes)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Facades/Bearer.php: -------------------------------------------------------------------------------- 1 | bearer = $bearer; 17 | } 18 | 19 | public function handle(Request $request, Closure $next) 20 | { 21 | $token = $this->findTokenStringFromRequest($request); 22 | 23 | if (! is_string($token)) { 24 | return $token; 25 | } 26 | 27 | $token = $this->bearer->find($token); 28 | 29 | if (! $token) { 30 | return $this->abort('Please provide a valid token.'); 31 | } 32 | 33 | if ($token->expired) { 34 | return $this->abort('This token has expired.'); 35 | } 36 | 37 | if (! config('bearer.verify_domains') || ! $token->domains || $token->domains->collect()->isEmpty()) { 38 | return $next($request); 39 | } 40 | 41 | if (! in_array($request->getSchemeAndHttpHost(), $token->domains->toArray())) { 42 | return $this->abort('This token cannot be used with your domain.'); 43 | } 44 | 45 | return $next($request); 46 | } 47 | 48 | protected function findTokenStringFromRequest(Request $request) 49 | { 50 | if ($request->has('token')) { 51 | return $request->input('token'); 52 | } 53 | 54 | $token = $request->header('Authorization'); 55 | 56 | if (! $token) { 57 | return $this->abort('Please provide a valid token.'); 58 | } 59 | 60 | $token = Str::after($token, 'Bearer '); 61 | 62 | if (! $token) { 63 | return $this->abort('Please provide a valid token.'); 64 | } 65 | 66 | return $token; 67 | } 68 | 69 | protected function abort(string $message) 70 | { 71 | return response()->json([ 72 | 'status' => 401, 73 | 'message' => $message, 74 | ], 401); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Models/Token.php: -------------------------------------------------------------------------------- 1 | AsArrayObject::class, 24 | 'expires_at' => 'datetime', 25 | ]; 26 | 27 | protected $appends = [ 28 | 'expired', 29 | ]; 30 | 31 | public function getExpiredAttribute() 32 | { 33 | return $this->expires_at && $this->expires_at->isPast(); 34 | } 35 | 36 | public function setExpiredAttribute(bool $expired) 37 | { 38 | $this->expires_at = $expired ? now() : null; 39 | } 40 | 41 | public function addDomain(string $domain) 42 | { 43 | if (! $this->domains) { 44 | $this->domains = []; 45 | } 46 | 47 | $this->domains[] = $domain; 48 | 49 | return $this; 50 | } 51 | 52 | public function expires(DateTimeInterface $expiresAt): Token 53 | { 54 | $this->expires_at = $expiresAt; 55 | 56 | return $this; 57 | } 58 | 59 | public function setDescription(string $description): Token 60 | { 61 | $this->description = $description; 62 | 63 | return $this; 64 | } 65 | 66 | } 67 | --------------------------------------------------------------------------------