Try   HackMD

甲、概述與資源

一、概述

要向 SAS Viya 做 HTTP 請求,必須攜帶 token,這個 token 可以在登入後從網頁取來用,但網頁式取法不適合程式化的 API,另外雖然有程式化取法但傳送帳密的作法在資安上是不好的方案。

以下筆記的目標,是為了 WEB API 以程式化的做法向 SAS 取 token 以便第二次做請求,在 OAuth 2.0 的架構下要能達成這個目標,必須先有 client credential,再視需求是否有另外的變數(username+password 或 authorization code),然而要建立 client,必須帳號擁有 SASAdministrators 等 scope,所以下面的筆記從最初的設定群組開始,若已經完成一個階段,可以跳過往後看。

流程大致如下:

  1. 管理者將帳號加入足夠權限建立 client 的群組
  2. 一般帳號網頁取得 token
  3. 使用 token 透過 API 註冊 client
  4. 用 client credential 透過 API 取得 token

二、文件資源

乙、群組權限

一、Insufficient scope

如果請求 OAuth 2.0 的一些資源,例如 GET 或 POST clients,碰到 errorinsufficient_scopeerror_descriptionInsufficient scope for this resource,表示夾帶的 token 是由權限不足的帳號所產生,必須先加入足夠的群組。下圖就是由權限不足的帳號產生的 token,來做請求的結果:

上圖有提示 token 必須由 uaa.adminclients.readclients.adminSASAdministratorszones.uaa.admin 其中之一的 scope 的帳號來產生,才能請求這個資源。

下面示範將帳號加入 SASAdministrators

二、加入群組

瀏覽器網址輸入 ~/~/SASLogon/login,登入管理者帳號

點選「是」來使用管理者權限繼續頁面:

點選左上角展開選單:

點選下方管理→管理環境:

點選系統→使用者(若沒有文字,下方有一個 >> 展開瀏覽列,點選後會展開):

自訂群組點選 SAS Administrators:

結果會在右邊,點選編輯:

選了要加入的人後,點中間的 +> 增加,選取的識別身分出現後,右邊點選確定:

完成後右上角登出:

丙、註冊 client

使用 API 建立 client,需要 token 且只要做一次就好,不需要反覆程式化取,所以用網頁取即可,若環境不方便開啟瀏覽器,也是有 API curl 的方法。

一、取最初 token

方法有網頁取和 API 取,由環境適合哪一項來判斷,以下 1. 和 2. 選一個做即可。

1. 網頁取最初 token

瀏覽器網址輸入 ~/~/SASLogon/login,登入 scope 足夠的一般帳號

不需要套用管理者權限。登入成功後在網址列輸入:

~/SASLogon/oauth/authorize?client_id=sas.cli&response_type=token

出現下面的畫面後,會重導向到另一個路徑,將網址列取出來:

截取 &access_token= 後面一直到 &expires_in= 之前的文字,複製出來:

2. API 取最初 token

填入帳號與密碼做以下請求:

  • -X 方法 POST
  • 請求路徑 ~/SASLogon/oauth/token
  • -u 使用 sas.cli:,要注意是冒號結尾
  • -H 夾帶 Header 變數 Content-Type: application/x-www-form-urlencoded
  • -d 夾帶 Data 變數 grant_type=password&username=帳號&password=密碼
    • grant_type 指定為 password
    • username 使用 scope 足夠的帳號
    • password 以上帳號的登入密碼

參考語法:

curl -X POST "主機/SASLogon/oauth/token" -u "sas.cli:" -H "Content-Type:application/x-www-form-urlencoded" -d "grant_type=password&username=帳號&password=密碼" 

image

取出回應 JSON 中 access_token 的內容(參考上圖黃框內容)。

二、查看已有 client

使用 GET 方法可對路徑 ~/SASLogon/oauth/clients 取得目前所有 client 的資料,可用來查看之前之後的變化。

1. cURL 檢視

  • -X 方法 GET
  • 請求路徑 ~/SASLogon/oauth/clients
  • -H 夾帶 Header 變數 Authorization: Bearer 加上前面網頁取得的 token

語法:

curl -X GET "主機/SASLogon/oauth/clients" -H "Authorization: Bearer 通行證"

送出後會得到回傳的 JSON 字串,列出所有 client 結果。

2. Postman 檢視

  • 方法 GET
  • 請求路徑 ~/SASLogon/oauth/clients
  • Authorization 分頁,Type 選 Bearer Token,右邊 Token 填上前面網頁取得的 token

回應 Body 得 JSON 的 client 結果。

三、註冊新的 client

使用 POST 方法可對路徑 ~/SASLogon/oauth/clients 建立新的 client,須要傳入以下變數:

  • client_id:client 名稱,下面以 app.demo 為範例(不一定要 app. 開頭,此為文件建議方便管理的命名)
  • client_secret:client 密碼,下面以 myPassword 為範例
  • scope:權限範圍,至少要有 "openid",以陣列 [] 放入
  • authorized_grant_types:授權 flow 的類型,以陣列 [] 放入,有下列幾種:
    • "client_credentials"
    • "password"
    • "refresh_token"
    • "authorization_code"
  • redirect_uri:完成後導向路徑,固定填入 "urn:ietf:wg:oauth:2.0:oob"

1. cURL 新增

  • -X 方法 POST
  • 請求路徑 ~/SASLogon/oauth/clients
  • -H 夾帶 Header 變數 Authorization: Bearer 加上前面網頁取得的 token
  • -H 夾帶 Header 變數 Content-Type: application/json
  • -d 夾帶 Data 資料,以 JSON 格式傳入 client_id、client_secret、scope、authorized_grant_types、redirect_uri

Windows 使用 curl 時 JSON 資料裡雙引號要從 " 改成 \",例如:
-d "{\"client_id\":\"app.demo\",\"client_secret\":\"myPassword\"}"

Linux 使用 curl 時外面用單引號 ' 則裡面可使用雙引號 ",例如:
-d '{"client_id":"app.demo","client_secret":"myPassword"}'

以下 authorized_grant_typesclient_credentials 為例,如果要使用其他 flow,修改或加入其他類型。

以下 Windows 語法:

curl -X POST "主機/SASLogon/oauth/clients" -H "Authorization: Bearer 通行證" -H "Content-Type: application/json" -d "{\"client_id\":\"app.demo\",\"client_secret\":\"myPassword\",\"scope\":[\"openid\"],\"authorized_grant_types\":[\"client_credentials\"],\"redirect_uri\":\"urn:ietf:wg:oauth:2.0:oob\"}"

參考 Linux 語法:

curl -X POST "主機/SASLogon/oauth/clients" -H "Authorization: Bearer 通行證" -H "Content-Type: application/json" -d '{"client_id":"app.demo","client_secret":"myPassword","scope":["openid"],"authorized_grant_types":["client_credentials"],"redirect_uri":"urn:ietf:wg:oauth:2.0:oob"}'

送出後會回傳新增的 client 的資料,可再用 GET 做確認有沒有新增進去。

2. Postman 新增

  • 方法 POST
  • 請求路徑 ~/SASLogon/oauth/clients
  • Authorization 分頁,Type 選 Bearer Token,右邊 Token 填上前面網頁取得的 token
  • Body 分頁選擇 raw,右邊下拉選 JSON,填入對應的變數

送出後會回傳新增的 client 的資料,可再用 GET 做確認有沒有新增進去。

四、刪除已註冊 client

沒有 PUT 方法可修改,所以做錯了或要異動 client 設定,需要使用 DELETE 方法刪除,再使用 POST 方法重新新增。

  • 方法 DELETE
  • 請求路徑 ~/SASLogon/oauth/clients/ 加上 client_id
  • 夾帶 Header 變數 Authorization: Bearer 加上前面網頁取得的 token;若 Postman 則於 Authorization 分頁的 Type 選擇 Bearer Token,填入取得 token

下面示範使用 Postman 刪除 app.demo ,路徑是 ~/SASLogon/oauth/clients/app.demo

成功會回傳刪除的 client 的資料,若不存在則回傳空字串。

丁、API 取 token

有了 client 後,依照新增時設定的 authorized_grant_types 為何,對應取得 token 的方法,和傳入的變數。

  • client_credentials,最基本要傳入 client_idclient_secret,curl 使用 -u,Postman 在 Authorization 分頁選擇 Basic Auth
  • password,要多傳送 usernamepassword 兩個變數
  • refresh_token,要多傳送 refresh_token 變數
  • authorization_code,要多傳送 code 變數

一、cURL 取 token

cURL 有兩個寫法,一個是用 -u 傳送驗證資料,另一個是在 -d 放入驗證資料。

1. -u 驗證

  • -X 方法 POST
  • 請求路徑 ~/SASLogon/oauth/token
  • -H 夾帶 Header 變數 Content-Type: application/x-www-form-urlencoded
  • -d 夾帶 Data 資料 grant_type=client_credentials
  • -u 附上驗證資料 $client_id$:$client_secret$,以冒號 : 分隔

送出後會得到一包 JSON,其中 access_token 即為之後需要的 token。

參考語法:

curl -X POST "主機/SASLogon/oauth/token" -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=client_credentials" -u "app.demo:myPassword"

2. -d 驗證

  • -X 方法 POST
  • 請求路徑 ~/SASLogon/oauth/token
  • -H 夾帶 Header 變數 Content-Type: application/x-www-form-urlencoded
  • -d 夾帶 Data 資料
    • grant_type=client_credentials
    • client_id=$client_id$
    • client_secret=$client_secret$

送出後會得到一包 JSON,其中 access_token 即為之後需要的 token。

參考語法:

curl -X POST "主機/SASLogon/oauth/token" -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=client_credentials&client_id=app.demo&client_secret=myPassword"

二、Postman 取 token

  • 方法 POST
  • 請求路徑 ~/SASLogon/oauth/token
  • Authorization 分頁,Type 選 Basic Auth,右邊 Username 填上 client_id,前面示範為 app.demo,Password 填上 client_secret,前面示範為 myPassword
  • Header 分頁,加入 KEY Content-Type,VALUE 為 application/x-www-form-urlencoded
  • Body 分頁選擇 x-www-form-urlencoded,加入 KEY grant_type,VALUE 為 client_credentials

Authorization 分頁

Headers 分頁

Body 分頁

送出後會得到一包 JSON,其中 access_token 即為之後需要的 token。

三、JavaScript 取 token

四、C# NET Core 取 token

client_id 和 client_secret 要用冒號組成一個字串,透過 System.Text.Encoding.UTF8.GetBytesConvert.ToBase64String 轉成 Base64 編碼字串。回應的 JSON 結果使用 System.Text.JsonJsonDocument 等物件方法取出 access_token,以下範例不做空值判斷與例外處理。

1. 範例

  • DefaultRequestHeadersAdd,加入驗證用的資訊,前面加上 "Basic "(注意要空格)放在 Authorization 中來做請求。
  • 另外用 StringContent 物件來傳入 form data,其中 grant_type 變數的值為 client_credentials
using System.Text; using System.Text.Json; string token = await GetToken(); Console.WriteLine(token); Console.ReadKey(); async Task<string> GetToken() { HttpClient client = new HttpClient(); string url = "$host$/SASLogon/oauth/token"; byte[] byteCredential = Encoding.UTF8.GetBytes("app.demo:myPassword"); string stringCredential = Convert.ToBase64String(byteCredential); client.DefaultRequestHeaders.Add("Authorization", "Basic " + stringCredential); StringContent para = new StringContent( "grant_type=client_credentials", Encoding.UTF8, "application/x-www-form-urlencoded"); using (var response = await client.PostAsync(url, para)) { string? jsonResult = await response.Content.ReadAsStringAsync(); JsonDocument doc = JsonDocument.Parse(jsonResult); JsonElement root = doc.RootElement; if (root.TryGetProperty("access_token", out JsonElement access_token)) { return access_token.GetString(); } else { return "*"; } } }

2. 範例

  • 使用正統 AuthenticationHeaderValue 物件傳入憑證資訊,前綴是 Basic
  • KeyValuePair 物件來傳入 form data,grant_type 變數的值是 client_credentials。若使用 passwordauthorization_coderefresh_token 等需要傳送其他變數的 flow,較適合用 KeyValuePair 寫法(可再參考 3. 範例)。
using System.Net.Http.Headers; using System.Text; using System.Text.Json; string token = await GetToken(); Console.WriteLine(token); Console.ReadKey(); async Task<string> GetToken() { HttpClient client = new HttpClient(); string url = "$host$/SASLogon/oauth/token"; byte[] byteCredential = Encoding.UTF8.GetBytes("app.demo:myPassword"); string stringCredential = Convert.ToBase64String(byteCredential); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", stringCredential); var paras = new List<KeyValuePair<string, string>>(); paras.Add(new KeyValuePair<string, string>("grant_type", "client_credentials")); var content = new FormUrlEncodedContent(paras); using (var response = await client.PostAsync(url, content)) { string? jsonResult = await response.Content.ReadAsStringAsync(); JsonDocument doc = JsonDocument.Parse(jsonResult); JsonElement root = doc.RootElement; if (root.TryGetProperty("access_token", out JsonElement access_token)) { return access_token.GetString(); } else { return "*"; } } }

3. 範例

若註冊新增 client 時,authorized_grant_types 陣列有放入 "password",則可以使用此 grant_type,必須多傳入 usernamepassword 兩個變數:

using System.Net.Http.Headers; using System.Text; using System.Text.Json; string token = await GetToken(); Console.WriteLine(token); Console.ReadKey(); async Task<string> GetToken() { HttpClient client = new HttpClient(); string url = "$host$/SASLogon/oauth/token"; byte[] byteCredential = Encoding.UTF8.GetBytes("app.demo:myPassword"); string stringCredential = Convert.ToBase64String(byteCredential); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", stringCredential); var paras = new List<KeyValuePair<string, string>>(); paras.Add(new KeyValuePair<string, string>("grant_type", "password")); paras.Add(new KeyValuePair<string, string>("username", "XXXXXXXX")); paras.Add(new KeyValuePair<string, string>("password", "OOOOOOOO")); var content = new FormUrlEncodedContent(paras); using (var response = await client.PostAsync(url, content)) { string? jsonResult = await response.Content.ReadAsStringAsync(); JsonDocument doc = JsonDocument.Parse(jsonResult); JsonElement root = doc.RootElement; if (root.TryGetProperty("access_token", out JsonElement access_token)) { return access_token.GetString(); } else { return "*"; } } }

關於 authorization_coderefresh_token 的 token 取法,請參照官方文件

五、使用取到的 token

如同前面用網頁取來的 token,不論是 cURL 還是 Postman 都是用 Bearer 的 Authorization 來做請求,所以將取得的 token 以 Bearer 前綴放在 header。

using System.Net.Http.Headers; using System.Text; using System.Text.Json; string token = await GetToken(); var data = GetData(); await RequestToSas(token, data); Console.ReadKey(); async Task<string> GetToken() { // 同前 } async Task RequestToSas(string token, Data data) { HttpClient client = new HttpClient(); // 決策發行後的模組 URI,注意可能要加 /steps/execute string url = "$host$/$api$"; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using (var jsonContent = new StringContent( JsonSerializer.Serialize(data), Encoding.UTF8, "application/json")) { using (var response = await client.PostAsync(url, jsonContent)) { string? jsonResult = await response.Content.ReadAsStringAsync(); /* 後續邏輯 */ } } } Data GetData() { /* SAS 決策流程接收變數所需要格式 * { * "inputs":[ * { "name": "EmployeeID_", "value": "54321" }, * { "name": "Salary_", "value": 35000 }, * { "name": "Name_", "value": "陳建國" } * ] * } * */ return new Data(); }