Laravel – API – Oauth2 架設

安裝認證

php artisan make:auth
php artisan migrate

接著透過 Laravel 生出來的使用者功能,前往 http://domain/register 註冊兩組帳號作為交互運用練習:

  • 一組是開發者帳號,用這個帳號來建立一個應用程式,也就是 client 端,例如這個應用程式是 Mobile APP 會需要連接的 Oauth。
  • 一組帳號是使用者帳號,會使用開發者所建立的 Mobile APP。開發者的 Client 端,會向使用者索取授權與 token。

 

安裝 Passport

composer require laravel/passport

下 artisan 指令,建立資料表。

php artisan migrate

接著下 artisan 指令,預設會建立「個人權限 personal access」與「密碼許可 password grant」

php artisan passport:install

在 App\User 添加 Laravel\Passport\HasApiTokens 特徵,如果你的 model 有需要使用 api token 的輔助方法,都可以加入這項特徵

<?php

namespace App;

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}

接著將路由 Passport::routes() 添加到 AuthServiceProvider 的 boot() 方法

<?php

namespace App\Providers;

use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();
    }
}

最後修改 config/auth.php 的 api driver 為 passport

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],

 

部屬 Passport

建立鑰匙

php artisan passport:keys

 

設定

令牌壽命

在 AuthServiceProvider 指定令牌過期時間 tokensExpireIn() 與重取令牌過期時間 refreshTokensExpireIn()

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::tokensExpireIn(now()->addDays(15));

    Passport::refreshTokensExpireIn(now()->addDays(30));
}

 

發行訪問令牌

管理客戶端

首先,當開發者建立應用程式時,會需要與你的應用程式API交互溝通;而你的應用程式 API ,會需要透過建立一個稱作 “client” 的方式,來註冊開發者的應用程式。

通常,這包括由應用程式提供的「名稱 name」以及使用者批准請求授權後,應用程式所需「返回的 網址 url」。

JSON API

這四種 axios 方法請記得先登入開發者帳號,可以讓開發者對自己開發的 client 做管理。但目前我們僅要用剛剛建立的開發者登入,透過 POST 新增客戶端,記得 GET 之外的方法要加入 CSRF token

<script
  src="https://code.jquery.com/jquery-3.3.1.min.js"
  integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
  crossorigin="anonymous"></script>
<meta name="csrf-token" content="{{ csrf_token() }}">

若是 jQuery 加入方式。axios 請改成特定方式

$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

 

  • GET /oauth/clients 查詢自己擁有的客戶端
    axios.get('/oauth/clients')
        .then(response => {
            console.log(response.data);
        });
  • POST /oauth/clients 新增客戶端
    const data = {
        name: 'Client Name',
        redirect: 'http://example.com/callback'
    };
    
    axios.post('/oauth/clients', data)
        .then(response => {
            console.log(response.data);
        })
        .catch (response => {
            // List errors on response...
        });
  • PUT /oauth/clients 修改自己的某個客戶端
    const data = {
        name: 'New Client Name',
        redirect: 'http://example.com/callback'
    };
    
    axios.put('/oauth/clients/' + clientId, data)
        .then(response => {
            console.log(response.data);
        })
        .catch (response => {
            // List errors on response...
        });
  • DELETE /oauth/clients 刪除自己的某個客戶端
    axios.delete('/oauth/clients/' + clientId)
        .then(response => {
            //
        });

 

請求令牌

當開發者建立一個用戶端以後,這個用戶端 Client 要向使用者請求令牌。下方的用戶端會帶領使用者,導向到授權頁面,使用者允許授權給用戶端,那會返回指定的 redirect_uri:http://example.com/callback 以取得授權碼。

Route::get('/redirect', function () {
    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://example.com/callback',
        'response_type' => 'code',
        'scope' => '',
    ]);

    return redirect('http://your-app.com/oauth/authorize?'.$query);
});

另外,我們可以下 artisan 建立 view 來讓我們自行修改版面

php artisan vendor:publish --tag=passport-views

 

授權碼轉換為訪問令牌

開發者的客戶端 client 取得了授權碼 code,我們要 POST /oauth/token 來轉換為訪問令牌。client_id 與 client_secret 我們可以從資料表中看到,或是發 GET 到 oauth/clients 找到屬於開發者剛建立的 client。

use Illuminate\Http\Request;

Route::get('/callback', function (Request $request) {
    $http = new GuzzleHttp\Client;

    $response = $http->post('http://your-app.com/oauth/token', [
        'form_params' => [
            'grant_type' => 'authorization_code',
            'client_id' => 'client-id',
            'client_secret' => 'client-secret',
            'redirect_uri' => 'http://example.com/callback',
            'code' => $request->code,
        ],
    ]);

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

返回參數包含訪問令牌 access_token, 重取令牌 refresh_token, 令牌過期時間 expires_in。如果要重取令牌可以這樣

$http = new GuzzleHttp\Client;

$response = $http->post('http://your-app.com/oauth/token', [
    'form_params' => [
        'grant_type' => 'refresh_token',
        'refresh_token' => 'the-refresh-token',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'scope' => '',
    ],
]);

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

 

密碼許可令牌 Password Grant Tokens

若之前已經使用 php artisan passport:client 那就不需要下以下指令

php artisan passport:client --password

 

請求令牌

另外透過這個方法,可以用 password_client = 1 的系統身分,來取得會員自己的 access token 與 refresh token。與上述 授權碼轉換為訪問令牌 的差異在於參數 grant_type 使用 password,改用會員的密碼拋送,取得的結果是一樣的。

  • client_id 與 client_secret:注意填入的是資料表 oauth_clients 中 password_client = 1 的編號
  • username 與 password:注意填入的是使用者的 email 與 password(未加密的值不是資料表欄位的值)
$http = new GuzzleHttp\Client;

$response = $http->post('http://your-app.com/oauth/token', [
    'form_params' => [
        'grant_type' => 'password',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'username' => 'taylor@laravel.com',
        'password' => 'my-password',
        'scope' => '',
    ],
]);

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

 

請求所有範圍

在 scope 添加萬用字元「*」。注意,這樣的指定只能在 /oauth/token 方法,且指定 grant_type 為 password 或 client_credentials。

$response = $http->post('http://your-app.com/oauth/token', [
    'form_params' => [
        'grant_type' => 'password',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'username' => 'taylor@laravel.com',
        'password' => 'my-password',
        'scope' => '*',
    ],
]);

 

隱藏式許可令牌

(待寫)

 

客戶端許可證授權令牌

(待寫)

 

個人權限令牌

(待寫)

保護路由

通過中介層

Passport 只要在路由配合使用 auth:api 就可以包含驗證權限令牌 access token 了

Route::get('/user', function () {
    //
})->middleware('auth:api');

傳遞訪問令牌

這個很常用,當我們透過如 JavaScript 發送請求到 Server 必須要夾帶 Access Token,注意 Authorization 參數值 Bearer 後方要保留一個空白,範例如下

const accessToken = '取得的 access token';

// 設置道 header
$.ajaxSetup({
    headers: {
        'Accept' : 'application/json',
        'Authorization' : 'Bearer ' + accessToken
    }
});

// 開始做任何的請求
$.get('/api/user', function (data){
    console.log(data)
});

若用 php 的話

$accessToken = '取得的 access token';
$client = new GuzzleHttp\Client;

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

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

而 Laravel 的路由 /api/user 也務必使用上述的中介層 middleware(‘auth:api’)。

 

令牌範圍

定義範圍

在 AuthServiceProvider 的方法 boot() 添加 Passport::tokensCan(),value 的部分請填上給使用者看得好懂語言

use Laravel\Passport\Passport;

Passport::tokensCan([
    'place-orders' => 'Place orders',
    'check-status' => 'Check order status',
]);

 

預設範圍

這種方式會在當我們在 redirect 需要 scope 參數,設定空白的時候的預設值。

use Laravel\Passport\Passport;

Passport::setDefaultScope([
    'check-status',
    'place-orders',
]);

 

分配範圍到令牌

參數 scope 值使用上述定義的名稱,要用空白分開,例如

Route::get('/redirect', function () {
    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://example.com/callback',
        'response_type' => 'code',
        'scope' => 'place-orders check-status',
    ]);

    return redirect('http://your-app.com/oauth/authorize?'.$query);
});

 

檢查範圍

在 app/Http/Kernel.php 的屬性 $routeMiddleware 添加兩個中介層

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

若要檢查符合權限範圍,中介層的 scope 或 scopes 之間的逗號不可以有空白

Route::get('/orders', function () {
    // 符合全部
})->middleware('scopes:check-status,place-orders');

Route::get('/orders', function () {
    // 符合其中一項
})->middleware('scope:check-status,place-orders');

或是手動判斷

use Illuminate\Http\Request;

Route::get('/orders', function (Request $request) {
    if ($request->user()->tokenCan('place-orders')) {
        //
    }
});

 

發表迴響