Connect flow
This page covers the Connect protocol: the browser-mediated Sudomimus flow used when your application wants direct control over the login round-trip. Connect speaks JSON over HTTPS, so any backend language with an HTTP client works; examples below are in curl, Node.js, Python, and Go.
If you’re building a native client (desktop, game, CLI), see Native clients. For OIDC, see OIDC relying parties.
Tabs are synchronised across the page: pick your language once and every block below switches with it.
Protocol at a glance
Section titled “Protocol at a glance”| Phase | Initiator | Endpoint | Result |
|---|---|---|---|
| 1. Establish | Application backend | connect-api POST /establish | { exposureKey, hiddenKey } |
| 2. Authenticate | Browser | via.sudomimus.com | The user completes an allowed challenge |
| 3. Redeem | Application backend | connect-api POST /redeem | { accessToken, refreshToken } |
| 4. Refresh | Application backend | connect-api POST /refresh | A new access token and rotated refresh token |
Three parties split responsibility:
- Your backend signs
/establish, storeshiddenKey, redeems the completed inquiry, and verifies the resulting tokens. - The browser carries
exposureKeyto the hosted authentication UI but never seeshiddenKey. via.sudomimus.comruns the passkey, email OTP, OAuth, or federation challenge and createsconfirmationKeyonly after authentication succeeds.
This lifecycle is specific to Connect. OIDC uses authorization code + PKCE, while native direct-issue exchanges a Steam ticket or AccessKey in one request.
1. Establish — start a session
Section titled “1. Establish — start a session”Your backend asks Connect to open an authentication session. The response gives you an exposure key (passed to the browser) and a hidden key (kept on the server).
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)Store hiddenKey against the user’s pending session (e.g. in a server-side store). Send the user to via.sudomimus.com with the exposureKey in the URL.
2. Authenticate — hand off to via.sudomimus.com
Section titled “2. Authenticate — hand off to via.sudomimus.com”Redirect the user’s browser to via.sudomimus.com with the exposure key. The user completes the passkey or email-OTP challenge there.
# No HTTP call — this is a 302 redirect from your application: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,)When the user finishes, via.sudomimus.com redirects the browser to your callbackUrl with exposure-key and confirmation-key appended as query parameters.
3. Redeem — exchange for a token
Section titled “3. Redeem — exchange for a token”In your callback handler, combine the three keys and exchange them at Connect for an access token plus a refresh token.
curl -X POST https://connect-api.sudomimus.com/redeem \ -H "Content-Type: application/json" \ -d '{ "exposureKey": "...", "hiddenKey": "...", "confirmationKey": "..." }'// inside 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();# inside 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"]// inside 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)The access token is a signed JWT. Verify it using your application’s token-signing public key — fetched once from POST /info and cached — then trust its claims. See Tokens and verification for the full verification recipe, including the kty: "Access" header check.
4. Refresh — keep the session alive
Section titled “4. Refresh — keep the session alive”Before the access token expires, exchange the refresh token for a fresh access token and a new refresh token. /refresh does not require a client-auth JWT. Refresh tokens are rotated — the token you present is consumed, and the response returns its replacement. Persist the new refreshToken and use it for the next refresh; re-using a spent one revokes the whole session. Near-simultaneous concurrent refreshes of the same token (e.g. multiple tabs) are tolerated and converge on one session; only reuse after the replacement has been issued revokes it.
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 }),});
// The refresh token is rotated — capture the new one and persist it,// replacing the token you just sent.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"]# The refresh token is rotated — persist the new one, replacing the old.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)
// The refresh token is rotated — persist data.RefreshToken, replacing the old one.store.SaveRefreshToken(data.RefreshToken)For introspection, logout, and account-wide revocation, see Managing sessions.
Looking up application metadata
Section titled “Looking up application metadata”POST /info returns the public profile of an application (name, public key, localized name) given its anchor. It does not require a client-auth JWT, so it is safe to call from browsers and untrusted contexts.
curl -X POST https://connect-api.sudomimus.com/info \ -H "Content-Type: application/json" \ -d '{ "applicationAnchor": "your-application", "locale": "en-US" }'const res = await fetch("https://connect-api.sudomimus.com/info", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ applicationAnchor, locale: "en-US" }),});
const { applicationAnchor: anchor, applicationName, applicationPublicKey } = await res.json();res = requests.post( "https://connect-api.sudomimus.com/info", json={"applicationAnchor": application_anchor, "locale": "en-US"},)
info = res.json()body, _ := json.Marshal(map[string]string{ "applicationAnchor": applicationAnchor, "locale": "en-US",})
res, _ := http.Post( "https://connect-api.sudomimus.com/info", "application/json", bytes.NewReader(body),)The applicationPublicKey returned by /info is the key your backend uses to verify access tokens for this application. Cache it; refetch only after key rotation.