clone repo

This commit is contained in:
Michal Sieciechowicz 2021-03-01 10:04:25 +01:00
commit f19181c63a
9 changed files with 787 additions and 0 deletions

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
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]
indent_style = space
indent_size = 2

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/vendor/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Nomad NT
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.

355
README.md Normal file
View File

@ -0,0 +1,355 @@
<p align="center"><img src="https://laravel.com/assets/img/components/logo-passport.svg"></p>
[![Total Downloads](https://poser.pugx.org/nomadnt/lumen-passport/downloads)](https://packagist.org/packages/nomadnt/lumen-passport)
[![Latest Stable Version](https://poser.pugx.org/nomadnt/lumen-passport/v/stable)](https://packagist.org/packages/nomadnt/lumen-passport)
[![License](https://poser.pugx.org/nomadnt/lumen-passport/license)](https://packagist.org/packages/nomadnt/lumen-passport)
# Lumen Passport
Lumen porting of Laravel Passport.
The idea come from https://github.com/dusterio/lumen-passport but try to make it transparent with original laravel passport
## Dependencies
* PHP >= 7.3.0
* Lumen >= 8.0
## Installation
First of all let's install Lumen Framework if you haven't already.
```sh
composer create-project --prefer-dist laravel/lumen lumen-app && cd lumen-app
```
Then install Lumen Passport (it will fetch Laravel Passport along):
```sh
composer require nomadnt/lumen-passport
```
## Configuration
Generate your APP_KEY and update .env with single command
```sh
sed -i "s|\(APP_KEY=\)\(.*\)|\1$(openssl rand -base64 24)|" .env
```
Configure your database connection (ie to use SQLite)
This is how your .env file should looking after the changes
```env
APP_NAME=Lumen
APP_ENV=local
APP_KEY=<my-super-strong-api-key>
APP_DEBUG=true
APP_URL=http://localhost:8000
APP_TIMEZONE=UTC
LOG_CHANNEL=stack
LOG_SLACK_WEBHOOK_URL=
DB_CONNECTION=sqlite
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
```
Copy the Lumen configuration folder to your project
```sh
cp -a vendor/laravel/lumen-framework/config config
```
Update `guards` and `provider` section of your config/auth.php to match Passport requirements
```php
<?php
return [
...
'guards' => [
'api' => ['driver' => 'passport', 'provider' => 'users']
],
...
'providers' => [
'users' => ['driver' => 'eloquent', 'model' => \App\Models\User::class]
]
...
];
```
You need to change a little the `bootstrap/app.php` file doing the following:
```php
<?php
...
// enable facades
$app->withFacades();
// enable eloquent
$app->withEloquent();
...
$app->configure('app');
// initialize auth configuration
$app->configure('auth');
...
// enable auth and throttle middleware
$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
'throttle' => Nomadnt\LumenPassport\Middleware\ThrottleRequests::class
]);
...
// register required service providers
// $app->register(App\Providers\AppServiceProvider::class);
$app->register(App\Providers\AuthServiceProvider::class);
$app->register(Laravel\Passport\PassportServiceProvider::class);
// $app->register(App\Providers\EventServiceProvider::class);
...
```
Create database.sqlite
```sh
touch database/database.sqlite
```
Lauch the migrations
```sh
php artisan migrate
```
Install Laravel passport
```sh
# Install encryption keys and other necessary stuff for Passport
php artisan passport:install
```
The previous command should give back to you an output similar to this:
```sh
Encryption keys generated successfully.
Personal access client created successfully.
Client ID: 1
Client secret: BxSueZnqimNTE0r98a0Egysq0qnonwkWDUl0KmE5
Password grant client created successfully.
Client ID: 2
Client secret: VFWuiJXTJhjb46Y04llOQqSd3kP3goqDLvVIkcIu
```
## Registering Routes
Now is time to register the passport routes necessary to issue access tokens and revoke access tokens, clients, and personal access tokens.
To do this open you `app/Providers/AuthServiceProvider.php` and change the `boot` function to reflect the example below.
```php
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Carbon;
// don't forget to include Passport
use Nomadnt\LumenPassport\Passport;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Boot the authentication services for the application.
*
* @return void
*/
public function boot()
{
// register passport routes
Passport::routes();
// change the default token expiration
Passport::tokensExpireIn(Carbon::now()->addDays(15));
// change the default refresh token expiration
Passport::refreshTokensExpireIn(Carbon::now()->addDays(30));
}
}
```
## User model
Make sure your user model uses Passport's `HasApiTokens` trait, eg.:
```php
<?php
namespace App;
use Illuminate\Auth\Authenticatable;
use Laravel\Passport\HasApiTokens;
use Laravel\Lumen\Auth\Authorizable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
class User extends Model implements AuthenticatableContract, AuthorizableContract
{
use HasApiTokens, Authenticatable, Authorizable;
// rest of the model
}
```
## Access Token Events
### Prune and/or Revoke tokens
If you want to revoke or purge tokens on event based you have to create related Listeners and
register on your `app/Http/Providers/EventServiceProvider.php` istead of using deprecated properties
`Passport::$revokeOtherTokens = true;` and `Passport::$pruneRevokedTokens = true;`
First you need to make sure that `EventServiceProvider` is registered on your `bootstrap/app.php`
```php
<?php
...
// $app->register(App\Providers\AppServiceProvider::class);
$app->register(App\Providers\AuthServiceProvider::class);
$app->register(Laravel\Passport\PassportServiceProvider::class);
$app->register(App\Providers\EventServiceProvider::class);
...
```
Then you need to listen for `AccessTokenCreated` event and register your required listeners
```php
<?php
namespace App\Providers;
use Laravel\Lumen\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'Laravel\Passport\Events\AccessTokenCreated' => [
'App\Listeners\RevokeOtherTokens',
'App\Listeners\PruneRevokedTokens',
]
];
}
```
Create the `app/Listeners/RevokeOtherTokens.php` file and put the following content
```php
<?php
namespace App\Listeners;
use Laravel\Passport\Events\AccessTokenCreated;
use Laravel\Passport\Token;
class RevokeOtherTokens
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param \App\Events\OrderShipped $event
* @return void
*/
public function handle(AccessTokenCreated $event)
{
Token::where(function($query) use($event){
$query->where('user_id', $event->userId);
$query->where('id', '<>', $event->tokenId);
})->revoke();
}
}
```
Create the `app/Listeners/PruneRevokedTokens.php` file and put the following content
```php
<?php
namespace App\Listeners;
use Laravel\Passport\Events\AccessTokenCreated;
use Laravel\Passport\Token;
class PruneRevokedTokens
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param \App\Events\AccessTokenCreated $event
* @return void
*/
public function handle(AccessTokenCreated $event)
{
Token::where(function($query) use($event){
$query->where('user_id', $event->userId);
$query->where('id', '<>', $event->tokenId);
$query->where('revoked', true);
})->delete();
}
}
```

27
composer.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "nomadnt/lumen-passport",
"description": "Lumen porting of Laravel Passport",
"keywords": ["php","lumen","laravel passport"],
"homepage": "https://github.com/nomadnt/lumen-passport",
"license": "MIT",
"authors": [
{
"name": "Filippo Sallemi",
"email": "fsallemi@nomadnt.com",
"homepage": "https://nomadnt.com"
}
],
"type": "library",
"require": {
"php": "^7.3.0",
"laravel/passport": "^10.1.0"
},
"autoload": {
"psr-4": {
"Nomadnt\\LumenPassport\\": "src/"
},
"files": [
"src/helpers.php"
]
}
}

View File

@ -0,0 +1,192 @@
<?php namespace Nomadnt\LumenPassport\Middleware;
use Closure;
use RuntimeException;
use Illuminate\Support\Str;
use Illuminate\Cache\RateLimiter;
use Illuminate\Support\InteractsWithTime;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
class ThrottleRequests
{
use InteractsWithTime;
/**
* The rate limiter instance.
*
* @var \Illuminate\Cache\RateLimiter
*/
protected $limiter;
/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
* @return void
*/
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int|string $maxAttempts
* @param float|int $decayMinutes
* @return mixed
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
$maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts);
if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
throw $this->buildException($key, $maxAttempts);
}
$this->limiter->hit($key, $decayMinutes);
$response = $next($request);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}
/**
* Resolve the number of attempts if the user is authenticated or not.
*
* @param \Illuminate\Http\Request $request
* @param int|string $maxAttempts
* @return int
*/
protected function resolveMaxAttempts($request, $maxAttempts)
{
if (Str::contains($maxAttempts, '|')) {
$maxAttempts = explode('|', $maxAttempts, 2)[$request->user() ? 1 : 0];
}
return (int) $maxAttempts;
}
/**
* Resolve request signature.
*
* @param \Illuminate\Http\Request $request
* @return string
* @throws \RuntimeException
*/
protected function resolveRequestSignature($request)
{
if ($user = $request->user()) {
return sha1($user->getAuthIdentifier());
}
if ($route = $request->route()) {
return sha1($request->getHost().'|'.$request->ip());
//return sha1($request->method().'|'.$request->getHost().'|'.$request->ip());
}
throw new RuntimeException(
'Unable to generate the request signature. Route unavailable.'
);
}
/**
* Create a 'too many attempts' exception.
*
* @param string $key
* @param int $maxAttempts
* @return \Symfony\Component\HttpKernel\Exception\HttpException
*/
protected function buildException($key, $maxAttempts)
{
$retryAfter = $this->getTimeUntilNextRetry($key);
$headers = $this->getHeaders(
$maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);
return new HttpException(
429, 'Too Many Attempts.', null, $headers
);
}
/**
* Get the number of seconds until the next retry.
*
* @param string $key
* @return int
*/
protected function getTimeUntilNextRetry($key)
{
return $this->limiter->availableIn($key);
}
/**
* Add the limit header information to the given response.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* @param int $maxAttempts
* @param int $remainingAttempts
* @param int|null $retryAfter
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null)
{
$response->headers->add(
$this->getHeaders($maxAttempts, $remainingAttempts, $retryAfter)
);
return $response;
}
/**
* Get the limit headers information.
*
* @param int $maxAttempts
* @param int $remainingAttempts
* @param int|null $retryAfter
* @return array
*/
protected function getHeaders($maxAttempts, $remainingAttempts, $retryAfter = null)
{
$headers = [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
];
if (! is_null($retryAfter)) {
$headers['Retry-After'] = $retryAfter;
$headers['X-RateLimit-Reset'] = $this->availableAt($retryAfter);
}
return $headers;
}
/**
* Calculate the number of remaining attempts.
*
* @param string $key
* @param int $maxAttempts
* @param int|null $retryAfter
* @return int
*/
protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
{
if (is_null($retryAfter)) {
return $this->limiter->retriesLeft($key, $maxAttempts);
}
return 0;
}
}

33
src/Passport.php Normal file
View File

@ -0,0 +1,33 @@
<?php
namespace Nomadnt\LumenPassport;
use Illuminate\Support\Facades\Route;
use Laravel\Passport\Passport as LaravelPassport;
class Passport extends LaravelPassport
{
/**
* 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));
});
}
}

128
src/RouteRegistrar.php Normal file
View File

@ -0,0 +1,128 @@
<?php
namespace Nomadnt\LumenPassport;
use Laravel\Lumen\Routing\Router;
use Laravel\Passport\RouteRegistrar as Registrar;
class RouteRegistrar extends Registrar
{
/**
* Create a new route registrar instance.
*
* @param \Illuminate\Contracts\Routing\Registrar $router
* @return void
*/
public function __construct(Router $router)
{
$this->router = $router;
}
/**
* Register the routes needed for authorization.
*
* @return void
*/
public function forAuthorization()
{
$this->router->group(['middleware' => ['auth']], function ($router) {
$router->get('/authorize', [
'uses' => 'AuthorizationController@authorize',
]);
$router->post('/authorize', [
'uses' => 'ApproveAuthorizationController@approve',
]);
$router->delete('/authorize', [
'uses' => 'DenyAuthorizationController@deny',
]);
});
}
/**
* Register the routes for retrieving and issuing access tokens.
*
* @return void
*/
public function forAccessTokens()
{
$this->router->post('/token', [
'uses' => 'AccessTokenController@issueToken',
'middleware' => 'throttle',
]);
$this->router->group(['middleware' => ['auth']], function ($router) {
$router->get('/tokens', [
'uses' => 'AuthorizedAccessTokenController@forUser',
]);
$router->delete('/tokens/{token_id}', [
'uses' => 'AuthorizedAccessTokenController@destroy',
]);
});
}
/**
* Register the routes needed for refreshing transient tokens.
*
* @return void
*/
public function forTransientTokens()
{
$this->router->post('/token/refresh', [
'middleware' => ['auth'],
'uses' => 'TransientTokenController@refresh',
]);
}
/**
* Register the routes needed for managing clients.
*
* @return void
*/
public function forClients()
{
$this->router->group(['middleware' => ['auth']], function ($router) {
$router->get('/clients', [
'uses' => 'ClientController@forUser',
]);
$router->post('/clients', [
'uses' => 'ClientController@store',
]);
$router->put('/clients/{client_id}', [
'uses' => 'ClientController@update',
]);
$router->delete('/clients/{client_id}', [
'uses' => 'ClientController@destroy',
]);
});
}
/**
* Register the routes needed for managing personal access tokens.
*
* @return void
*/
public function forPersonalAccessTokens()
{
$this->router->group(['middleware' => ['auth']], function ($router) {
$router->get('/scopes', [
'uses' => 'ScopeController@all',
]);
$router->get('/personal-access-tokens', [
'uses' => 'PersonalAccessTokenController@forUser',
]);
$router->post('/personal-access-tokens', [
'uses' => 'PersonalAccessTokenController@store',
]);
$router->delete('/personal-access-tokens/{token_id}', [
'uses' => 'PersonalAccessTokenController@destroy',
]);
});
}
}

14
src/helpers.php Normal file
View File

@ -0,0 +1,14 @@
<?php
if (! function_exists('config_path')) {
/**
* Get the path to the configuration folder.
*
* @param string $path
* @return string
*/
function config_path($path = '')
{
return app()->getConfigurationPath().($path ? '/'.$path : $path);
}
}