Connect with Spunto

Sign people into your app with their Spunto account — OAuth 2 authorization code with PKCE, and who they are in which organizations.

Spunto is an OAuth 2 provider for its apps. "Connect with Spunto" tells your app who a member is, which organizations they belong to, and with which role — enough to show each member what belongs to their organization, and to let only owners and admins change it.

It is what the page where an organization manages its own machines uses: that page lives on the app's domain, not in the dashboard, and needs to know who is looking at it.

FlowAuthorization code, PKCE S256 required
client_idYour app's id, from its manifest
client_secretYour app's signing secret — the one secret you and Spunto already share
Redirect URIsThose your manifest declares in oauth.redirectUris, compared exactly. HTTPS only (http is accepted for localhost). Once listed, only those that were reviewed
Who can sign inUntil your app is listed, only members of the organization that registered it
Scopesidentity
Access tokenOpaque (spo_…), valid 1 hour. No refresh token

1

Send the user to Spunto

Generate a code_verifier and a state, keep both (a short-lived signed cookie does), and redirect:

https://spunto.net/api/oauth/authorize
  ?response_type=code
  &client_id=acme-git
  &redirect_uri=https%3A%2F%2Fgit.acme.dev%2Fspunto%2Foauth%2Fcallback
  &scope=identity
  &state=…
  &code_challenge=BASE64URL(SHA256(code_verifier))
  &code_challenge_method=S256

A user who isn't signed in to Spunto signs in first. The first time, they see a consent screen; after that, the consent is remembered for your app and these scopes, and they come straight back.

2

Get the code back

Spunto redirects to your redirect_uri with ?code=…&state=…. Check state. The code is valid for 60 seconds and works once.

3

Exchange it, server to server

curl -X POST https://spunto.net/api/oauth/token \
  -d grant_type=authorization_code \
  -d code=… \
  -d redirect_uri=https://git.acme.dev/spunto/oauth/callback \
  -d code_verifier=… \
  -d client_id=acme-git \
  -d client_secret=…

Form-encoded or JSON; the client credentials may also go in an HTTP Basic header.

{ "access_token": "spo_…", "token_type": "Bearer", "expires_in": 3600, "scope": "identity" }
4

Ask who it is

curl https://spunto.net/api/oauth/userinfo -H "Authorization: Bearer spo_…"
{
  "sub": "usr_…",
  "name": "Ada Lovelace",
  "email": "ada@acme.dev",
  "picture": "https://…",
  "orgs": [{ "id": "org_…", "name": "Acme", "slug": "acme", "role": "admin" }]
}

Then open your own session — a cookie on your domain — with what userinfo said. When it expires, run the flow again: with the consent remembered, it costs the user no click.

  • Errors on an unknown redirect URI are not redirected. If the redirect_uri is not one your manifest declares, Spunto shows the error itself rather than sending the browser to an address it can't vouch for.
  • A role read at sign-in is a role at sign-in. If an admin is demoted, your app learns it at their next connection. Keep your sessions short if that matters to you.
  • org.id is the same id Spunto sends in every capability call — it is how you match a signed-in member to the installations of their organization.
  • Serve your pages on your own origin. OAuth only means something between separate origins; never mount your app's pages under Spunto's domain.
  • An API key can't authorize. Only a member signed in to the dashboard can consent; an spk_ key is refused.

Note

identity acts on nothing — it only says who the user is. Scopes that let an app act on Spunto on the user's behalf (reading their sandboxes, say) will use the same tokens when they come. Until then, an app that needs to call Spunto's API does it with an API key the organization gives it.