Signing in your users
Your users already have an account on your product. Your server vouches for them with a signed identity token, and they post, vote and comment as themselves, without a second sign-in.
An identity token works in two places:
- The widget: your page hands the token to the frame. See Widget.
- The portal, with single sign-on: the portal sends people to your product to sign in, and your product sends them back with a token.
Identity secrets#
Your server signs tokens with an identity secret. Owners and admins manage them under Settings → Sign-in:
- Choose Create a secret. The secret,
si_…, shows once: put it in your secrets manager. - Note the secret's id,
idsec_…, which the page lists. Tokens name it in theirkidheader. - Note your workspace's id,
ws_…, which the same page shows. Tokens name it in theirissclaim.
A workspace has at most 3 live secrets. To rotate one, create a new secret, move your servers to it, then revoke the old one. Tokens the old secret signed stop working at once. The page shows when each secret was last used.
The token#
An identity token is a JWT signed with HS256. The key is the whole secret, si_ included, as text. Its header is
{ "alg": "HS256", "typ": "JWT", "kid": "idsec_…" }: alg must be HS256, and typ may be left out.
| Claim | Required | Value |
|---|---|---|
iss | Yes | Your workspace's id, ws_…. |
aud | Yes | swingback, or an array that contains it. |
sub | Yes | Your user's id in your product, up to 255 characters. It becomes the customer's external id. |
iat | Yes | When you signed the token, in seconds since the Unix epoch. |
exp | Yes | When it expires: at most an hour after iat. |
nbf | No | When it starts to work. |
jti | For SSO | A unique id, up to 128 characters. Single sign-on needs it and takes each token once. |
Clocks may differ by 60 seconds. A token can be up to 16 KB long.
The profile claims are optional:
email,name(up to 100 characters) andavatar_url(an http or https address).traits: facts about the user, such as{ "plan": "pro", "seats": 12 }. A key starts with a letter and has up to 64 letters, digits or underscores. A value is text (up to 256 characters), a number ortrue/false;nullremoves the trait.companies: up to 20 companies the user belongs to. Each has anid, your id for it, and optionally aname, adomain, avalueandtraits. Thevalueis a whole number, such as its monthly revenue in cents.
For example:
{
"iss": "ws_01h455vb4pex5vsknk084sn02q",
"aud": "swingback",
"sub": "user_8412",
"iat": 1790640000,
"exp": 1790640600,
"jti": "3f1c9e2a-5b7d-4c8e-9a1f-2d6b8e4c7a90",
"email": "dana@example.com",
"name": "Dana Reyes",
"traits": { "plan": "pro", "seats": 12 },
"companies": [{ "id": "org_311", "name": "Initech", "value": 49900 }]
}What a token does#
Swingback finds your customer by sub, else by email, else creates one. Someone who signed in to your portal
with that email before becomes the same customer. Then the token brings the customer up to date:
email,nameandavatar_urlreplace what the customer had. A claim you leave out changes nothing.traitsset the keys they name, and leave the others. A key's first value sets its type. Tokens send dates as text, such as2026-09-29, so define a date trait under Settings → Traits first. A trait that can't be kept (a value of another type, a key past the workspace's 50) is dropped rather than refusing the sign-in.companiesis the whole list: the customer leaves the companies you don't name. Leave the claim out to keep them as they are. See Customers and companies.
A token whose email belongs to another customer, or whose profile isn't valid, is refused.
Sign a token in Node.js#
No library needed:
// identity-token.mjs: signs an identity token for your signed-in user. Node.js 18 or later.
import { createHmac, randomUUID } from "node:crypto";
const secret = process.env.SWINGBACK_IDENTITY_SECRET; // si_…, shown once under Settings → Sign-in
const secretId = process.env.SWINGBACK_IDENTITY_SECRET_ID; // idsec_…, the secret's id
const workspaceId = process.env.SWINGBACK_WORKSPACE_ID; // ws_…, your workspace's id
const encode = (object) => Buffer.from(JSON.stringify(object)).toString("base64url");
export function identityToken(user) {
const now = Math.floor(Date.now() / 1000);
const header = encode({ alg: "HS256", typ: "JWT", kid: secretId });
const claims = encode({
iss: workspaceId,
aud: "swingback",
sub: String(user.id),
iat: now,
exp: now + 10 * 60, // 10 minutes: at most an hour
jti: randomUUID(), // single sign-on takes each token once
email: user.email,
name: user.name,
traits: { plan: user.plan, seats: user.seats },
companies: [{ id: String(user.company.id), name: user.company.name, value: user.company.revenueCents }],
});
const signature = createHmac("sha256", secret).update(`${header}.${claims}`).digest("base64url");
return `${header}.${claims}.${signature}`;
}Sign a token only for the user signed in to your product, from an endpoint of yours that checks their session. Keep tokens short-lived, and out of addresses, except single sign-on's, which work once.
In the widget#
When the frame says it's ready, your page posts the token to it:
frame.contentWindow.postMessage({ source: "swingback", type: "identify", token }, "https://acme.example.com");The frame answers identified, or error with a code such as invalid_identity_token. A token works until it
expires, so the same one can sign in again after a reload. Widget has a whole page as an example.
Single sign-on for the portal#
With single sign-on, the portal's sign-in page offers Continue with your workspace's name, which takes people to
your product. Under Settings → Sign-in, enter your Sign-in page: an https address on your product, such as
https://app.example.com/feedback-sign-in. The email code, and Google or GitHub where offered, stay on the portal's
sign-in page. Leave the field empty to turn single sign-on off.
- The portal sends the visitor to your sign-in page with a
return_toparameter: your portal's/auth/ssoaddress, with parameters of its own (astate, sometimes anextpage). - Your page signs the visitor in to your product, if they aren't already.
- Your page sends them to
return_toas it is, with atokenparameter added: a token that has ajti. - The portal signs them in and takes them back to where they were. What they wrote as a guest in that browser becomes theirs.
// Once the visitor is signed in to your product, redirect the browser to the address this returns.
import { identityToken } from "./identity-token.mjs";
const portal = "https://acme.example.com";
export function ssoRedirect(returnTo, user) {
const url = new URL(returnTo);
// Only to your portal: a link someone else made must not carry your user's token elsewhere.
if (url.origin !== portal || url.pathname !== "/auth/sso") throw new Error("return_to isn't your portal");
url.searchParams.set("token", identityToken(user));
return url.toString();
}The portal takes each token once: a jti it has seen is refused, and so is any other bad token. A refused token
brings the visitor back to the sign-in page with an error.
A link of your product can also point straight to your portal's /auth/sso?token=…, with a fresh token. The portal
then asks the visitor to confirm before it signs them in.
