Junx
14 min readDec 28, 2017

OAuth原理與Laravel Passport實作(2)

Laravel Passport 建置OAuth Service與Client

PS: Laravel版本為5.4,本篇略過.env與vhost等設定

首先準備兩個Laravel專案分別為oauth_service與oauth_client

  • oauth_service當作Authorization Server與Resource Server
  • oauth_client當作client

oauth_service 網址為http://service.oauth.test/

oauth_client 網址為http://client.oauth.test/

進到oauth_service專案根目錄

cd oauth_service

安裝Laravel OAuth套件

composer require laravel/passport

在config/app.php $providers中新增

Laravel\Passport\PassportServiceProvider::class

加入Laravel預設Auth

php artisan make:auth

執行migrate

php artisan migrate

如果出現以下Error,請在app/Providers/AuthServiceProvider.php中加入以下,再清空DB再執行

Schema::defaultStringLength(191);

[Illuminate\Database\QueryException]
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table
users add unique users_email_unique(email))

[PDOException]
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes

執行passport

php artisan passport:install

會產生token加密金鑰

產生 Resource Owner Credentials Grant Flow 客戶端

產生 Client Credentials Grant Flow客戶端

修改User.php (app/User.php)

加入use Laravel\Passport\HasApiTokens; traits

在app/Providers/AuthServiceProvider.php

加入Passport::routes();

修改config/auth.php

guards 陣列中的 api driver 修改成 passport

Laravel 內建已配置用Vue.js寫好的前端OAuth註冊畫面

php artisan vendor:publish — tag=passport-components

打開resources/assets/js/app.js

Vue.component( ‘passport-clients’, require(‘./components/passport/Clients.vue’));Vue.component( ‘passport-authorized-clients’, require(‘./components/passport/AuthorizedClients.vue’));Vue.component( ‘passport-personal-access-tokens’, require(‘./components/passport/PersonalAccessTokens.vue’));

打開resources/assets/view/home.blade.php修改為以下

@extends('layouts.app')@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<passport-clients></passport-clients>
<passport-authorized-clients></passport-authorized-clients>
<passport-personal-access-tokens></passport-personal-access-tokens>
</div>
</div>
</div>
@endsection

安裝node 模組(需要安裝Node.js)

npm install

編譯assets

npm run dev

http://service.oauth.test/register註冊帳號並登入

就會看到OAuth頁面

如果畫面是白色代表Browser把js cache住,打開resources/assets/view/layouts/app.blade.php對js/app.js加上版本號

點擊Create New Client 向OAuth註冊一個服務

Redirect URL 填回傳Authorization Grant網址

http://client.oauth.test/authorize

就會拿到Client ID與Secret

這邊叫Client ID但在Spec 2.2. Client Identifier中叫Client Identifier,兩者是一樣的意思,Spec中也未規定Client ID規則,只要是唯一字串就好

切到oauth_client專案

cd oauth_client

安裝guzzlehttp/guzzle

composer require guzzlehttp/guzzle

打開routes/web.php增加以下route

Route::get('/authorize', function () {
$query = http_build_query([
'client_id' => '3',
'redirect_uri' => 'http://client.oauth.test/token',
'response_type' => 'code',
'scope' => 'Email',
'state' => '1234',
]);

return redirect('http://service.oauth.test/oauth/authorize?' . $query);
});

Route::get('/token', function (Request $request) {
$http = new GuzzleHttp\Client;
$response = $http->post('http://service.oauth.test/oauth/token', [
'form_params' => [
'grant_type' => 'authorization_code',
'redirect_uri' => 'http://client.oauth.test/token',
'code' => $request->code,
],
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Basic ' . base64_encode('3:yJfcjtOfmIoJfNuqAPiFXDzGHHvajCToSIwxWiTr'),
],
]);

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

client_id與client_secret就是OAuth註冊完成所給的

/authorize : 代表Authorization Request

/token: 接收Authorization Grant並向Authorization Server取Access Token

切到oauth_service專案

cd oauth_service

到app/Providers/AuthServiceProvider.php加上

Passport::tokensCan([
'Profile' => 'Access your profile',
'Email' => 'Access your Email',
]);
Passport::tokensExpireIn(Carbon::now()->addMinute(60));Passport::refreshTokensExpireIn(Carbon::now()->addDays(1));

profile 個資的存取權限

Email 電子郵件存取權限

限制Access Token expires為1小時

限制Refresh Token在一天內都可以刷新Access Token

PS: 默認Access Token expires無期限

到app/Http/Kernel.php加上

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

scope : Access Token有任意指定Scope的即可通過

scopes : Access Token指定Scope需都匹配才可通過

打開http://client.oauth.test/authorize,可以看到client對Resource Owner發出一個Authorization Request,內容為要求對Email的授權,此時Resource Owner決定是否授權,授權後就可以拿到Access Token等資料,這中間Client取得Authorization Grant再對Authorization Server請求Authorization Server這過程Resource Owner不可見的,由後端自行處理

打開routes/api.php修改成以下

Route::group(['prefix' => '/user', 'middleware' => 'auth:api'], function (){    Route::get('/profile', function (Request $request) {
return $request->user()->toArray();
})->middleware('scope:Profile');
Route::get('/email', function (Request $request) {
return $request->user()->email;
})->middleware('scope:Email');
});

http://service.oauth.test/api/user/email 取得user email但限制需有email權限

http://service.oauth.test/api/user/profile 取得user profile但限制需有profile權限

Access Token有email權限成功取email

Access Token沒有profile權限

以上就完成簡易的Laravel OAuth,後續會討論到上方提到的route參數

OAuth 2.0 Client 認證

在上一章節可以看到Client向OAuth註冊完成後會拿到Client ID與Secret,這兩組就是用來給Authorization Server認證Client所使用的

認證方式分為兩種,如下

  1. HTTP Basic Auth (推薦)

用 HTTP Basic Auth 來認證(參考 RFC 2617

Client ID為3

Secret為yJfcjtOfmIoJfNuqAPiFXDzGHHvajCToSIwxWiTr

步驟:

  • 將 Client ID與Secret 連起來,中間用冒號 : 分開
3:yJfcjtOfmIoJfNuqAPiFXDzGHHvajCToSIwxWiTr
  • 用base64編碼(以php為例)
base64_encode('3:yJfcjtOfmIoJfNuqAPiFXDzGHHvajCToSIwxWiTr')

PS: base64只是一種編碼不是加密,所以請一定要走HTTPS

  • 根據HTTP Basic Auth規定前綴要加上Basic並一個space
Basic Mzp5SmZjanRPZm1Jb0pmTnVxQVBpRlhEekdISHZhakNUb1NJd3hXaVRy
  • Headers用Authorization
Authorization: Basic Mzp5SmZjanRPZm1Jb0pmTnVxQVBpRlhEekdISHZhakNUb1NJd3hXaVRy

Laravel版本

2. 使用POST發送(不推薦)

在無法使用 HTTP Basic Auth或其他 HTTP Authentication 方式的 Client 才使用

POST /token HTTP/1.1
Host: http://service.oauth.test/
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&redirect_uri=http://client.oauth.test/token&client_id=3&client_secret=yJfcjtOfmIoJfNuqAPiFXDzGHHvajCToSIwxWiTr&code=1234

Laravel 版本

參考Spec 2.3.1. Client Password

下一篇OAuth原理與Laravel Passport實作(3)