# 甲、概述與資源
## 一、概述
要向 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
## 二、文件資源
* [SAS for Developers : SAS REST APIs: Authentication & Authorization](https://developer.sas.com/reference/viya35/auth/)
* [SAS Blogs : Authentication to SAS Viya: a couple of approaches](https://blogs.sas.com/content/sgf/2023/02/07/authentication-to-sas-viya/)
* [SAS Blogs : Building custom apps on top of SAS Viya, Part Three: Choosing an OAuth flow](https://blogs.sas.com/content/sgf/2020/09/14/building-custom-apps-on-top-of-sas-viya-part-three-choosing-an-oauth-flow/)
* [SAS Blogs : Building custom apps on top of SAS Viya, Part Four: Examples](https://blogs.sas.com/content/sgf/2020/09/25/building-custom-apps-on-top-of-sas-viya-part-four-examples/)
* [SAS Supports : OpenID Connect Opens the Door to SAS® Viya® APIs](https://support.sas.com/resources/papers/proceedings18/1737-2018.pdf)
# 乙、群組權限
## 一、Insufficient scope
如果請求 OAuth 2.0 的一些資源,例如 GET 或 POST clients,碰到 **error** 是 `insufficient_scope` 或 **error_description** 是 `Insufficient scope for this resource`,表示夾帶的 token 是由權限不足的帳號所產生,必須先加入足夠的群組。下圖就是由權限不足的帳號產生的 token,來做請求的結果:

上圖有提示 token 必須由 `uaa.admin`、 `clients.read`、 `clients.admin`、 `SASAdministrators`、 `zones.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` 以上帳號的登入密碼
參考語法:
```bash!
curl -X POST "主機/SASLogon/oauth/token" -u "sas.cli:" -H "Content-Type:application/x-www-form-urlencoded" -d "grant_type=password&username=帳號&password=密碼"
```

取出回應 JSON 中 `access_token` 的內容(參考上圖黃框內容)。
## 二、查看已有 client
使用 `GET` 方法可對路徑 `~/SASLogon/oauth/clients` 取得目前所有 client 的資料,可用來查看之前之後的變化。
### 1. cURL 檢視
* -X 方法 `GET`
* 請求路徑 `~/SASLogon/oauth/clients`
* -H 夾帶 Header 變數 `Authorization: Bearer ` 加上前面網頁取得的 token
語法:
```bash!
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_types` 以 `client_credentials` 為例,如果要使用其他 flow,修改或加入其他類型。
以下 Windows 語法:
```bash!
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 語法:
```bash!
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_id` 和 `client_secret`,curl 使用 -u,Postman 在 Authorization 分頁選擇 `Basic Auth`。
* password,要多傳送 `username` 和 `password` 兩個變數
* 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。
參考語法:
```bash!
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。
參考語法:
```bash!
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.GetBytes` 和 `Convert.ToBase64String` 轉成 Base64 編碼字串。回應的 JSON 結果使用 `System.Text.Json` 的 `JsonDocument` 等物件方法取出 `access_token`,以下範例不做空值判斷與例外處理。
### 1. 範例
* 給 `DefaultRequestHeaders` 做 `Add`,加入驗證用的資訊,前面加上 `"Basic "`(注意要空格)放在 `Authorization` 中來做請求。
* 另外用 `StringContent` 物件來傳入 form data,其中 `grant_type` 變數的值為 `client_credentials`。
```csharp=
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`。若使用 `password` 或 `authorization_code` 或 `refresh_token` 等需要傳送其他變數的 flow,較適合用 `KeyValuePair` 寫法(可再參考 3. 範例)。
```csharp=
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,必須多傳入 `username` 和 `password` 兩個變數:
```csharp=
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_code` 與 `refresh_token` 的 token 取法,請參照官方文件
## 五、使用取到的 token
如同前面用網頁取來的 token,不論是 cURL 還是 Postman 都是用 Bearer 的 Authorization 來做請求,所以將取得的 token 以 Bearer 前綴放在 header。
```csharp=
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();
}
```