Connect 流程
本页讲解 Connect 协议:当应用希望直接控制浏览器登录往返时使用的 Sudomimus 流程。Connect 走 JSON over HTTPS,任何带 HTTP 客户端的后端语言都能使用;下面给出 curl、Node.js、Python、Go 四种示例。
如果你做的是原生客户端(桌面、游戏、CLI),见 原生客户端。如果走 OIDC,见 OIDC 接入方。
Tab 全页同步:选一次语言,下面所有代码块都会跟随切换。
| 阶段 | 发起方 | 端点 | 结果 |
|---|---|---|---|
| 1. Establish | 应用后端 | connect-api POST /establish | { exposureKey, hiddenKey } |
| 2. Authenticate | 浏览器 | via.sudomimus.com | 用户完成一项允许的认证挑战 |
| 3. Redeem | 应用后端 | connect-api POST /redeem | { accessToken, refreshToken } |
| 4. Refresh | 应用后端 | connect-api POST /refresh | 新 access token 与轮换后的 refresh token |
三个参与方各自承担不同责任:
- 应用后端签名
/establish、保存hiddenKey、兑换已完成的 inquiry,并验证最终令牌。 - 浏览器把
exposureKey带到托管认证界面,但永远看不到hiddenKey。 via.sudomimus.com执行通行密钥、邮箱验证码、OAuth 或联合登录挑战,只有认证成功后才创建confirmationKey。
这套生命周期只属于 Connect。OIDC 使用 authorization code + PKCE;原生 direct-issue 则在一次请求中交换 Steam ticket 或 AccessKey。
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 res = await fetch("https://connect-api.sudomimus.com/establish", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `SudomimusClientJWT ${await signEstablishJwt(body)}`, }, body: JSON.stringify({ applicationAnchor: process.env.SUDOMIMUS_APPLICATION_ANCHOR, returnMethods: [ { type: "CALLBACK", payload: { callbackUrl: "https://your-app.com/auth/callback" }, }, ], }),});
const { exposureKey, hiddenKey } = await res.json();import os, requests
res = requests.post( "https://connect-api.sudomimus.com/establish", headers={ "Authorization": f"SudomimusClientJWT {sign_establish_jwt(body)}", }, json={ "applicationAnchor": os.environ["SUDOMIMUS_APPLICATION_ANCHOR"], "returnMethods": [ { "type": "CALLBACK", "payload": {"callbackUrl": "https://your-app.com/auth/callback"}, }, ], },)
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)access token 是一个签名后的 JWT。验签时使用应用的 token-signing 公钥(从 POST /info 拉一次后缓存),即可信任其 claim。完整流程(包括 kty: "Access" header 检查)见 令牌与验证。
4. Refresh —— 维持会话有效
Section titled “4. Refresh —— 维持会话有效”在 access token 过期前,用 refresh token 换取一个新的 access token 和一个新的 refresh token。/refresh 不需要 client-auth JWT。refresh token 会被轮换 —— 你提交的令牌被消费,响应里返回它的替代者。请存下新的 refreshToken 并在下一次刷新时使用它;重复使用已经用过的令牌会导致整个会话被吊销。对同一个令牌近乎同时的并发刷新(例如多个标签页)会被容忍并收敛到同一个会话;只有在替代令牌签发之后再复用才会吊销它。
curl -X POST https://connect-api.sudomimus.com/refresh \ -H "Content-Type: application/json" \ -d '{ "refreshToken": "..." }'const res = await fetch("https://connect-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://connect-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://connect-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,可以从浏览器或其他不可信上下文中调用。
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, applicationPublicKey } = 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),)/info 返回的 applicationPublicKey 就是应用后端用于验证 access token 的密钥。请缓存它,仅在密钥轮换后重新拉取。