Yii 有不少 extension 可使用,在查看了 Yii 官網上提供的與 OAuth 相關的擴展後,發現了幾個 OAuth2 的客戶端擴展,可是並無找到能夠做爲 OAuth2 Server 的擴展。由於 Yii 是組織良好的易於擴展的框架,因此徹底能夠集成其它的 PHP OAuth2 Server 實現方案。在 OAuth.net/2/ 官網上,提供了幾個 PHP 實現的 OAuth2 Server。這裏使用第一個 OAuth2-Server-php 來做爲 Yii 框架的 OAuth2 Server 擴展,須要進行一些必要的整合操做,主要是編寫一個類來接受 client 訪問和頒發 access_token 等。
第一部分: 數據庫準備
OAuth2-Server-php 使用的數據庫結構採用 Github 上的 oauth2-server-php README.md 提供的表結構(Schema),一共有五張表:
mysql> show tables;
+--------------------------+
| Tables_in_oauth2 |
+--------------------------+
| oauth_access_token |
| oauth_authorization_code |
| oauth_client |
| oauth_refresh_token |
| user |
+--------------------------+
5 rows in set (0.00 sec)
各表的名字說明了表中存取的內容,表名可自定義,自定義位置爲:OAuth2/Storage/Pdo.php 48行的 config 數組中,由於這裏採用的是 mysql 數據庫,因此須要修改的是 Pdo,如果採用其它的存儲方案,如 Redis,則自行修改對應文件便可。注意這裏的數據庫名稱是都是單數形式。
使用如下 sql 語句建立這5個表,並添加一個測試 client:
###############################
### oauth2 tables
###############################
drop table if exists `oauth_client`;
drop table if exists `oauth_access_token`;
drop table if exists `oauth_authorization_code`;
drop table if exists `oauth_refresh_token`;
drop table if exists `user`;
CREATE TABLE `oauth_client` (
`client_id` VARCHAR(80) NOT NULL,
`client_secret` VARCHAR(80) NOT NULL,
`redirect_uri` VARCHAR(2000) NOT NULL,
CONSTRAINT client_id_pk PRIMARY KEY (client_id)
);
CREATE TABLE `oauth_access_token` (
`access_token` VARCHAR(40) NOT NULL,
`client_id` VARCHAR(80) NOT NULL,
`user_id` VARCHAR(255),
`expires` TIMESTAMP NOT NULL,
`scope` VARCHAR(2000),
CONSTRAINT access_token_pk PRIMARY KEY (access_token)
);
CREATE TABLE `oauth_authorization_code` (
`authorization_code` VARCHAR(40) NOT NULL,
`client_id` VARCHAR(80) NOT NULL,
`user_id` VARCHAR(255),
`redirect_uri` VARCHAR(2000),
`expires` TIMESTAMP NOT NULL,
`scope` VARCHAR(2000),
CONSTRAINT auth_code_pk PRIMARY KEY (authorization_code)
);
CREATE TABLE `oauth_refresh_token` (
`refresh_token` VARCHAR(40) NOT NULL,
`client_id` VARCHAR(80) NOT NULL,
`user_id` VARCHAR(255),
`expires` TIMESTAMP NOT NULL,
`scope` VARCHAR(2000),
CONSTRAINT refresh_token_pk PRIMARY KEY (refresh_token)
);
--
CREATE TABLE `user` (
`user_id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(2000),
`first_name` VARCHAR(255),
`last_name` VARCHAR(255),
CONSTRAINT user_pk PRIMARY KEY (user_id)
);
-- test data
INSERT INTO oauth_client (client_id, client_secret, redirect_uri)
VALUES ("testclient", "testpass", "http://fake/");
INSERT INTO user (username, password, first_name, last_name)
VALUES ('rereadyou', '8551be07bab21f3933e8177538d411e43b78dbcc', 'bo', 'zhang');
第二部分: 認證方案及實現
OAuth2 RFC 6749 規範提供了四種基本認證方案,如下針對這四種認證方案以及它們在本實現中的使用方式進行分別說面。
第一種認證方式: Authorization Code Grant (受權碼認證)
受權碼經過使用受權服務器作爲客戶端與資源全部者的中介而得到。客戶端不是直接從資源全部者請求受權,而是引導資源全部者至受權服務器(由在RFC2616中定義的用戶代理),受權服務器以後引導資源全部者帶着受權碼回到客戶端。
在引導資源全部者攜帶受權碼返回客戶端前,受權服務器會鑑定資源全部者身份並得到其受權。因爲資源全部者只與受權服務器進行身份驗證,因此資源全部者的憑據不須要與客戶端分享。
受權碼提供了一些重要的安全益處,例如驗證客戶端身份的能力,以及向客戶端直接的訪問令牌的傳輸而非經過資源全部者的用戶代理來傳送它而潛在暴露給他人(包括資源全部者)。
受權碼許可類型用於得到訪問令牌和刷新令牌並未機密客戶端進行了優化。因爲這是一個基於重定向的流程,客戶端必須可以與資源全部者的用戶代理(一般是Web瀏覽器)進行交互並可以接收來自受權服務器的傳入請求(經過重定向)。
Authorization Code Grant 過程(又稱爲 Web Server Flow) 參見以下:
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| +----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent +----(B)-- User authenticates --->| Server |
| | | |
| +----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
注:說明步驟(A)、(B)和(C)的直線由於經過用戶代理而被分爲兩部分。
圖1:受權碼流程
在圖1中所示的流程包括如下步驟:
(A)客戶端經過向受權端點引導資源全部者的用戶代理開始流程。客戶端包括它的客戶端標識、請求範圍、本地狀態和重定向URI,一旦訪問被許可(或拒絕)受權服務器將傳送用戶代理回到該URI。
(B)受權服務器驗證資源擁有者的身份(經過用戶代理),並肯定資源全部者是否授予或拒絕客戶端的訪問請求。
(C)假設資源全部者許可訪問,受權服務器使用以前(在請求時或客戶端註冊時)提供的重定向URI重定向用戶代理回到客戶端。重定向URI包括受權碼和以前客戶端提供的任何本地狀態。
(D)客戶端經過包含上一步中收到的受權碼從受權服務器的令牌端點請求訪問令牌。當發起請求時,客戶端與受權服務器進行身份驗證。客戶端包含用於得到受權碼的重定向URI來用於驗證。
(E)受權服務器對客戶端進行身份驗證,驗證受權代碼,並確保接收的重定向URI與在步驟(C)中用於重定向客戶端的URI相匹配。若是經過,受權服務器響應返回訪問令牌與可選的刷新令牌。
過程實現:
1. client app 使用 app id 獲取 authorization code:
www.yii.com/oauth2/index.php?r=oauth2/authroize&response_type=code&client_id=testclient&state=xyz
返回:$authcode = authorization code.
Tips: authorization code will expired in 30s,能夠修改 OAuth2/ResponseType/AuthorizationCode.php 中的 AuthorizationCode class 的構造方法配置參數來自定義 authorization_code 有效時間。
client_id 是以前註冊在本 Server 上的應用名稱,這屬於客戶端管理範疇。
這一步須要進行用戶(資源全部者)登陸 OAuth2 Server 來完成受權操做。用戶登陸屬用戶管理範疇,不屬 OAuth2 Server 中應編寫的功能。
用戶登陸後可選擇本身能夠向 client app 開放的操做(受權)。
這一步綁定過程當中,從安全角度來考慮應強制用戶從新輸入用戶名密碼確認綁定,不要直接讀取當前用戶session進行綁定。
2. 獲取 access_token:
client app 使用 authorization code 換取 access_token
curl -u testclient:testpass www.yii.com/oauth2/index.php?r=oauth2/token -d "grant_type=authorization_code&code=$authcode
返回:
成功:
{"access_token":"aea4a1059d3194a3dd5e4117bedd6e07ccc3f402",
"expires_in":3600,
"token_type":"bearer",
"scope":null,
"refresh_token":"269a623f54171e8598b1852eefcf115f4882b820"
}
失敗:
{"error":"invalid_grant",
"error_description":"Authorization code doesn't exist or is invalid for the client"
}
Tip: 本步驟須要使用客戶端的 client_id 和 client_secret 以及上一步獲取的 authorization_code 換取 access_code.
access_tokne 有效期爲 3600s, refresh_token 有效期爲 1209600s,能夠在 OAuth2/ResponseType/AccessToken.php 中的 AccessToken class 中的構造函數配置中進行修改。
第二種認證方式: Implicit (隱式認證)
隱式受權類型被用於獲取訪問令牌(它不支持發行刷新令牌),並對知道操做具體重定向URI的公共客戶端進行優化。這些客戶端一般在瀏覽器中使用諸如JavaScript的腳本語言實現。
因爲這是一個基於重定向的流程,客戶端必須可以與資源全部者的用戶代理(一般是Web瀏覽器)進行交互並可以接收來自受權服務器的傳入請求(經過重定向)。
不一樣於客戶端分別請求受權和訪問令牌的受權碼許可類型,客戶端收到訪問令牌做爲受權請求的結果。
隱式許可類型不包含客戶端身份驗證而依賴於資源全部者在場和重定向URI的註冊。由於訪問令牌被編碼到重定向URI中,它可能會暴露給資源全部者和其餘駐留在相同設備上的應用。
採用Implicit Grant方式獲取Access Token的受權驗證流程又被稱爲User-Agent Flow,適用於全部無Server端配合的應用(因爲應用每每位於一個User Agent裏,如瀏覽器裏面,所以這類應用在某些平臺下又被稱爲Client-Side Application),如手機/桌面客戶端程序、瀏覽器插件等,以及基於JavaScript等腳本客戶端腳本語言實現的應用,他們的一個共同特色 是,應用沒法妥善保管其應用密鑰(App Secret Key),若是採起Authorization Code模式,則會存在泄漏其應用密鑰的可能性。其流程示意圖以下:
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| +----(A)-- & Redirection URI --->| |
| User- | | Authorization |
| Agent |----(B)-- User authenticates -->| Server |
| | | |
| |<---(C)--- Redirection URI ----<| |
| | with Access Token +---------------+
| | in Fragment
| | +---------------+
| |----(D)--- Redirection URI ---->| Web-Hosted |
| | without Fragment | Client |
| | | Resource |
| (F) |<---(E)------- Script ---------<| |
| | +---------------+
+-|--------+
| |
(A) (G) Access Token
| |
^ v
+---------+
| |
| Client |
| |
+---------+
注:說明步驟(A)和(B)的直線由於經過用戶代理而被分爲兩部分。
圖2:隱式許可流程
圖2中的所示流程包含如下步驟:
(A)客戶端經過向受權端點引導資源全部者的用戶代理開始流程。客戶端包括它的客戶端標識、請求範圍、本地狀態和重定向URI,一旦訪問被許可(或拒絕)受權服務器將傳送用戶代理回到該URI。
(B)受權服務器驗證資源擁有者的身份(經過用戶代理),並肯定資源全部者是否授予或拒絕客戶端的訪問請求。
(C)假設資源全部者許可訪問,受權服務器使用以前(在請求時或客戶端註冊時)提供的重定向URI重定向用戶代理回到客戶端。重定向URI在URI片斷中包含訪問令牌。
(D)用戶代理順着重定向指示向Web託管的客戶端資源發起請求(按RFC2616該請求不包含片斷)。用戶代理在本地保留片斷信息。
(E)Web託管的客戶端資源返回一個網頁(一般是帶有嵌入式腳本的HTML文檔),該網頁可以訪問包含用戶代理保留的片斷的完整重定向URI並提取包含在片斷中的訪問令牌(和其餘參數)。
(F)用戶代理在本地執行Web託管的客戶端資源提供的提取訪問令牌的腳本。
(G)用戶代理傳送訪問令牌給客戶端。
Tips: 1. 通常不需提供 client_secret,僅需 client_id,單用戶一樣須要認證。
2. Implicit Grant Type 不支持 refresh_token(或可自行實現)機制。
3. THE FIRST TIME THE USER AUTHENTICATES YOUR APP USING IMPLICIT GRANT FLOW STORE THE ACCESS TOKEN! Once you have the access token do not try to re-authenticate. Your access token that you stored should continue to work!
一旦獲取 access_token (存在於 redirect_uri 的 fragment 中, 即 uri 中的 # 部分),Client 須要本身存儲 access_token。
4. 比較適用於 Client-Side Application,如手機/桌面客戶端程序、瀏覽器插件等
oauth2-server-php 對本受權方式的實現以下:
1. 這種受權方式包含於 Authorization Code Grant (是對 Authorization Code Grant 方式的簡化)。
初始化 OAuth2Controller 時, 只需向 OAuth2 Server 添加 AuthorizationCode 類型的受權便可,以下:
$server->addGrantType(new OAuth2\GrantType\AuthorizationCode($storage));
Authorization Code 默認不支持 Implicit Grant, 須要將 Server.php 第 104 行的 'allow_implicit' 修改成 'true' 以開啓 Implicit 受權。
2. 獲取 access_token
http://www.yii.com/oauth2/index.php?r=oauth2/authorize&response_type=token&client_id=testclient&state=xyz&redirect_uri=www.baidu.com
參數: response_type=token (必須, 固定值)
client_id (必須)
redirect_uri 可選
scope 可選
state 推薦
注意:response_type = token 而不是 code, 由於隱式受權不用獲取 authorization code。
返回:
成功:
須要用戶先點擊受權按鈕。
SUCCESS! Authorization Code: www.baidu.com?#access_token=9f0c38b475e51ccd3
出錯: redirect_uri 與註冊的 client redirect_uri 不匹配。
{"error":"redirect_uri_mismatch","error_description":"The redirect URI provided is missing or does not match","error_uri":"http:\/\/tools.ietf.org\/html\/rfc6749#section-3.1.2"}
access_token 存在於 redirect_uri 中的片斷(fragment)中, 即‘#’符號以後,client 須要本身提取片斷中的 access_token 並注意保存。開發人員應注意,一些用戶代理不支持在HTTP「Location」HTTP響應標頭字段中包含片斷組成部分。這些客戶端須要使用除了3xx 重定向響應之外的其餘方法來重定向客戶端——-例如,返回一個HTML頁面,其中包含一個具備連接到重定向URI的動做的「繼續」按鈕。
第三種認證方式: Resource Owner Password Credentials (資源全部者密碼憑證許可)
資源全部者密碼憑據許可類型適合於資源全部者與客戶端具備信任關係的狀況,如設備操做系統或高級特權應用。當啓用這種許可類型時受權服務器應該特別關照且只有當其餘流程都不可用時才能夠。
這種許可類型適合於可以得到資源全部者憑據(用戶名和密碼,一般使用交互的形式)的客戶端。經過轉換已存儲的憑據至訪問令牌,它也用於遷移現存的使用如HTTP基本或摘要身份驗證的直接身份驗證方案的客戶端至OAuth。
+----------+
| Resource |
| Owner |
| |
+----------+
v
| Resource Owner
(A) Password Credentials
|
v
+---------+ +---------------+
| |>--(B)---- Resource Owner ------->| |
| | Password Credentials | Authorization |
| Client | | Server |
| |<--(C)---- Access Token ---------<| |
| | (w/ Optional Refresh Token) | |
+---------+ +---------------+
圖3:資源全部者密碼憑據流程
圖3中的所示流程包含如下步驟:
(A)資源全部者提供給客戶端它的用戶名和密碼。
(B)經過包含從資源全部者處接收到的憑據,客戶端從受權服務器的令牌端點請求訪問令牌。當發起請求時,客戶端與受權服務器進行身份驗證。
(C)受權服務器對客戶端進行身份驗證,驗證資源全部者的憑證,若是有效,頒發訪問令牌。
Tips: 客戶端一旦得到訪問令牌必須丟棄憑據。
oauth2-server-php 對 Resource Owner Password Credentials 的實現以下:
1. 首先在 Oauth2Controller 的構造函數中添加對於 Resource Owner Password Credentials 受權方式的支持,加入如下代碼:
$server->addGrantType(new OAuth2\GrantType\UserCredentials($storage));
2. 獲取 access_token :
curl -u testclient:testpass www.yii.com/oauth2/index.php?r=oauth2/token -d 'grant_type=password&username=rereadyou&password=rereadyou'
返回:
{"access_token":"66decd1b10891db5f8f63efe7cc352ce326895c6",
"expires_in":3600,
"token_type":"bearer",
"scope":null,
"refresh_token":"b5fa0c24e786e37e7ce7d6e2f911805dc65a0d7c"}
Tips: Github 上 oauth2-server-php 提供的 sql schema user 表裏面沒有 user_id 字段[12],須要自行添加該字段(主鍵, auto_increment)。
user 表設計使用 sha1 摘要方式,沒有添加 salt。
在 Pdo.php 中有:
// plaintext passwords are bad! Override this for your application
protected function checkPassword($user, $password)
{
return $user['password'] == sha1($password);
}
對於用戶認證須要改寫這個函數。
第四種認證方式: Client Credentials Grant (客戶端憑證許可)
當客戶端請求訪問它所控制的,或者事先與受權服務器協商(所採用的方法超出了本規範的範圍)的其餘資源全部者的受保護資源,客戶端能夠只使用它的客戶端憑據(或者其餘受支持的身份驗證方法)請求訪問令牌。
客戶端憑據許可類型必須只能由機密客戶端使用。
+---------+ +---------------+
| | | |
| |>--(A)- Client Authentication --->| Authorization |
| Client | | Server |
| |<--(B)---- Access Token ---------<| |
| | | |
+---------+ +---------------+
圖4:客戶端憑證流程
圖4中的所示流程包含如下步驟:
(A)客戶端與受權服務器進行身份驗證並向令牌端點請求訪問令牌。
(B)受權服務器對客戶端進行身份驗證,若是有效,頒發訪問令牌。
Tips: 這是最簡單的認證方式。
因爲客戶端身份驗證被用做受權許可,因此不須要其餘受權請求。
實現以下:
1. 在 Oauth2Controller 中添加對 client credentials 認證方式的支持:
$server->addGrantType(new OAuth2\GrantType\ClientCredentials($storage));
2. 獲取 access_token:
curl -u testclient:testpass www.yii.com/oauth2/index.php?r=oauth2/token -d 'grant_type=client_credentials'
提交參數: grant_type REQUIRED. Value MUST be set to "client_credentials".
scope OPTIONAL.
返回:
{"access_token": "f3c30def0d28c633e34921b65388eb0bbd9d5ff9",
"expires_in":3600,
"token_type":"bearer",
"scope":null}
Tips: Client 直接使用本身的 client id 和 client_secret 獲取 access_token;
RFC6749規範指明[10] clinet crendentials 客戶端認證取得 access_token 時不包括 refresh_token。
不過,oauth2-server-php 提供了控制開關,在 OAuth2/GrantTypes/ClientCredentials.php 第 33 行[11],
默認 $includeRefreshToken = false; 設置爲 true, 則可在頒發 access_token 同時頒發 refresh_token。
第三部分: access_token 類型說明
客戶端在操做數據資源時(經過 api)須要向 server 出示 access_token,關於如何出示 access_token 和 access_token 類型由如下部分說明。
IETF rfc 6749 中說明的 access_token 類型有兩種:Bearer type 和 MAC type。
因爲 OAuth2-Server-php 對於 MAC 類型的 access_token 尚在開發之中,如下僅對最常使用的 Bearer 類型 access_token 進行說明。
有三種在資源請求中發送 bearer access_token 資源給資源服務器的方法[13]。客戶端不能在每次請求中使用超過一個方法傳輸令牌。
a. 當在由HTTP/1.1[RFC2617]定義的「Authorization」請求頭部字段中發送訪問令牌時,客戶端使用「Bearer」身份驗證方案來傳輸訪問令牌。
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM
客戶端應該使用帶有「Bearer」HTTP受權方案的「Authorization」請求頭部字段發起帶有不記名令牌的身份驗證請求。資源服務器必須支持此方法。
b. 表單編碼的主體參數
當在HTTP請求實體主體中發送訪問令牌時,客戶端採用「access_token」參數向請求主體中添加訪問令牌。客戶端不能使用此方法,除非符合下列全部條件:
HTTP請求的實體頭部含有設置爲「application/x-www-form-urlencoded」的「Content-Type」頭部字段。
實體主體遵循HTML4.01[W3C.REC-html401-19991224]定義的「application/x-www-form-urlencoded」內容類型的編碼要求。
HTTP請求實體主體是單一的部分。
在實體主體中編碼的內容必須徹底由ASCII[USASCII]字符組成。
HTTP請求方法是請求主體定義爲其定義的語法。尤爲是,這意味着「GET」方法不能被使用。
客戶端採用傳輸層安全發起以下的HTTP請求:
POST /resource HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
access_token=mF_9.B5f-4.1JqM
c. 當在HTTP請求URI中發送訪問令牌時,客戶端採用「access_token」參數,向「統一資源標示符(URI):通用語法」RFC3986定義的請求URI查詢部分添加訪問令牌。
例如,客戶端採用傳輸層安全發起以下的HTTP請求:
GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
Host: server.example.com
它不該該被使用,除非不能在「Authorization」請求頭部字段或HTTP請求實體主體中傳輸訪問令牌。
以上在 rfc6750 規範中提出的三種 access_token 的使用方式。推薦使用第一種方案。Bearer token 的使用須要藉助 TLS 來確保 access_token 傳輸時的安全性。
第四部分: 使用 Bearer access_token 的調用 api
1. 使用 refresh_token 換取 access_token:
curl -u testclient:testpass www.yii.com/oauth2/index.php?r=oauth2/token -d "grant_type=refresh_token&refresh_token=1ce1a52dff3b5ab836ae25714c714cb86bf31b6f"
返回:
{"access_token":"50540a7ead3a27cdb458b6cdc38df25f64da18f1",
"expires_in":3600,
"token_type":"bearer",
"scope":null}
這裏沒有新的 refresh_token,須要進行配置以從新獲取 refresh_token,可修改 OAuth2/GrantType/RefreshToken.php 中的 RefreshToken class __construct 方法中的 'always_issue_new_refresh_token' => true 來開啓頒發新的 refresh_token。
Tips: IETF rfc2649 中對於 refresh_token section 的部分說明,
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
須要提供客戶端的 client_id 和 client_secret, grant_type 值必須是 refresh_token。
access_token 有效期內不能使用 refresh_token 換取新的 access_token。
2. 使用 access_token:
a. client app 使用 access_token 獲取 resource 信息。
oauth2-server 驗證 access_token:
curl www.yii.com/oauth2/index.php?r=oauth2/verifytoken -d 'access_token=aea4a1059d3194a3dd5e4117bedd6e07ccc3f402'
返回:
{"result":"success",
"message":"your access token is valid."
} 這個部分只是爲了驗證 access token 的有效性,client app 並不該該直接調用該方法,而是在請求資源時有server自行調用,根據判斷結果進行不一樣處理。
能夠在 Oauth2 extension 的 Server.php 中來修改 access_token 的有效期。
3. scope
scope 須要服務端肯定具體的可行操做。
scope 用來肯定 client 所能進行的操做權限。項目中操做權限由 srbac 進行控制, Oauth2 中暫不作處理。
4. state
state 爲 client app 在第一步驟中獲取 authorization code 時向 OAuth2 Server 傳遞並由 OAuth2 Server 返回的隨機哈希參數。state 參數主要用來防止跨站點請求僞造(Cross Site Request Forgery, CSRF),相關討論可參見本文最後的參考【7】和【8】。
References:
php