客製化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)