功能簡介
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)