客製化API配置

功能簡介

SAML 2 是一個標準的 SSO 協議, 並受 Windows AD 支援, 因此在 SSO 領域有著廣泛的應用,但其要求開發者具有一定的背景知識(建議閱讀官方文件),對接成本較高, 因此我們補充實現了自訂API。

自訂API是簡化的 SAML 協議,企業客戶可在現有 SSO API基礎上進行修改,按照Jodoo的要求調用服務和返回參數,並將認證後的使用者資訊返回給Jodoo,完成帳號關聯。

設定步驟

設定入口

1. 登入Jodoo帳號,進入企業管理 > 管理工具 > 企業設定頁面中。

2. 在企業安全 > 單一登入處,打開單一登入的開關,並點擊配置按鈕。

選擇配置方式

進入配置單一登入詳情頁中,選擇單一登入配置方式為自訂介面」。如下所示:

設定Idp登入API

Idp 登入API,指的是透過開發人員部署所需的基礎內容,允許使用者登入系統的API。通常用於需要驗證使用者身份並且允許使用者造訪系統的場景當中。詳情可參見文件:Idp 配置說明

在Jodoo的單一登入中,您可以根據企業自身的伺服器配置,設定登入API:

生成認證密鑰

認證密鑰指的是資訊的發送方和接收方,需要透過一個密鑰去加密和解密資料。在自訂API當中,使用者可以自訂設定認證密鑰,也可以點擊「生成密鑰」按鈕,直接生成一串認證密鑰。如下所示:

附註

認證密鑰需要與程式碼中的 SECRET 保持一致。

選擇認證加密算法

Jodoo的單一登入中,支援以下 3 種加密算法,您可以根據自己的需求來選擇合適的加密算法,從而完成單一登入配置:

  • HS256
  • HS384
  • HS512

設定Issuer URL

Issuer URL 用於驗證請求內容與服務後台是否能夠匹配成功,若匹配成功則可以進行解析,否則將請求失敗。您可自訂 Issuer URL 的內容。如設定為: Issuer.test。

附註

若設定了 Issuer URL,則 Issuer URL 中的內容需要與程式碼中的 Issuer 保持一致。

設定登出API

Idp 登出API,是指當企業成員造訪了Jodoo的單點登出地址時,Jodoo不僅會登出當前成員,同時還會將成員重定向至 Idp 並攜帶登出請求參數。

附註

1. IdP 可以銷燬與此成員的會話以實現單點登出的效果。

2. 單點登出請求參數格式與認證請求參數一致, 並額外包含 jti 或 nameId 參數, 但不包含 state 欄位, 在 Token 中的 type 為常量slo_req

程式碼範例

絕大多數編程語言都有較為良好的 JWT 算法實現, 第三方庫列表可在這個頁面中查找,下面給出以 Python 程式碼Golang 程式碼 實現 IdP 配置 的簡單範例:

Golang Demo


package main
 
import (
   "fmt"
   "github.com/dgrijalva/jwt-go"
   "log"
   "net/http"
   "time"
)
 
const (
   acs      = "https://www.jodoo.com/sso/custom/5cd91fe50e42834f41b7c6ef/acs"
   issuer   = "com.example"
   username = "angelmsger"
   secret   = "jd"
)
 
func ValidBody(body jwt.MapClaims) bool {
   return body["iss"] == "com.jodoo" && body["aud"] == issuer && body["type"] == "sso_req"
}
 
func ValidToken(query string) bool {
   token, err := jwt.Parse(query, func(token *jwt.Token) (interface{}, error) {
      if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
         return nil, fmt.Errorf("Unexpected Signing Method: %v ", token.Header["alg"])
      }
      return []byte(secret), nil
   })
   if err != nil {
      return false
   }
   claims, ok := token.Claims.(jwt.MapClaims)
   return ok && token.Valid && ValidBody(claims)
}
 
func GetTokenByUsername(username string) (string, error) {
   now := time.Now()
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
      "type":     "sso_res",
      "username": username,
      "iss":      issuer,
      "aud":      "com.jodoo",
      "nbf":      now.Unix(),
      "iat":      now.Unix(),
      "exp":      now.Add(1 * time.Minute).Unix(),
   })
   return token.SignedString([]byte(secret))
}
 
func BuildResponseUri(token string, state string) string {
   target := acs + "?response=" + token
   if state != "" {
      target += "&state=" + state
   }
     return target
}
 
func handler(w http.ResponseWriter, r *http.Request) {
   query := r.URL.Query()
   reqToken := query.Get("request")
   if ok := ValidToken(reqToken); ok {
      if resToken, err := GetTokenByUsername(username); err == nil {
         target := BuildResponseUri(resToken, query.Get("state"))
         http.Redirect(w, r, target, http.StatusSeeOther)
      }
      w.WriteHeader(404)
   }
   w.WriteHeader(404)
}
 
func main() {
   http.HandleFunc("/sso", handler)
   log.Fatal(http.ListenAndServe(":8080", nil))
}

Python Demo

from datetime import datetime, timedelta
from flask import Flask, abort, redirect, request
import jwt
 
from jwt import InvalidTokenError
 
 
class Const:
    ACS = 'https://www.jodoo.com/sso/custom/5cd91fe50e42834f41b7c6ef/acs'
    SECRET = 'jdy'
    ISSUER = 'com.example'
    USERNAME = 'angelmsger'
 
 
app = Flask(__name__)
 

 def valid_token(query):
    try:
        token = jwt.decode(
            query, Const.SECRET,
            audience=Const.ISSUER,
            issuer='com.jodoo'
        )
        return token.get('type') == 'sso_req'
    except InvalidTokenError:
        return False
 
 
def get_token_from_username(username):
    now = datetime.utcnow()
    return jwt.encode({
        "type": "sso_res",
        'username': username,
        'iss': Const.ISSUER,
        "aud": "com.jodoo",
        "nbf": now,
        "iat": now,
        "exp": now + timedelta(seconds=60),
    }, Const.SECRET, algorithm='HS256').decode('utf-8')
 
 
@app.route('/sso', methods=['GET'])
def handler():
    query = request.args.get('request', default='')
    state = request.args.get('state')
    if valid_token(query):
        token = get_token_from_username(Const.USERNAME)
        stateQuery = "" if not state else f"&state={state}"
        return redirect(f'{Const.ACS}?response={token}{stateQuery}')
    else:
        return abort(404)
 
 
if __name__ == '__main__':
    app.run(port=8080)

這項資訊有幫助嗎?
需要更多協助嗎?聯繋技術支援。