With single sign-on (SSO), people signed in to your app are signed in on your portal and in the widget too, without a separate account. Your server signs a short-lived token for the user and sends them to your portal. SSO works on every plan. Owners and admins set it up in Settings → Single sign-on.

1. Enable SSO

  1. Open Settings → Single sign-on and choose Enable SSO.

  2. Copy the secret. It starts with tsso_ and is shown only once.

  3. Keep it on your server only, for example as TUTI_SSO_SECRET. Never send it to the browser.

Settings → Single sign-on with SSO enabled, Your app's sign-in page filled in, and the example code
  • Rotate secret makes a new secret. Tokens signed with the old one stop working at once, so update your server straight away.

  • Disable turns SSO off. Your app's sign-in links stop working at once. People can still sign in on the portal with email.

2. Sign a token on your server

The token is a JWT signed with HS256 using the secret.

Claim

Required

What it's for

sub

Yes

Your user's ID. Tuti knows the user by it, so keep it stable.

jti

Yes

A unique ID per token, such as a UUID. Each token works once.

iat

Yes

When it was made.

exp

Yes

When it expires: at most 10 minutes after iat. A few minutes is plenty.

email

No

The user's email address. See "The email claim" below.

name

No

Shown on their posts and comments. Without it, the part of the email before the @ is used.

avatar

No

An https address of the user's picture.

groups

No

Their groups in your app, like ["enterprise", "beta"]. Kept as their groups property.

properties

No

What you know about the user, like { "plan": "pro", "seats": 12 }, for reader groups.

In Node, with the jose library:

import { SignJWT } from "jose";

const token = await new SignJWT({
  email: user.email,
  name: user.name,
  groups: user.isBetaTester ? ["beta"] : [],
  properties: { plan: user.plan, role: user.role },
})
  .setProtectedHeader({ alg: "HS256" })
  .setSubject(String(user.id))
  .setJti(crypto.randomUUID())
  .setIssuedAt()
  .setExpirationTime("5m")
  .sign(new TextEncoder().encode(process.env.TUTI_SSO_SECRET));

Any JWT library works the same way.

Properties

properties is an object of what your app knows about the user, by name. Reader groups match their conditions against it, and Users shows it. See See your users and the companies they're at.

  • Names are up to 64 characters. Values are text (up to 200 characters), numbers, true or false, or lists of up to 50 pieces of text.

  • A user has up to 50 properties. New ones past that are left out.

  • Each sign-in sets the properties it names and keeps the rest. Send a property as null to remove it. A token without properties changes none.

  • A property Tuti can't read, like an object or a list of numbers, is left out on its own. It never stops the user signing in.

  • attributes, the old name for properties, still works.

groups is stored as the groups property, up to 20 per user. Each is up to 50 letters, digits and -, _, . or :, starting with a letter or digit, and Tuti keeps it in lowercase. Unlike properties, a group Tuti can't read stops the sign-in (invalid_claims). A token with groups replaces the ones Tuti has, and [] removes them.

You can also send properties without a sign-in, through the API's users.sync. See See your users and the companies they're at.

The email claim

Tuti never uses email to sign anyone in: users are matched by sub only. But the address counts in two places:

  • Someone who signs in to your portal another way, with that address verified, gets this user's groups.

  • In the widget's Messages, the user sees the conversations emailed to your support address from that address.

So send only an address your app has verified. See Let people message your team from the widget.

3. Send the user to your portal

Send them to your portal's /sso with the token:

https://your-project.tuti.so/sso?token=<token>&redirect=<path>

The exact address is in Settings → Single sign-on. It works on your custom domain or sub-folder too.

  • redirect is optional: a path on your portal, such as /feedback/feature-requests. Without it, or if it isn't a path on your portal, they land on the portal's home.

  • Make the token right before sending them. Tokens expire quickly and only work once, so don't put them in emails or cached pages.

A common pattern is a "Feedback" link in your app to a route that signs a token for the current user and redirects.

Add "Continue with" to the portal's sign-in page

Once SSO is on, Your app's sign-in page adds a Continue with button, with your project's name, to the portal's sign-in page.

  1. Enter an https:// address in your app, like https://app.yourcompany.com/tuti, and choose Save.

  2. The button sends people there with redirect, the portal page to come back to.

  3. Sign them in to your app if they aren't yet, then send them to /sso as in step 3, with that redirect.

Leave the address empty to take the button away. See how it looks in Make your portal private.

Sign people in to the widget

The widget runs on your site, where your portal's sign-in can't reach it. Give it a way to get a fresh token from your server:

<script>
  window.TutiSettings = {
    identify: () => fetch("/tuti-token").then((response) => response.text()),
  };
</script>
<script src="https://tuti.so/widget.js" data-project="your-project" async></script>

/tuti-token is a route in your app that signs a token as in step 2. People your app signs in can then post, vote and message your team from the widget as themselves, and read a private portal there. The widget keeps their sign-in for up to 12 hours. See Open the widget from your own code.

If sign-in fails

The user sees "Couldn't sign you in" on your portal, with a reason and a code:

Code

What's wrong

sso_not_enabled

SSO is off for this project.

invalid_token

The token isn't signed with the project's secret using HS256, it's malformed, or it lacks sub, jti, iat or exp.

expired_token

exp has passed. Make the token right before redirecting, and check your server's clock.

token_lifetime_too_long

exp is more than 10 minutes after iat, or iat is in the future.

token_already_used

A token with this jti was already used. Make a new token for each sign-in.

invalid_claims

A claim has the wrong type or form, like an email that isn't an email address, or a group Tuti can't read.

Keep it safe

  • Each sub is its own user on your portal. Your tokens can't sign anyone in as another project's user or an existing Tuti account, even with the same email.

  • Signing a token is the same as signing in as that user on your portal, so protect the secret like a password.