关于oauth 2.0:Laravel护照范围

Laravel Passport Scopes

我对laravel示波器部分有些困惑。

我有一个用户模型和表。

如何为用户分配用户,客户和/或管理员的角色。

我有一个带有vue和laravel api后端的SPA。我使用https://laravel.com/docs/5.3/passport#taking-your-api-with-javascript

1
2
3
4
5
    Passport::tokensCan([
        'user' => 'User',
        'customer' => 'Customer',
        'admin' => 'Admin',
    ]);

我如何分配哪个用户模型具有哪个范围?

作用域与角色不同吗?

您将如何实施?

预先感谢!


Or are scopes not the same as roles?

两者之间的最大区别是它们适用的上下文。直接使用Web应用程序时,基于角色的访问控制(RBAC)控制用户的访问控制,而Oauth-2范围代表用户为外部客户端控制对API资源的访问。

How can i assign which user model has which scope(s)?

在一般的Oauth流程中,要求用户(作为资源所有者)授权客户端进行其可以代表自己做或不能做的事情,这些就是您所说的范围。成功授权后,客户端请求的范围将分配给生成的令牌,而不是分配给用户本身。

根据您选择的Oauth授予流程,客户端应在其请求中包括范围。在将用户重定向到授权页面时,在授权代码授予流程中,范围应包含在HTTP GET查询参数中,而在密码授予流程中,必须将范围包含在HTTP POST主体参数中以请求令牌。

How would you implement this?

这是密码授予流程的示例,假设您事先完成了laravel / passport的设置

定义管理员和用户角色的范围。尽可能具体一些,例如:admin可以管理订单,而用户只能读取它。

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

准备REST控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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
    }
}

使用api Guard和scope

分配路由

1
2
3
4
5
6
7
// 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']);

在颁发令牌时,请首先检查用户角色,然后根据该角色授予作用域。为此,我们需要一个额外的控制器,该控制器使用AuthenticatesUsers特性来提供登录端点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
namespace App\\Http\\Controllers\\Auth;

use App\\Http\\Controllers\\Controller;
use Illuminate\\Foundation\\Auth\\AuthenticatesUsers;
use Illuminate\\Http\
equest;
use Illuminate\\Support\\Facades\
oute;

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);
    }
}

为api登录端点添加路由

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

而不是对/ oauth / token路由进行POST,而是对我们之前提供的api登录端点进行POST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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);

在成功授权后,将为客户端应用程序发布基于我们之前定义的范围的access_token和refresh_token。将其保留在某处,并在向API发出请求时将令牌包括在HTTP标头中。

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

API现在应该返回

1
{"error":"unauthenticated"}

每当具有欠特权的令牌用于消耗受限制的端点时。


实施雷蒙德·拉贡达(Raymond Lagonda)的响应,效果很好,只是要注意以下几点。
您需要从ApiLoginController中的AuthenticatesUsers特性中覆盖一些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    /**
     * Send the response after the user was authenticated.
     *
     * @param  \\Illuminate\\Http\
equest  $request
     * @return \\Illuminate\\Http\
esponse
     */
    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\
equest  $request
     * @return \\Illuminate\\Http\
edirectResponse
     */
    protected function sendFailedLoginResponse(Request $request)
    {
        return response()->json([
                               "status"=>"error",
                               "message"=>"Autentication Error",
                               "data"=>[
                                   "errors"=>[
                                        $this->username() => Lang::get('auth.failed'),
                                    ]
                                ]
                            ]);
    }

如果您将login:username字段更改为自定义用户名字段,例如:e_mail。您必须像在LoginController中那样优化用户名方法。
另外,您还必须重新定义和编辑方法:validateLogin,tryLogin,凭据,因为一旦登录验证通过,请求便会转发到passport,并且必须称为用户名。


我已经成功地使用@RaymondLagonda解决方案,将其用于带有Sentinel的Laravel 5.5,但也应该在没有Sentinel的情况下也可以使用。

该解决方案需要重写某些类方法(因此请记住这一点,以备将来更新),并为您的api路由添加一些保护(例如,不公开client_secret)。

第一步是修改您的ApiLoginController以添加构造函数:

1
2
3
4
5
6
7
8
9
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]);
    }

在此示例中,您需要在.env中定义var(\\'PASSPORT_CLIENT_ID \\')并创建OauthClients模型,但是可以通过在此处放置适当的测试值来安全地跳过此过程。

要注意的一件事是,我们正在将$request->email值设置为username,只是为了遵守Oauth2约定。

第二步是要覆盖导致错误的sendLoginResponse方法,例如Session storage not set,我们这里不需要会话,因为它是api。

1
2
3
4
5
6
7
8
9
protected function sendLoginResponse(Request $request)
    {
//        $request->session()->regenerate();

        $this->clearLoginAttempts($request);

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

第三步是按照@RaymondLagonda的建议修改您的身份验证方法。您需要在此处编写自己的逻辑,尤其是配置您的范围。

最后一步(如果您使用的是Sentinel)是修改AuthServiceProvider。添加

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

在启动方法中$this->registerPolicies();之后。

完成这些步骤后,您应该可以通过提供用户名(\\'在该实现中,这始终是电子邮件\\'),密码和grant_type = \\'password \\'

这时,您可以添加到中间件作用域scopes:...scope:...以保护您的路由。

我希望这会对您有所帮助...


我知道这有点晚了,但是如果您使用Web中间件中的CreateFreshApiToken在SPA中使用后端API,那么您只需在应用中添加\\'admin \\'中间件即可:<铅>

php artisan make:middleware Admin

然后在\\App\\Http\\Middleware\\Admin中执行以下操作:

1
2
3
4
5
6
7
8
9
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);
}

确保已将role方法添加到\\App\\User中以检索用户角色。

现在您要做的就是在app\\Http\\Kernel.php $routeMiddleware中注册中间件,如下所示:

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

并将其添加到routes/api.php

中的路由中

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

现在,如果您尝试未经许可访问api,则会收到" 401未经授权"错误,您可以在应用程序中进行检查和处理。


使用@RaymondLagonda解决方案。如果遇到找不到类作用域的错误,请将以下中间件添加到app/Http/Kernel.php文件的$routeMiddleware属性中:

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

此外,如果遇到错误Type error: Too few arguments to function,则应该能够从如下所示的请求中获取$user

(我正在使用laratrust来管理角色)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);

}

感谢您,这个问题让我心烦了好一阵子!我采用了雷蒙德·拉贡达(Raymond Lagonda)的解决方案,使用内置的速率限制,使用单个thirdparty客户端(或根据需要进行更多定制),针对Laravel 5.6对其进行了一些自定义,同时仍为每个用户提供了权限列表(范围)。

  • 使用Laravel Passport password授予并遵循Oauth流程
  • 使您能够为不同用户设置角色(范围)
  • 不要公开/释放客户ID或客户机密,只有用户的用户名(电子邮件)和密码,几乎是密码授权,减去客户/授权的东西

底部的例子

routes/api.php

1
2
3
    Route::group(['namespace' => 'ThirdParty', 'prefix' => 'thirdparty'], function () {
        Route::post('login', 'ApiLoginController@login');
    });

ThirdParty/ApiLoginController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<?php

namespace App\\Http\\Controllers\\ThirdParty;

use Hash;
use App\\User;
use App\\ThirdParty;
use Illuminate\\Http\
equest;
use Illuminate\\Support\\Facades\
oute;
use App\\Http\\Controllers\\Controller;
use Illuminate\\Foundation\\Auth\\AuthenticatesUsers;

class ApiLoginController extends Controller
{
    use AuthenticatesUsers;

    /**
     * Thirdparty login method to handle different
     * clients logging in for different reasons,
     * we assign each third party user scopes
     * to assign to their token, so they
     * can perform different API tasks
     * with the same token.
     *
     * @param  Request $request
     * @return Illuminate\\Http\
esponse
     */
    protected function login(Request $request)
    {
        if ($this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            return $this->sendLockoutResponse($request);
        }

        $user = $this->validateUserLogin($request);

        $client = ThirdParty::where(['id' => config('thirdparties.client_id')])->first();

        $request->request->add([
            'scope' => $user->scopes,
            'grant_type' => 'password',
            'client_id' => $client->id,
            'client_secret' => $client->secret
        ]);

        return Route::dispatch(
            Request::create('/oauth/token', 'post')
        );
    }

    /**
     * Validate the users login, checking
     * their username/password
     *
     * @param  Request $request
     * @return User
     */
    public function validateUserLogin($request)
    {
        $this->incrementLoginAttempts($request);

        $username = $request->username;
        $password = $request->password;

        $user = User::where(['email' => $username])->first();

        abort_unless($user, 401, 'Incorrect email/password.');

        $user->setVisible(['password']);

        abort_unless(Hash::check($password, $user->password), 401, 'Incorrect email/password.');

        return $user;
    }
}

config/thirdparties.php

1
2
3
4
5
<?php

return [
    'client_id' => env('THIRDPARTY_CLIENT_ID', null),
];

ThirdParty.php

1
2
3
4
5
6
7
8
9
10
<?php

namespace App;

use Illuminate\\Database\\Eloquent\\Model;

class ThirdParty extends Model
{
    protected $table = 'oauth_clients';
}

.env

1
2
## THIRDPARTIES
THIRDPARTY_CLIENT_ID=3

php artisan make:migration add_scope_to_users_table --table=users

1
2
3
4
5
6
7
8
        // up
        Schema::table('users', function (Blueprint $table) {
            $table->text('scopes')->nullable()->after('api_access');
        });
        // down
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('scopes');
        });

(注意:api_access是一个标志,它决定用户是否可以登录到应用程序的网站/前端部分,以查看仪表板/记录等),

routes/api.php

1
2
3
Route::group(['middleware' => ['auth.client:YOUR_SCOPE_HERE', 'throttle:60,1']], function () {
    ...routes...
});

MySQL - Users scopes

1
2
3
INSERT INTO `users` (`id`, `created_at`, `updated_at`, `name`, `email`, `password`, `remember_token`, `api_access`, `scopes`)
VALUES
    (5, '2019-03-19 19:27:08', '2019-03-19 19:27:08', '', '[email protected]', 'YOUR_HASHED_PASSWORD', NULL, 1, 'YOUR_SCOPE_HERE ANOTHER_SCOPE_HERE');

MySQL - ThirdParty Oauth Client

1
2
3
INSERT INTO `oauth_clients` (`id`, `user_id`, `name`, `secret`, `redirect`, `personal_access_client`, `password_client`, `revoked`, `created_at`, `updated_at`)
VALUES
    (3, NULL, 'Thirdparty Password Grant Client', 'YOUR_SECRET', 'http://localhost', 0, 1, 0, '2019-03-19 19:12:37', '2019-03-19 19:12:37');

cURL - Logging in/requesting a token

1
2
3
4
5
6
curl -X POST \\
  http://site.localhost/api/v1/thirdparty/login \\
  -H 'Accept: application/json' \\
  -H 'Accept-Charset: application/json' \\
  -F [email protected] \\
  -F password=YOUR_UNHASHED_PASSWORD
1
2
3
4
5
6
{
   "token_type":"Bearer",
   "expires_in": 604800,
   "access_token":"eyJ0eXAiOiJKV1QiLCJhbGciO...",
   "refresh_token":"def502008a75cd2cdd0dad086..."
}

正常使用长寿命的access_token / refresh_token!

Accessing forbidden scope

1
2
3
4
5
6
7
8
9
{
   "data": {
       "errors":"Invalid scope(s) provided."
    },
   "meta": {
       "code": 403,
       "status":"FORBIDDEN"
    }
}