Connect 流程
本页讲解 Connect 协议:当应用希望直接控制浏览器登录往返时使用的 Sudomimus 流程。Connect 走 JSON over HTTPS,任何带 HTTP 客户端的后端语言都能使用;下面给出 curl、Node.js、Python、Go 四种示例。
生产代码中,优先使用已有的官方 SDK。可以先看 选择 SDK,或直接跳到使用 @sudomimus/connect 的 TypeScript SDK。
如果你做的是原生客户端(桌面、游戏、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 | 应用后端 | session-api POST /refresh | 新 access token 与轮换后的 refresh token |
三个参与方各自承担不同责任:
- 应用后端签名
/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。
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。先读取其中的 kid,再从应用的 Session JWKS GET /applications/{applicationAnchor}/jwks.json 选择对应密钥并验签,验签通过后才能信任其中的 claim。完整流程(包括缓存刷新和 kty: "Access" header 检查)见 令牌与验证。
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),)令牌验证密钥从 GET https://session-api.sudomimus.com/applications/{applicationAnchor}/jwks.json 获取。请按 Cache-Control 缓存响应、按 JWT 的 kid 精确选取密钥,并在遇到未知 kid 时立即刷新一次。