跳轉到

URL登入驗證

產生進入 URL 登入頁的網址

在範例程式中,我們提供一個簡單外部系統網頁,當填入參數後按下按鈕,就可以模擬透過 URL 登入開啟 UOFX 的情境,畫面如下:

Image auth-url

模擬外部系統 URL 登入

當使用者按下 '連線到 UOFX' 按鈕,則會依序進行下列事項:

請查看範例 Controllers > HomeController.cs

1、產生 access-token

首先需要產生一組 access-token 供後續 callback 使用,token 類型與內容沒有限制,但應該包含下列資訊與規範:

  • 需含能代表登入帳號的資訊: 例如範例中把帳號辨識碼 (sid) 放入 token
  • 要有時效性: 無時效性的 token 容易被拿來進行資安攻擊
[HttpPost]
public ActionResult ButtonClick(HomeViewModel body)
{
    ... 

    // 產生要登入的一次性帳號識別碼並放入 Token 中, callback 時用來識別身分 
    // (請依自己需求調整,但不建議把真實帳號放入 Token 中,因為 token 內容是公開的)
    var sid = GetSid(uofxAccount);

    // 產生短時效 Token (可改成使用自己製作的 token)
    var accessToken = TokenHelper.GenToken(sid, DateTimeOffset.Now.AddMinutes(5));

    ...
}

Note

範例中的 JWT Token 內容是公開的,只要拿到 token 的人都可以輕易取得其內容,因此請勿放敏感資料在其中,不過 token 含有 簽章 來避免被串改,所以不用擔心 token 的公開特性。

2、設定魔法連結

魔法連結是指 URL model 中要前往的目標位置 target,target 的類型可自訂也可套用範例中的MagicLinkModel,主要需提供ModuleActionPayload三個參數。若 target 設定為null會前往使用者大廳,若要前往其他目標可按照下列參數類型進行設定,最後提供給 URL model 中的 target 時,再進行 JSON Serialize 的轉換,target 接受的參數型別為string

MagicLinkModel target = new MagicLinkModel() 
{
    Module: ModuleType.Bpm,
    Action: BpmActionType.Apply,
    Payload: { formCode: "表單代號" }
}
目標 Module Action Payload
申請表單 ModuleType.Bpm BpmActionType.Apply { formCode: "表單代號" }
簽核表單 ModuleType.Bpm BpmActionType.Sign { formSn: "表單編號" }

Note

若設定的目標參數不正確或找不到編號單號等狀況,會轉至 404 頁面,若發生此狀況,請再次檢查設定的目標位置是否正確。

3、產生 URL Model

URL model 需要下列資訊,公司代碼URL 登入名稱access-TokenTarget時間戳 (Timestamp),時間戳請放當下時間,在 UOFX 會對時間戳進行驗證,確保此連結具有時效性,避免被濫用。 因為要將 URL model 放進 query-string,所以還需要把 model 用 base64 編碼

// 產生短時效 Token (可改成使用自己製作的 token)
...

// 進入 UOFX 後要前往的頁面
...

// 產生 URL Model 並使用 Base64 編碼
var urlLoginModel = new UrlLoginModel()
{
    CorpCode = uofxCorpCode,  //公司代碼
    UrlLoginName = uofxUrlLoginName,    //URL 登入名稱
    Token = accessToken,      //access-Token
    Target =  target == null ? null : JsonSerializer.Serialize(target),  //進入 UOFX 後要前往的頁面
    Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() 
};
var urlLoginJsonString = JsonSerializer.Serialize(urlLoginModel);
var urlLoginBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(urlLoginJsonString));

4、製作 hash

接下來要製作 hash,避免 URL Model 內容被修改,請使用 HashKey 把 URL Model 進行 hash,使用的規格為 HMACSHA-256

HMAC(Hash-based Message Authentication Code)

// 進入 UOFX 後要前往的頁面
...

// 產生 URL Model 並使用 Base64 編碼
...

// 產生 Hash
/*
    哈希演算法:SHA-256
    金鑰哈希演算法:HMAC(Hash-based Message Authentication Code)
    編碼:使用 UTF-8 編碼將輸入字串和金鑰轉換為位元組
    輸出格式:將哈希結果轉換為十六進制字串
*/
var hashString = HashHelper.HMACSHA256(urlLoginBase64, _HashKey);

Note

hash 結果請勿去除連字符 '-',保留原樣即可,例 0e-aa-c1-f6-d6-36...

5、組合出網址

最後我們將 URL Model 的 base64 字串,以及 hash 的結果放入 query string,並組出完整的網址。

{UOFX 站台網址}/UniversalLink/url?p={url model base64 字串}&h={hash}

// 產生 URL Model 並使用 Base64 編碼
...

// 產生 Hash
...

// 要開啟 UOFX 的 URL
var fullUrl = new Uri($"{uofxUrl}/UniversalLink/url?p={Uri.EscapeDataString(urlLoginBase64)}&h={Uri.EscapeDataString(hashString)}").ToString();

return Redirect(fullUrl);

Note

請使用 Uri.EscapeDataString 來確保 query string 不會出現不適當的字元。

建立 Callback API

在流程中最後會透過 Callback API 來驗證 access-token 的正確性,並取得真正的使用者資訊加密回傳,此方式有一些好處:

  1. 避免在 post message 暴露太多登入資訊
  2. 二次驗證加強安全性,避免 access-token 被偽造
  3. 透過加密隱藏真正要登入的使用者

Note

Callback API 可以是一個獨立的站台,不需要跟外部系統放在一起。

1、Callback API 規格

我們需要一個支援 web-api 的站台,並提供一個 API 給 UOFX 使用

項目 備註
method GET
URL 自訂 需與 URL 登入驗證設定相同

API 回應結果

狀態碼 備註
200 加密字串 成功回應
所有非200 不限 失敗回應

凡是非狀態碼 200 的回應,UOFX 皆會以 callback 異常處理,並在 log 中紀錄狀態碼和 response body。

在範例中 (Controllers > HomeController.cs) 有實作 API ( [HttpGet("/url/callback")]),後續將以此進行說明。

2、驗證 access-token

access-token 會在呼叫 API 時,透過 query-string 傳送,因此第一步要取得 token 並驗證其正確性

API: GET https://mycallback.com.tw/uofx/accountkey?t=xxxxxx

// 從網址中接收 token 參數 (UOFX 會以 query-string 't' 傳遞過來)
if (!Request.Query.TryGetValue("t", out var accessToken))
    throw new Exception("Token not found");

// 驗證 Token,並從 Token 中取得 sid
if (!TokenHelper.VirtyfyAndGetData(accessToken, out var sid))
    throw new Exception("Invalid Token");

在驗證 token 的過程中,也把之前放在 token 的帳號辨識碼 (sid) 取出

3、取得使用者資訊

接著藉由帳號辨識碼 (sid) 取得使用者帳號 (accountKey),accountKey 會事先在 UOFX 中使用者身上個別設定。 再來產生要回傳的 model CallbackResponseModel,此 model 有兩個屬性:

屬性 類型 備註
AccountKey string 使用者帳號
Timestamp long 當下的時間戳
// 根據 sid 取得帳號
var accountKey = GetAccountBySid(sid);

// 產生要回傳的 model
var result = new CallbackResponseModel()
{
    AccountKey = accountKey,
    Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() //務必放當下時間,會依此時間來判斷是否過期
};

時間戳

時間戳是用來確保此 api 的回應內容具有 時效性 ,避免被有心人士以同樣的內容在不同時間進行非法登入。

4、加密並回傳

API 的回應 (response) 內容 (body) 需要先透過 AES 加密,其規格如下:

項目 類型 備註
加密演算法 AES(Advanced Encryption Standard)
填充模式 PKCS7
加密模式 CBC(Cipher Block Chaining)
反饋大小 128 位元

接下來要透過 HashKey 轉換成 AES 加密所需的 KeyIV,轉換方式如下:

  1. Key: 透過 SHA256 hash HashKey 成為 Key
  2. IV: 取 Key 的後 16 個 byte 作為 IV

最後我們將加密結果回傳就完成了。

// 使用 AES 加密 model
byte[] aeskeyBytes = HashHelper.SHA256ToBytes(_HashKey);
var aesKey = aeskeyBytes;   // HashKey 的 SHA256 Hash
var aesIv = aeskeyBytes.Skip(16).ToArray(); // HashKey 的 SHA256 Hash 後 16 bytes
var encodeResult = AesHelper.EncodeData(JsonSerializer.Serialize(result), aesKey, aesIv);
return Ok(encodeResult);