Skip to main content
Partner SSO lets a platform partner put an Open in SpitShake button in its own console. The partner’s server signs a short-lived token for the console user, the browser sends it to SpitShake, and SpitShake starts a normal session in the managed account. There is no email, no magic link and no SpitShake password.

The secret

Each managed account has its own partner_sso_secret, an HS256 key.
  • It is minted at provisioning and returned once as partner_sso_secret (and on exact idempotent replays).
  • Rotate it, or mint the first one for an account provisioned before partner SSO existed, with POST /api/partner/v1/accounts/{id}/sso_secret/rotate. Tokens signed with the old secret are refused at once. A suspended account’s secret can still be rotated, as its API tokens can, so you can answer a leak before reactivating it. An archived account is read-only: rotation answers 409 (account_archived) and changes nothing.
200
Keep the secret on your server only; anyone holding it can sign into the account. Use the secret string itself as the HMAC key (do not hex-decode it).

The token

An HS256 JWT. Any other algorithm, including none, is refused. SpitShake allows 5 seconds of clock skew on iat and exp. Mint the token when the user clicks the button, not when the page loads. return_to must be a relative path such as /templates or /templates/123/edit. A value starting with //, containing a backslash, a scheme (https:, javascript:), spaces, CR/LF or non-ASCII characters is refused, and so is the whole sign-in.

Sending the user

GET or POST https://spitshake.io/sso/partner with the token in a token parameter, in the query string or a form body (application/x-www-form-urlencoded). Send it in one place only: a query token and a different form token are refused with 400. Prefer a form POST: it keeps the token out of browser history and access logs. Open it as a page (a link, a form, window.open), not from an image, iframe or fetch; those are refused. A HEAD request (link previews, scanners) gets 405 and neither signs anyone in nor uses up the token.
What happens next depends on whether that browser is already signed in to SpitShake:
  • Not signed in, or signed in as the same user: SpitShake starts the session and redirects (303) to return_to.
  • Signed in as someone else: SpitShake does not switch silently. It shows a page naming the current user and workspace and the one the token is for. Only after the user clicks Continue (a same-origin form with a CSRF token) is the token used and the session switched. The token must still be inside its minute when they click.
A POST from your console is a cross-site request, so the browser does not send its SpitShake session cookie with it. SpitShake answers it with a page that immediately resubmits the same token from SpitShake’s own origin, which does carry the cookie, and then proceeds as above. With JavaScript off, the page shows a Continue button instead.
Residual login-CSRF risk. Anyone holding a valid, unused token can still sign a browser that has no SpitShake session into that token’s account. For example, a page could post a token for an account the attacker controls, and the victim might then upload documents into it. Only you can mint tokens, with the account’s secret. Keep the secret on your server, mint tokens only for the signed-in console user who clicked the button, and never put a token where a third party can read it.
On any failure SpitShake returns the same 401 page and never says which check failed. The exceptions are for a correctly signed token only: a role SpitShake cannot grant (the page says read-only access is not available), the account’s sign-in limit (429, see Rate limits) and the plan’s user limit (403, see Users and roles). The reason is in SpitShake’s server log and, once the token’s signature has been verified, in the account’s audit trail as partner.sso_login_failed. A correctly signed token is audited only until a minute after its exp (plus the 5 s skew), whatever it is refused for; after that it is only logged, so a token found later in a browser history cannot fill the audit trail. A token whose exp is not an integer, or is further ahead than any valid token’s could be (60 s after an iat at most 5 s ahead), is never audited, only logged.

Minting a token

Users and roles

  • The first sign-in for a sub creates a SpitShake user in that account; later sign-ins with the same sub return to it. The same sub in two accounts is two separate users.
  • SSO signs in only users it created itself. It never links to an existing SpitShake user, even one with the same email. Every SSO user’s own SpitShake email is a non-routable placeholder (sso-…@partner-sso.invalid), whatever you send.
  • An SSO user signs in only through you. SpitShake refuses their password sign-in and their “Forgot password” requests, answering exactly as it does for an address it does not know, and they cannot set a SpitShake email or password of their own, whatever address they have. Disabling them is how you cut their access.
  • The email you send is the user’s contact: SpitShake sends it the mail it would send to the user’s email (security alerts to the account’s admins, and a document they sent being completed or declined). Each sign-in replaces it with the token’s email, and a sign-in without one, or with one that is not a valid address, clears it; with none, that mail is not sent. It is never verified, and it claims nothing: the same address can be another SSO user’s contact, or anyone’s SpitShake email, in any account. SpitShake never finds, signs in or sends a reset link to a user by it.
  • A new user is created confirmed, with a random password nobody knows. No welcome, confirmation or password-reset email is sent. Being confirmed gives them nothing beyond SSO: it only matters to password sign-in.
  • A first sign-in counts against the plan’s user limit, as an invitation does. When the account is full, the sign-in is refused with 403 and a page that says so; users who already exist still sign in. Size the plan for the users you will send.
  • The role follows the token on every sign-in, demotion included, with one exception: the account’s last active admin is never demoted, so the account always has an admin. They stay admin, and the sign-in’s partner.sso_login event records role_not_applied: "last_active_admin". admin and editor map to the same SpitShake roles.
  • There is no read-only access. Every SpitShake role can create and send documents, so a token with role: "viewer", with no role, or with any other value is refused rather than signed in with more access than you meant to grant. The refusal page says that read-only access is not available, so you can tell it apart from an expired link. Map your read-only users to no button, or to editor if they should be able to send.
  • A user who is archived in SpitShake, or disabled by you, cannot sign in by SSO.
  • SpitShake MFA is not asked for: your own sign-in is the gate. Send amr so the audit trail shows how the user authenticated with you.
  • Because an SSO user has no password they know, they cannot do what SpitShake confirms with the user’s password: turning off MFA, rotating the identity-handoff secret, or deleting the account. The API answers those requests with 403 and a message that says so (never the 401 of a wrong password, which would send the app to the password sign-in page), and the account settings page shows that message in place of its delete button. An admin who signs in with a password does those.

Disabling a user

When you remove a user on your side, disable them in SpitShake. Disabling archives the user, which blocks SSO and password sign-in. It also ends their open sessions: a tab that is already open is signed out on its next request. Their API and OAuth tokens are revoked. The subject is the token’s sub, URL-encoded. Disabling is a revocation, so it also works on a suspended or archived account.
200 (one account)
200 (every account)
The user is archived, their tokens are revoked and their sessions are invalidated in one database transaction, which is what refuses them. Deleting their server-side session records happens afterwards and is best effort: sessions_purged counts the records deleted. If the session store is unavailable, session_purge_failed is true, but the user is still disabled and every one of their sessions is still refused on its next request. Both calls are idempotent. A subject that is unknown or already disabled answers 200 with "already": true and changes nothing, so retrying is always safe. The every-account form reaches only your own accounts, and accounts_affected lists those where something changed. Each change writes a partner.sso_user_disabled audit event with the subject and the counts of sessions and tokens ended. Only users created by SSO are reached; other users of the account are never touched. There is no re-enable through the Partner API: an account admin can restore an archived user in SpitShake, and their tokens stay revoked.

Refusals

Besides an invalid token, SSO is refused for an account that is not partner-managed, has no partner SSO secret, belongs to a suspended partner, or is itself suspended or archived. Each of these gets the same generic 401 page as an invalid token. Each successful sign-in writes a partner.sso_login audit event with the user, sub, role, amr, a SHA-256 hash of the jti, and the client IP. Tokens are never logged or stored.

Rate limits

30 requests per minute per client IP (a POST launch takes two), and 60 sign-ins per minute per account. Only fresh tokens correctly signed for that account count against the account limit: forged tokens and replays of an already-used token are refused before it is touched, so neither can lock your users out. Over either limit SpitShake answers 429 with Retry-After. A token refused by the account limit is not used up, so the same token can be retried until it expires.