Connect 流程
本页讲解 Connect 协议:当应用希望直接控制浏览器登录往返时使用的 Sudomimus 流程。Connect 走 JSON over HTTPS,任何带 HTTP 客户端的后端语言都能使用;下面给出 curl、Node.js、Python、Go 四种示例。
生产代码中,优先使用已有的官方 SDK。可以先选择框架或 API SDK,或直接查看使用 @sudomimus/connect 的 TypeScript API SDK。
如果你做的是原生客户端(桌面、游戏、CLI),见 原生客户端。如果走 OIDC,见 OIDC 接入方。
Tab 全页同步:选一次语言,下面所有代码块都会跟随切换。
开始登录前,请配置所选流程需要的规则和凭据,再由组织 OWNER 激活应用。新应用会保持 DRAFT,直到明确执行激活;登录要求应用为 ACTIVE,且所属组织和扇区均可用。
| 阶段 | 发起方 | 端点 | 结果 |
|---|---|---|---|
| 1. Establish | 应用后端 | connect-api POST /establish |
{ exposureKey, hiddenKey } |
| 2. Authenticate | 浏览器 | via.sudomimus.com |
用户完成一项允许的认证挑战 |
| 3. Redeem | 应用后端 | connect-api POST /redeem |
{ accessToken, refreshToken } |
| 4. Refresh | 应用后端 | session-api POST /refresh |
新 access token 与轮换后的 refresh token |
下图展示本页示例采用的标准 CALLBACK 路径:
sequenceDiagram
autonumber
participant App as 应用后端
participant Browser as 用户浏览器
participant Connect as Connect API
participant Via as via.sudomimus.com
participant Session as Session API
App->>Connect: POST /establish<br/>client-auth JWT + return method
Connect-->>App: exposureKey + hiddenKey
Note over App: hiddenKey 只保存在服务端
App-->>Browser: 302 重定向,携带 exposureKey
Browser->>Via: 打开托管认证界面
Note over Browser,Via: 用户完成一项允许的认证挑战
Via-->>Browser: 302 到应用 callback<br/>exposureKey + confirmationKey
Browser->>App: GET 应用 callback
App->>Connect: POST /redeem<br/>exposureKey + hiddenKey + confirmationKey
Connect-->>App: accessToken + refreshToken
App->>Session: GET 应用 JWKS
Session-->>App: 公钥验证材料
Note over App: 在本地验证 access token
loop access token 过期前
App->>Session: POST /refresh<br/>当前 refreshToken
Session-->>App: 新 accessToken + 轮换后的 refreshToken
end
三个参与方各自承担不同责任:
- 应用后端签名
/establish、保存hiddenKey、兑换已完成的 inquiry,并验证最终令牌。 - 浏览器把
exposureKey带到托管认证界面,但永远看不到hiddenKey。 via.sudomimus.com执行通行密钥、邮箱验证码、OAuth 或联合登录挑战,只有认证成功后才创建confirmationKey。
前三个阶段只属于 Connect。refresh、introspection、logout 和 revoke-all 是普通应用会话生命周期,由共享的 Session API 承接。OIDC 使用 authorization code + PKCE;原生 direct-issue 则在一次请求中交换 Steam ticket、AccessKey 或 PublicKey。
1. Establish —— 开启一次会话
Section titled “1. Establish —— 开启一次会话”后端请求 Connect 开启一次认证会话。返回里会同时给出 exposure key(要传给浏览器)和 hidden key(留在服务器)。
curl -X POST https://connect-api.sudomimus.com/establish \ -H "Content-Type: application/json" \ -H "Authorization: SudomimusClientJWT $SUDOMIMUS_CLIENT_AUTH_JWT" \ -d '{ "applicationAnchor": "your-application", "returnMethods": [ { "type": "CALLBACK", "payload": { "callbackUrl": "https://your-app.com/auth/callback" } } ] }'const body = JSON.stringify({ applicationAnchor: process.env.SUDOMIMUS_APPLICATION_ANCHOR, returnMethods: [ { type: "CALLBACK", payload: { callbackUrl: "https://your-app.com/auth/callback" }, }, ],});
const res = await fetch("https://connect-api.sudomimus.com/establish", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `SudomimusClientJWT ${await signEstablishJwt(body)}`, }, body,});
const { exposureKey, hiddenKey } = await res.json();import json, os, requests
body = json.dumps({ "applicationAnchor": os.environ["SUDOMIMUS_APPLICATION_ANCHOR"], "returnMethods": [ { "type": "CALLBACK", "payload": {"callbackUrl": "https://your-app.com/auth/callback"}, }, ],}, separators=(",", ":")).encode("utf-8")
res = requests.post( "https://connect-api.sudomimus.com/establish", headers={ "Content-Type": "application/json", "Authorization": f"SudomimusClientJWT {sign_establish_jwt(body)}", }, data=body,)
data = res.json()exposure_key = data["exposureKey"]hidden_key = data["hiddenKey"]body, _ := json.Marshal(map[string]any{ "applicationAnchor": os.Getenv("SUDOMIMUS_APPLICATION_ANCHOR"), "returnMethods": []map[string]any{{ "type": "CALLBACK", "payload": map[string]any{ "callbackUrl": "https://your-app.com/auth/callback", }, }},})
req, _ := http.NewRequest( "POST", "https://connect-api.sudomimus.com/establish", bytes.NewReader(body),)req.Header.Set("Content-Type", "application/json")req.Header.Set("Authorization", "SudomimusClientJWT "+signEstablishJwt(body))
res, err := http.DefaultClient.Do(req)
var data struct { ExposureKey string `json:"exposureKey"` HiddenKey string `json:"hiddenKey"`}json.NewDecoder(res.Body).Decode(&data)把 hiddenKey 与用户的 pending session 绑定存到服务端,然后用 URL 里带上 exposureKey 把用户跳转到 via.sudomimus.com。
2. Authenticate —— 交给 via.sudomimus.com
Section titled “2. Authenticate —— 交给 via.sudomimus.com”把用户浏览器跳转到 via.sudomimus.com,URL 里带上 exposure key。用户在那里完成通行密钥或邮箱验证码挑战。
# 不发 HTTP —— 你的应用做一次 302:Location: https://via.sudomimus.com/?exposure-key=<exposureKey>const authUrl = new URL("https://via.sudomimus.com/");authUrl.searchParams.set("exposure-key", exposureKey);
return Response.redirect(authUrl.toString(), 302);from urllib.parse import urlencodefrom flask import redirect
return redirect( "https://via.sudomimus.com/?" + urlencode({"exposure-key": exposure_key}), code=302,)http.Redirect( w, r, "https://via.sudomimus.com/?exposure-key="+url.QueryEscape(exposureKey), http.StatusFound,)用户完成挑战后,via.sudomimus.com 会将浏览器跳转回 callbackUrl,并在 URL 中附带 exposure-key 和 confirmation-key 两个查询参数。
3. Redeem —— 兑换令牌
Section titled “3. Redeem —— 兑换令牌”在回调处理程序中,将三个密钥一并提交给 Connect,换取访问令牌和刷新令牌。
curl -X POST https://connect-api.sudomimus.com/redeem \ -H "Content-Type: application/json" \ -d '{ "exposureKey": "...", "hiddenKey": "...", "confirmationKey": "..." }'// 位于 GET /auth/callback?exposure-key=...&confirmation-key=...const { exposureKey, hiddenKey } = await loadPendingSession(req);const confirmationKey = req.query["confirmation-key"];
const res = await fetch("https://connect-api.sudomimus.com/redeem", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ exposureKey, hiddenKey, confirmationKey }),});
const { accessToken, refreshToken } = await res.json();# 位于 GET /auth/callback?exposure-key=...&confirmation-key=...exposure_key, hidden_key = load_pending_session(request)confirmation_key = request.args["confirmation-key"]
res = requests.post( "https://connect-api.sudomimus.com/redeem", json={ "exposureKey": exposure_key, "hiddenKey": hidden_key, "confirmationKey": confirmation_key, },)
data = res.json()access_token = data["accessToken"]refresh_token = data["refreshToken"]// 位于 GET /auth/callback?exposure-key=...&confirmation-key=...exposureKey, hiddenKey := loadPendingSession(r)confirmationKey := r.URL.Query().Get("confirmation-key")
body, _ := json.Marshal(map[string]string{ "exposureKey": exposureKey, "hiddenKey": hiddenKey, "confirmationKey": confirmationKey,})
res, _ := http.Post( "https://connect-api.sudomimus.com/redeem", "application/json", bytes.NewReader(body),)
var data struct { AccessToken string `json:"accessToken"` RefreshToken string `json:"refreshToken"`}json.NewDecoder(res.Body).Decode(&data)返回的令牌在通过完整的令牌验证流程前都应视为不透明凭据;验证还包括 typ 与执行主体结构检查。
4. Refresh —— 通过 Session API 维持会话有效
Section titled “4. Refresh —— 通过 Session API 维持会话有效”在 access token 过期前,把 refresh token 交给 Session API,换取一个新的 access token 和一个新的 refresh token。/refresh 不需要 client-auth JWT。refresh token 会被轮换 —— 你提交的令牌被消费,响应里返回它的替代者。请存下新的 refreshToken 并在下一次刷新时使用它;重复使用已经用过的令牌会导致整个会话被吊销。对同一个令牌近乎同时的并发刷新(例如多个标签页)会被容忍并收敛到同一个会话;只有在替代令牌签发之后再复用才会吊销它。
curl -X POST https://session-api.sudomimus.com/refresh \ -H "Content-Type: application/json" \ -d '{ "refreshToken": "..." }'const res = await fetch("https://session-api.sudomimus.com/refresh", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken }),});
// refresh token 会被轮换 —— 捕获新的那个并持久化,替换掉你刚发出去的令牌。const { accessToken, refreshToken: newRefreshToken } = await res.json();await store.saveRefreshToken(newRefreshToken);res = requests.post( "https://session-api.sudomimus.com/refresh", json={"refreshToken": refresh_token},)
data = res.json()access_token = data["accessToken"]# refresh token 会被轮换 —— 持久化新的那个,替换掉旧的。store.save_refresh_token(data["refreshToken"])body, _ := json.Marshal(map[string]string{"refreshToken": refreshToken})
res, _ := http.Post( "https://session-api.sudomimus.com/refresh", "application/json", bytes.NewReader(body),)
var data struct { AccessToken string `json:"accessToken"` RefreshToken string `json:"refreshToken"`}json.NewDecoder(res.Body).Decode(&data)
// refresh token 会被轮换 —— 持久化 data.RefreshToken,替换掉旧的。store.SaveRefreshToken(data.RefreshToken)会话内省、登出、账户级吊销见 管理会话。
查询应用元数据
Section titled “查询应用元数据”POST /info 根据 anchor 返回某个应用的本地化公开资料。它不需要 client-auth JWT,可以从浏览器或其他不可信上下文中调用。签名密钥由 Session JWKS 端点负责,不属于这个元数据路由。
curl -X POST https://connect-api.sudomimus.com/info \ -H "Content-Type: application/json" \ -d '{ "applicationAnchor": "your-application", "locale": "zh-CN" }'const res = await fetch("https://connect-api.sudomimus.com/info", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ applicationAnchor, locale: "zh-CN" }),});
const { applicationAnchor: anchor, applicationName } = await res.json();res = requests.post( "https://connect-api.sudomimus.com/info", json={"applicationAnchor": application_anchor, "locale": "zh-CN"},)
info = res.json()body, _ := json.Marshal(map[string]string{ "applicationAnchor": applicationAnchor, "locale": "zh-CN",})
res, _ := http.Post( "https://connect-api.sudomimus.com/info", "application/json", bytes.NewReader(body),)请使用令牌与验证中说明的应用专属 Session JWKS;缓存与未知 kid 的处理规则由该页统一说明。
当原生客户端能拉起用户的系统浏览器、但又不方便接收回调 URL 时,使用轮询流程:
- 客户端后端调用
connect POST /establish(带上应用的 client-auth JWT),声明STATUS_POLL返回方法,拿到{ exposureKey, hiddenKey }。 - 客户端拉起系统浏览器并打开
https://via.sudomimus.com/?exposure-key=<exposureKey>。 - 用户在浏览器中完成通行密钥或邮箱验证码挑战。
- 客户端每隔几秒调用一次
connect POST /status-poll,提交{ exposureKey, hiddenKey }。一旦用户完成,轮询会返回{ status: "REALIZED", confirmationKey }。 - 客户端将三个密钥提交给
connect POST /redeem,换取{ accessToken, refreshToken }。
这一方案适用于任何带默认浏览器的平台——Windows、macOS、Linux 桌面应用、Electron 等等。应用的 Layer 3 规则中必须允许 STATUS_POLL。
/establish 就是标准的、用 client-auth 签名的 Connect 请求 —— 完整形状见 Web 应用 —— 只是返回方法换成 STATUS_POLL:
curl -X POST https://connect-api.sudomimus.com/establish \ -H "Content-Type: application/json" \ -H "Authorization: SudomimusClientJWT $SUDOMIMUS_CLIENT_AUTH_JWT" \ -d '{ "applicationAnchor": "your-application", "returnMethods": [ { "type": "STATUS_POLL", "payload": {} } ] }'# → { "exposureKey": "exp_...", "hiddenKey": "hid_..." }随后用这两把 key 每隔几秒轮询一次 /status-poll。轮询不带 client-auth JWT —— 授权它的是对 hiddenKey 的持有:
curl -X POST https://connect-api.sudomimus.com/status-poll \ -H "Content-Type: application/json" \ -d '{ "exposureKey": "exp_...", "hiddenKey": "hid_..." }'
# 用户仍在浏览器里鉴权时:# { "status": "PENDING" }# 用户完成后:# { "status": "REALIZED", "confirmationKey": "cnf_..." }轮询返回 REALIZED 后,将三个密钥提交给 connect POST /redeem,换取访问令牌和刷新令牌。该调用与 Web 流程中的 /redeem 相同。