蒼時弦也
蒼時弦也
資深軟體工程師
發表於

使用 Oauth-Plugin 與 Devise 建置 OAuth2 伺服器

其實,這是一篇掃雷文(誤

大約半年前(根據某人開 Issue 的記錄,是 7 個月之前)我也嘗試過安裝 oauth-plugin 來建置 OAuth2 伺服器,但是因為長達好幾小時 400 Bad Request 最後受不了而放棄。

這次,除了順利挑戰 Invalid OAuth Request 之外,從 400 甚至 401 都挑戰的我,終於戰勝了地雷啊!

事不宜遲,讓我們開始進行這一連串的大戰吧!

###前情提要

基本上,就是老爸公司的系統發展到某個程度後,開始有 SSO 的需求。因為系統本身是一個獎金計算的機制,為了搭配購物而在上週死命暴走把精簡版的訂單管理系統加入。

不過,客戶總不可能每個都天生神力自行開發購物車吧?因此就有了從來沒有停過的建構一個購物平台(基於原有系統)的輔助平台功能了(遠目

不過,多重的帳號與重複的資料是我們不樂見的,因此開始尋找 SSO (Single Sign-on) 的幾解決方案。

從 RubyCAS, BrowserID, OpenID, OAuth 都有評估,最後身為萬惡開發人員的我還是判斷 OAuth 最為適合。而 OAuth 2.0 也是我比較希望使用的模式,因此再次開啓失敗之路(誤

###現況分析

因為目前主要的系統是使用 Rails 去進行開發的,所以就不考慮其他語言的實踐,以及 RubyCAS 需要另外建置伺服器的方式(另一方面是再開伺服器除了維護成本外,還有資金的限制,整合在現有系統內會是筆記好的選擇)

而目前 Ruby 大致上呈現 OAuth2 Client > OAuth2 Server 的狀況,相對的之後打算開發輔助平台的 PHP 則呈現 OAuth2 Server > OAuth2 Client 的狀態(此指的是實踐的容易程度)

在眾多 Ruby Gems 裡面,唯獨 oauth-plugin 較為完整(平均 OAuth2 相關 Gem 大多 7 個月沒有更新,因此差異不大)

###伺服器實作

因為會員系統是基於 Devise 的,因此一開始就先考慮要搭配 Devise 實作。

建置上基本上參考 Rails and OAuth-Plugin Part1 這一篇文章

首先針對 Gemfile 追加 gems

1gem 'devise'
2gem 'oauth-plugin', ">= 0.4.1" # 使用最新的 0.4.1 版

接著執行 bundle install 進行安裝

$ bundle install

完成之後建置所需的 Controller / Model / View

$ Rails g devise:install $ Rails g devise User $ Rails g oauth_provider

然後更新資料庫 在這之前可以先修改 db/migrations 的 migrate 資訊,調整成所需的資料表結構

$ rake db:migrate

接下來修改 config/application.rb 加入 OAuth-Plugin 的 Middleware

1require 'oauth/rack/oauth_filter'
2config.middleware.use OAuth::Rack::OAuthFilter

註:上述程式碼不是在第一行/最後加入,而是在再 class Application < Rails::Application 裡面

接下來編輯 User Model ( 前面 Rails g devise User 所產生的,依照你產生的 Model 修改 )

加入以下程式碼(對應 user 的 application 以及產生的 tokens)

1has_many :client_applications
2has_many :tokens, :class_name=>"OauthToken",:order=>"authorized_at desc",:include=>[:client_application]

註: :include => [:client_application] 這部分目前不確定是否適用,不過因為 oauth-plugin 的設計,預設也是無法修改此部分,在 Rails 3.2.13 仍是正常的

在參考文件中,會將 alias 分別放入 oauth_clients_controller.rb 和 oauth_controller.rb

不過這邊我們要改放到 application_controller.rb (後面會提到這個問題)

加入以下程式碼

1alias :logged_in? :user_signed_in?
2alias :loggin_required :authenticate_user!

上面的程式碼其實只是將 Devise 的方法提供給 OAuth-Plugin 而已(因為使用名稱不同,所以提供一個別名)

接著創建一些被 OAuth 保護的 Resources

$ Rails g controller data index

這部分就由各位自行處理

然後編輯這個 resources 加入 before_filter 為保護狀態

加入以下程式碼(相同於 before_filter 的位置)

1oauthenticate # before_filter :oauth_required 的替代,也可傳入 :only => [:show, :index] 等

官方文件上所敘述的before_filter :oauth_require 只會呼叫 OAuth 1.0 的方法,即使順利取得 Access Token 附加在網址上發出請求,也會得到 401 的錯誤,因此改為使用 oauthenticate 就能夠正常(從原始碼來看,在 oauth_require 執行的時候是直接呼叫 oauth 1.0 的 method 而 oauthenticate 則是建立 filter 從 env 判斷版本)

最後依照參考資料補上一些必要的部分。

1def current_user=(user)
2	current_user = user
3end

在原始碼中有使用到這個部分,需要額外補上

以及 Routes 設定中補上 oauth/revoke 的數值(不知道為什麼預設的 route 漏掉了⋯⋯)

1post 'oauth/revoke'

直到這裡,就全部告一段落了!

另外 app/views/oauth_clients 裡面的 _form.html.erb, edit.html.erb, index.html.erb 使用很多舊版或者現在無法使用的語法,記得自行重寫,不然是無法正常運作的!

###客戶端實作

解決完 Server 之後,就要來處理 Client 部分了(這部分使用 PHP 製作)

首先,到 Server 註冊好帳號、並且創建 Application 然後取得 Client Key, Client Secret 之後在繼續吧!

註:OAuth-Plugin 產生的 View 呈現的是 OAuth 1.0 的 Token 網址,後面會提到 2.0 所需使用的部分

首先,先進行 composer 的初始化

$ composer init

或者本地版本(我習慣把 composer 安裝到 /usr/local/bin 裡面方便使用 - MacOSX)

PHP composer.phar init

接著編輯 composer.json

在 require 裡面加入 OAuth Client 的實作

1require: {
2	"lncd/oauth2-client": "*" 
3}

註:這邊是為了測試方便而使用這一個 Client 實際開發時還是建議用 OAuth2-PHP 之類的 Library (以後有機會,會再寫一篇相關的文章討論 OAuth2 PHP Client 的使用心得)

完成後更新套件

$ composer install

或者 update (如果執行過 install)

$ composer update

因為這款 OAuth Client 是以 Provider 為基準的,因此我們要自行時做一個 Provider Class 來進行連接。

我是參考原有的 Provider 中 Github 的改寫

 1<?PHP
 2
 3class MyProvider extends OAuth2\Client\Provider\IdentityProvider
 4{
 5	public $respondType = 'json'; // 我們的伺服器預設為 json 這行可以省略
 6	
 7	public function urlAuthorize() {
 8		return "https://myserver.dev/oauth/authorize";
 9	}
10	
11	public function urlAccessToken() {
12		return "https://myserver.dev/oauth/token"; // OAuth 2.0 使用這個即可,不過得在 rake routes 才會注意到它
13	}
14	
15	// 以下為 Client 本身實作,因為繼承的父類別會呼叫,因此須實做出來
16	public function urlUserDetails(OAuth2\Client\Token\AccessToken $token) {
17		// 這裡假設 Server 已經建立好 user_controller 並且以 OAuth 保護
18		return "https://myserver.dev/user.json?access_token=" . $token;
19	}
20	
21	public function userDetails($response, OAuth2\Client\Token\AccessToken $token)
22	{
23	 	// 預設是建立一個 User Object, 這裡只需看結果所以直接傳回 json_decode 後的資料
24		return $response;
25	}

然後我們建立一個 index.PHP 來連接 OAuth 伺服器

 1<?PHP
 2
 3require 'vendor/autoload.PHP'; // Composer 的 Autoload
 4require 'myProvider.PHP'; // 剛剛建立的 Provider
 5
 6$provider = new MyProvider(Array(
 7	'clientId' => 'YOUR_CLIENT_KEY',
 8	'clientSecret' => 'YOUR_CLIENT_SECRET',
 9	'redirectUri' => 'https://myclient.dev/' // 這是這個 PHP 運行的位置
10));
11
12if(!isset($_GET['code'])) {
13	$provider->authorize(); // 如果沒有取得 authorization code 則先進行認證
14} else {
15	try {
16		$token = $provider->getAccessToken('authorization_code', array('code' => $_GET['code']));
17		
18		try {
19			$user = $provider->getUserDetails($token);
20			
21			// 傳回一些 $user 的資訊
22			
23		} catch (Exception $e) {
24			// 取得資源失敗時的處理
25		}
26		
27	} catch (Exception $e) {
28		// 處理取得 Access Token 失敗時的處理
29	}
30}

如果到此都順利完成,那就是完成 Server (Ruby) / Client (PHP) 的實作了!

據說某人從下午開始弄,弄到晚上十二點才全部解完,白白排了很多原始碼找問題

那麼,祝各位順利~