web-dev-qa-db-fra.com

Portées de passeport de Laravel

Je suis un peu confus sur la partie laropes.

J'ai un modèle d'utilisateur et une table.

Comment puis-je attribuer à un utilisateur le rôle d'utilisateur, de client et/ou d'administrateur?.

J'ai un spa avec vue et laravel api backend. J'utilise https://laravel.com/docs/5.3/passport#consuming-your-api-with-javascript

    Passport::tokensCan([
        'user' => 'User',
        'customer' => 'Customer',
        'admin' => 'Admin',
    ]);

Comment puis-je attribuer quel modèle d'utilisateur a quelle portée?

Ou les champs d'application ne sont-ils pas les mêmes que les rôles?

Comment mettriez-vous cela en œuvre?

Merci d'avance!

21
Jeroen Herczeg

Ou les champs d'application ne sont-ils pas les mêmes que les rôles?

La plus grande différence entre les deux est le contexte auquel ils s'appliquent. Le contrôle d'accès basé sur le rôle (RBAC) régit le contrôle d'accès d'un utilisateur lors de l'utilisation de l'application Web directement, tandis que la portée de Oauth-2 régit l'accès aux ressources de l'API pour un client externe au nom d'un utilisateur.

Comment puis-je attribuer quel modèle d'utilisateur a quelle portée?

En général, Oauth flow demande à un utilisateur (en tant que propriétaire de ressource) d’autoriser un client à effectuer des tâches qu’il peut ou ne peut pas faire en son nom. C’est ce que vous avez appelé scope. Sur l'autorisation réussie, l'étendue demandée par le client sera attribuée au jeton généré et non à l'utilisateur en tant que tel.

En fonction du flux de subvention Oauth que vous choisissez, le client doit inclure la portée de sa demande. Dans le flux d'octroi de code d'autorisation, la portée doit être incluse dans le paramètre de requête HTTP GET lors de la redirection de l'utilisateur sur la page d'autorisation, tandis que dans le flux d'octroi de mot de passe, la portée doit être incluse dans le paramètre de corps HTTP POST pour demander un jeton.

Comment mettriez-vous cela en œuvre?

Ceci est un exemple avec le flux d’octroi de mot de passe, en supposant que vous avez terminé la configuration laravel/passport au préalable.

Définissez les étendues pour les rôles d'administrateur et d'utilisateur. Soyez aussi précis que possible, par exemple: admin peut gérer-order et seul l'utilisateur peut le lire.

// in AuthServiceProvider boot
Passport::tokensCan([
    'manage-order' => 'Manage order scope'
    'read-only-order' => 'Read only order scope'
]);

Préparer le contrôleur REST

// in controller
namespace App\Http\Controllers;

class OrderController extends Controller
{   
    public function index(Request $request)
    {
        // allow listing all order only for token with manage order scope
    }

    public function store(Request $request)
    {
        // allow storing a newly created order in storage for token with manage order scope
    }

    public function show($id)
    {
        // allow displaying the order for token with both manage and read only scope
    }
}

Attribuer la route avec api guard et scope

// in api.php
Route::get('/api/orders', 'OrderController@index')
    ->middleware(['auth:api', 'scopes:manage-order']);
Route::post('/api/orders', 'OrderController@store')
    ->middleware(['auth:api', 'scopes:manage-order']);
Route::get('/api/orders/{id}', 'OrderController@show')
    ->middleware(['auth:api', 'scopes:manage-order, read-only-order']);

Et lors de l'émission d'un jeton, vérifiez d'abord le rôle d'utilisateur et accordez la portée en fonction de ce rôle. Pour ce faire, nous avons besoin d’un contrôleur supplémentaire qui utilise le trait AuthenticatesUsers pour fournir un point de connexion.

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

class ApiLoginController extends Controller
{
    use AuthenticatesUsers;

    protected function authenticated(Request $request, $user)
    {               
        // implement your user role retrieval logic, for example retrieve from `roles` database table
        $role = $user->checkRole();

        // grant scopes based on the role that we get previously
        if ($role == 'admin') {
            $request->request->add([
                'scope' => 'manage-order' // grant manage order scope for user with admin role
            ]);
        } else {
            $request->request->add([
                'scope' => 'read-only-order' // read-only order scope for other user role
            ]);
        }

        // forward the request to the oauth token request endpoint
        $tokenRequest = Request::create(
            '/oauth/token',
            'post'
        );
        return Route::dispatch($tokenRequest);
    }
}

Ajouter une route pour le point de connexion api login

//in api.php
Route::group('namespace' => 'Auth', function () {
    Route::post('login', 'ApiLoginController@login');
});

Au lieu d'effectuer POST sur/oauth/jeton, POST vers le point de terminaison de connexion api que nous avons fourni auparavant

// from client application
$http = new GuzzleHttp\Client;

$response = $http->post('http://your-app.com/api/login', [
    'form_params' => [
        'grant_type' => 'password',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'username' => '[email protected]',
        'password' => 'my-password',
    ],
]);

return json_decode((string) $response->getBody(), true);

En cas d'autorisation réussie, un code d'accès et un élément de régénération basé sur la portée définie précédemment seront émis pour l'application cliente. Conservez-le quelque part et incluez le jeton dans l'en-tête HTTP lors de toute demande adressée à l'API.

// from client application
$response = $client->request('GET', '/api/my/index', [
    'headers' => [
        'Accept' => 'application/json',
        'Authorization' => 'Bearer '.$accessToken,
    ],
]);

L'API devrait maintenant retourner

{"error":"unauthenticated"}

chaque fois qu'un jeton avec privilège under est utilisé pour le point de terminaison restreint consommé.

43
Raymond Lagonda

Implémentez la réponse Raymond Lagonda et cela fonctionne très bien, juste pour faire attention à ce qui suit . Vous devez remplacer certaines méthodes des traits AuthenticatesUsers dans ApiLoginController:

    /**
     * Send the response after the user was authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendLoginResponse(Request $request)
    {
        // $request->session()->regenerate(); // coment this becose api routes with passport failed here.

        $this->clearLoginAttempts($request);

        return $this->authenticated($request, $this->guard()->user())
                ?: response()->json(["status"=>"error", "message"=>"Some error for failes authenticated method"]);

    }

    /**
     * Get the failed login response instance.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\RedirectResponse
     */
    protected function sendFailedLoginResponse(Request $request)
    {
        return response()->json([
                                "status"=>"error", 
                                "message"=>"Autentication Error", 
                                "data"=>[
                                    "errors"=>[
                                        $this->username() => Lang::get('auth.failed'),
                                    ]
                                ]
                            ]);
    }

Si vous avez modifié le champ de connexion: nom d'utilisateur en un champ de nom d'utilisateur personnalisé, par exemple: e_mail. Vous devez affiner la méthode de nom d'utilisateur comme dans votre LoginController . Vous devez également redéfinir et modifier les méthodes: validateLogin, tryLogin, informations d'identification, car une fois la connexion validée, la demande est transférée vers passport et doit s'appeler nom d'utilisateur.

4
Leonardo Jauregui

J'ai réussi à le faire fonctionner avec la solution @RaymondLagonda pour Laravel 5.5 avec Sentinel , mais cela devrait également fonctionner sans Sentinel.

Certaines solutions de classe doivent être écrasées (gardez cela à l’esprit, pour les futures mises à jour), et une certaine protection sera fournie à vos routes d’API (ne pas exposer client_secret par exemple).

La première étape consiste à modifier votre ApiLoginController afin d’ajouter une fonction de construction:

public function __construct(Request $request){
        $oauth_client_id = env('PASSPORT_CLIENT_ID');
        $oauth_client = OauthClients::findOrFail($oauth_client_id);

        $request->request->add([
            'email' => $request->username,
            'client_id' => $oauth_client_id,
            'client_secret' => $oauth_client->secret]);
    }

Dans cet exemple, vous devez définir var ('PASSPORT_CLIENT_ID') dans votre fichier .env et créer le modèle OauthClients. Vous pouvez toutefois le passer en toute sécurité en y ajoutant vos valeurs de test appropriées.

Une chose à noter, c'est que nous définissons la valeur $request->email en nom d'utilisateur, pour nous en tenir à la convention Oauth2.

La deuxième étape est, pour remplacer, la méthode sendLoginResponse qui cause des erreurs comme Session storage not set, nous n'avons pas besoin de sessions ici, car c'est api.

protected function sendLoginResponse(Request $request)
    {
//        $request->session()->regenerate();

        $this->clearLoginAttempts($request);

        return $this->authenticated($request, $this->guard()->user())
            ?: redirect()->intended($this->redirectPath());
    }

La troisième étape consiste à modifier vos méthodes authentifiées comme suggéré par @RaymondLagonda. Vous devez écrire ici votre propre logique et surtout configurer vos portées.

Et la dernière étape (si vous utilisez Sentinel) consiste à modifier AuthServiceProvider. Ajouter 

$this->app->rebinding('request', function ($app, $request) {
            $request->setUserResolver(function () use ($app) {
                 return \Auth::user();
//                return $app['sentinel']->getUser();
            });
        });

juste après $this->registerPolicies(); dans la méthode de démarrage.

Après ces étapes, vous devriez pouvoir faire fonctionner votre API en fournissant un nom d’utilisateur ("ce sera toujours un email, dans cette implémentation"), un mot de passe et un grant_type = "un mot de passe".

À ce stade, vous pouvez ajouter aux étendues des middlewares scopes:... ou scope:... pour protéger vos itinéraires.

J'espère que ça va vraiment aider ...

2
Bart

Je sais que c'est un peu tard, mais si vous utilisez une API backend dans un SPA utilisant la variable CreateFreshApiToken dans le middleware Web, vous pouvez simplement ajouter un middleware 'admin' à votre application:

php artisan make:middleware Admin

Ensuite, dans \App\Http\Middleware\Admin, procédez comme suit:

public function handle($request, Closure $next)
{
    if (Auth::user()->role() !== 'admin') {
        return response(json_encode(['error' => 'Unauthorised']), 401)
            ->header('Content-Type', 'text/json');
    }

    return $next($request);
}

Assurez-vous d'avoir ajouté la méthode role à \App\User pour récupérer le rôle des utilisateurs.

Il ne vous reste plus qu'à enregistrer votre middleware dans app\Http\Kernel.php$routeMiddleware, comme suit:

protected $routeMiddleware = [
    // Other Middleware
    'admin' => \App\Http\Middleware\Admin::class,
];

Et ajoutez cela à votre route dans routes/api.php

Route::middleware(['auth:api','admin'])->get('/customers','Api\CustomersController@index');

Maintenant, si vous essayez d'accéder à l'API sans autorisation, vous recevrez une erreur "401 Unauthorized", que vous pourrez vérifier et gérer dans votre application.

1
craig_h

Avec la solution @RaymondLagonda. Si vous obtenez une erreur d'étendue de classe non trouvée, ajoutez le middleware suivant à la propriété $routeMiddleware de votre fichier app/Http/Kernel.php

'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class, 
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,`

De plus, si vous obtenez l'erreur Type error: Too few arguments to function, vous devriez pouvoir obtenir le $user à partir de la requête, comme ci-dessous. 

(J'utilise laratrust pour gérer les rôles)

public function login(Request $request)
{

    $email = $request->input('username');
    $user = User::where('email','=',$email)->first();

    if($user && $user->hasRole('admin')){
        $request->request->add([
            'scope' => 'manage-everything'
        ]);
    }else{
        return response()->json(['message' => 'Unauthorized'],403);
    }

    $tokenRequest = Request::create(
      '/oauth/token',
      'post'
    );

    return Route::dispatch($tokenRequest);

}
1
Amal Ajith