Home For developers

For developers

Single sign-on from your own website or app.
By Faizan Shaikh
• 2 articles

SSO developer guide

Connect your website or app to a TribeCrafter community, so your users sign in to the community with the account they already have. How it works 1. A user is signed in on your platform and goes to the community. 2. Your server makes a short JWT that says who the user is, signed with your private key. 3. Your server sends the user's browser to the community's callback URL with that token. 4. The community checks the token with your public key. It signs the user in, and on their first visit it creates their account as a member. The community never calls your API. Everything it learns about a user comes from the token. The one exception: if you send a photo link, it downloads the photo once. Before you start The owner of the community saves the SSO settings in Studio. Only the owner can do this. You work out the values together: You give the owner: | Item | Where it goes in Settings → Sign-in & SSO | |---|---| | Public key, key name (kid) and type (ES256 or RS256). See SSO keys explained: public vs private. | Public keys | | The page where users sign in on your platform, for example https://acme.com/login | Your login page | | Button text, for example "Continue with Acme" (up to 40 characters) | Button label | | Optional: the page that signs users out on your platform | Your logout page | All addresses must start with https://. The owner gives you: | Item | Value | |---|---| | Audience (aud) | https:// plus the address under Settings → Domain → Your community address, for example https://your-community.tribecrafter.app. It never changes, even after the owner adds their own domain. | | Callback URL | https:// plus the address members use, plus /sso/callback. With the community's own domain: https://community.acme.com/sso/callback. Without one: https://your-community.tribecrafter.app/sso/callback. | | Member status URL (optional) | Only if you want to suspend members from your server. The owner asks TribeCrafter for it through Need help? in Studio. | Settings → Domain Own domain: a member's session works only on the address in the callback URL. If members use the community's own domain, put the callback there. The owner also fills in Community address for sign-in with that address (for example https://community.acme.com). Members who click your button on the .tribecrafter.app address are then moved to that address first. If the domain changes later, change the callback URL and that field at the same time. The audience stays the same. Test, then go live In Settings → Sign-in & SSO, under Sign in with your website (SSO), the owner picks Off, Testing or Live and clicks Save changes: | Mode | What happens | |---|---| | Off | Members sign in with email and password. Tokens are refused ("SSO isn't switched on"). | | Testing | Your sign-in links work, but the login page shows no button yet. Use this while you test. | | Live | The login page shows your button first. Needs Button label and Your login page. | Accounts created or linked in Testing are real community accounts. Test with real user ids and emails from your production system, and with an ordinary member, not an admin. 1. The token A JWT signed with your private key. Header | Field | Value | |---|---| | alg | ES256 or RS256, the same as the key type the owner picked in Studio (ES256 or RS256) | | kid | The key name saved in Studio | Claims | Claim | Required | Value | |---|---|---| | sub | Yes | Your permanent user id, as a string (up to 255 characters). Never reuse it for another person. | | email | Yes | The user's email. It must be a real address (not .local or .test). | | email_verified | Yes | true. Send a token only for an email you have verified. | | aud | Yes | The audience. | | iat | Yes | Issued at, in seconds. | | exp | Yes | Expiry. At most 5 minutes after iat. We suggest 2 minutes. | | jti | Yes | A random unique id, for example a UUID. Each token works once. | | given_name | No | First name (up to 50 characters are kept). | | family_name | No | Last name (up to 50 characters are kept). | | name | No | Full name, if you don't have first and last name. | | picture | No | An https:// link to the profile photo. | Leave out claims you don't have. Empty values are ignored. What the claims do: - Email follows your platform. A changed email updates the community account on the next sign-in. The member gets a change notice and is signed out on their other devices. Members can't change it in the community. - Names and photo only fill in a new account. After that, the member owns them, and later tokens don't change them. - Roles: every SSO user is a member. Admins give roles in the community. The token can't. - Existing account with the same email: the user is signed in to that account and it is linked to your sub. - Community Admins, Moderators, Course Managers and Blog Managers can't sign in through SSO, and that includes the owner. They click Log in with email instead and use their email and password. Example payload { "sub": "user_18422", "email": "sam@example.com", "email_verified": true, "given_name": "Sam", "family_name": "Rivera", "picture": "https://cdn.example.com/avatars/18422.jpg", "aud": "https://your-community.tribecrafter.app", "iat": 1790000000, "exp": 1790000120, "jti": "3b1f8f0e-6a8e-4a55-9d5e-2f1c9a7b1e44" } 2. Sign the token (on your server only) Node.js (npm i jose) import { SignJWT, importPKCS8 } from 'jose'; import { randomUUID } from 'node:crypto'; const privateKey = await importPKCS8(process.env.TC_SSO_PRIVATE_KEY, 'ES256'); export async function communityToken(user) { return new SignJWT({ email: user.email, email_verified: true, given_name: user.firstName, family_name: user.lastName, picture: user.avatarUrl, }) .setProtectedHeader({ alg: 'ES256', kid: 'acme-2026' }) .setSubject(String(user.id)) .setAudience('https://your-community.tribecrafter.app') .setIssuedAt() .setExpirationTime('2m') .setJti(randomUUID()) .sign(privateKey); } PHP (composer require firebase/php-jwt) use Firebase\JWT\JWT; function community_token(array $user): string { $now = time(); $payload = [ 'sub' => (string) $user['id'], 'email' => $user['email'], 'email_verified' => true, 'given_name' => $user['first_name'], 'family_name' => $user['last_name'], 'aud' => 'https://your-community.tribecrafter.app', 'iat' => $now, 'exp' => $now + 120, 'jti' => bin2hex(random_bytes(16)), ]; $privateKey = getenv('TC_SSO_PRIVATE_KEY'); // PEM contents return JWT::encode($payload, $privateKey, 'ES256', 'acme-2026'); } Python (pip install pyjwt cryptography) import os, time, uuid, jwt def community_token(user): now = int(time.time()) payload = { "sub": str(user.id), "email": user.email, "email_verified": True, "given_name": user.first_name, "family_name": user.last_name, "aud": "https://your-community.tribecrafter.app", "iat": now, "exp": now + 120, "jti": str(uuid.uuid4()), } return jwt.encode(payload, os.environ["TC_SSO_PRIVATE_KEY"], algorithm="ES256", headers={"kid": "acme-2026"}) 3. Send the user to the community Make the token at the moment of the redirect. It expires in minutes and works once. POST (recommended). It keeps the token out of URLs and browser history. Return this page from your server: <!doctype html> <html><body onload="document.forms[0].submit()"> <form method="post" action="https://community.acme.com/sso/callback"> <input type="hidden" name="token" value="{{TOKEN}}"> <input type="hidden" name="return_to" value="{{OPTIONAL_PATH}}"> <noscript><button type="submit">Continue to the community</button></noscript> </form> </body></html> GET also works: 302 Location: https://community.acme.com/sso/callback?token={{TOKEN}} When to send a user: - They clicked your button on the community's login page (Live only). The community sends them to Your login page. Once they are signed in with you, sign a token and send them to the callback. Leave return_to out: the community remembers the page they wanted for 30 minutes. - They clicked a community link on your platform. Sign a token and send them to the callback. To open a page, add return_to with a path inside the community, for example /groups/photography. It must start with one /. Full URLs and sign-in pages (/login, /logout, /register, /sso/...) are ignored. Where they land: on the first visit, the community's onboarding, with the name and photo filled in if you sent them. After that, the page they asked for, or the feed. 4. Log out When a member who signed in through SSO logs out of the community, the community ends its session. If Your logout page is set, it then sends them there. That page must sign the user out on a plain GET, with no confirmation step. Without it, the user stays signed in on your platform, and your button signs them straight back in. Logging out on your platform doesn't end an open community session. 5. Remove a user Stop making tokens for them, and they can't sign in through SSO again. A session they already have stays open. To end it, suspend them: - A Community Admin or Moderator suspends them in Studio (Members, then Suspend member). They are signed out at once. - Or your server suspends them with the member status API below. They are signed out on their next page load. A suspended member can't sign in again, through SSO or by email, until an admin clicks Reactivate member in Studio or your server sends reinstate. Suspend or reinstate from your server (optional) Send a POST to the member status URL with the JSON body {"token": "<token>"}. Sign the token with the same key as a sign-in token, with these claims: | Claim | Value | |---|---| | aud | The audience plus /sso/members, for example https://your-community.tribecrafter.app/sso/members. A sign-in token is refused here, and this token can't sign anyone in. | | sub | The user's id, the same as at sign-in. | | action | "suspend" or "reinstate". | | iat, exp, jti | Same rules as sign-in: at most 5 minutes, a new jti each time. | Leave out email and the profile claims. The header is the same as for sign-in. // Node.js, same key and imports as above. action: 'suspend' or 'reinstate' export async function setCommunityMemberStatus(userId, action) { const token = await new SignJWT({ action }) .setProtectedHeader({ alg: 'ES256', kid: 'acme-2026' }) .setSubject(String(userId)) .setAudience('https://your-community.tribecrafter.app/sso/members') .setIssuedAt() .setExpirationTime('2m') .setJti(randomUUID()) .sign(privateKey); const res = await fetch(process.env.TC_MEMBER_STATUS_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }), }); const body = await res.json(); if (!res.ok) throw new Error(`Community member update failed: ${res.status} ${body.error}`); return body; // { sub, status: 'suspended' | 'active', changed: true | false } } - Suspend suspends the account and ends all its sessions. - Reinstate lifts a suspension, made by you or in Studio. It changes nothing else. - Both are safe to repeat. changed is false when the member was already in that state. - Members only. Community Admin, Moderator, Course Manager and Blog Manager accounts are never changed. - After a network error or a 500, sign a new token and try again. A token sent twice is refused. | Status | Body | Meaning | |---|---|---| | 200 | {"sub": "user_18422", "status": "suspended", "changed": true} | Done. | | 400 | {"error": "invalid_token"} | Wrong key, kid, alg or aud, a missing claim, an unknown action, or the token was changed. | | 401 | {"error": "token_expired"} | exp has passed, the lifetime is over 5 minutes, or your server clock is off. | | 401 | {"error": "token_replayed"} | The same token was sent twice. | | 403 | {"error": "sso_disabled"} | SSO is Off, or no key is saved. | | 403 | {"error": "staff_account"} | The user is a Community Admin, Moderator, Course Manager or Blog Manager. Nothing changed. | | 404 | {"error": "member_not_found"} | No community account has this sub. Users who never signed in have no account. | | 409 | {"error": "cannot_reinstate"} | The account isn't suspended or active, for example a removed account. | | 500 | {"error": "server_error"} | A problem on our side. Try again with a new token. | Errors your users might see A failed sign-in opens a page on the community with one of these headings: | Heading | Likely cause | |---|---| | "Sign-in link invalid" | Wrong key, kid, alg or aud, a missing claim, an email we can't use, a sub over 255 characters, or the token was changed. | | "Sign-in link expired" | exp has passed, exp is more than 5 minutes after iat, iat is in the future, or your server clock is off. Keep it synced with NTP. | | "Sign-in link already used" | The same token was sent twice. Make a new token for each redirect. | | "Email not verified" | email_verified is missing or not true. | | "Account suspended" | The member is suspended. | | "Staff account" | The user is a Community Admin, Moderator, Course Manager or Blog Manager. They log in with email and password. | | "Already signed in" | The browser is signed in to the community as someone else. The user logs out of the community first. | | "Email already in use" | Another account in the community has this email, or the email is linked to another sub. | | "SSO isn't switched on" | SSO is Off, or no key is saved. | | "Too many sign-in attempts" | Too many sign-ins from one network in a minute. Wait a minute. | | "Something went wrong" | A problem on our side. Try again. | Community sign-in error page, "SSO isn't switched on" heading Test checklist 1. A new user goes through onboarding with their profile filled in, then lands on the feed. 2. The same user comes back and goes straight in. 3. A user who already had a community account with the same email lands in that account. 4. After you change a user's email, their next sign-in updates it in the community. 5. A link from your platform with return_to opens that page. 6. Sending the same token twice is refused, and so is an expired token. 7. In Live: from a logged-out community page, your button brings the user back to that page. 8. Logging out of the community goes to your logout page, if set. 9. An admin's email sent through SSO shows "Staff account". 10. If you use the member status API: suspending a test member signs them out on their next page load. Reinstating lets them sign in again. Security checklist - Sign tokens and call the member status API only from your server. The private key never goes into a browser, an app or a repository. - Set email_verified: true only for emails you have verified. - Use a sub that never changes and is never reused. - Make a new token (new jti) for every redirect, with an expiry of 5 minutes or less. - Use HTTPS everywhere. Related - SSO keys explained: public vs private - Sign-in and single sign-on (SSO) - Connect your own domain

Last updated on Nov 05, 2026

SSO keys explained: public vs private

Make the key pair for single sign-on (SSO), add the public key in Studio, and change or replace keys later without downtime. Only the owner can add or remove keys. The owner is the person who created the community in their TribeCrafter account. Other admins see "Only the community owner can change sign-in settings." If you are the developer, you make the keys and the owner pastes the public key into Studio. The two keys SSO uses a key pair: 2 files that are made together and only work together. | | Private key | Public key | |---|---|---| | File (with the commands below) | tc-sso-private.pem | tc-sso-public.pem | | First line | -----BEGIN PRIVATE KEY----- | -----BEGIN PUBLIC KEY----- | | What it does | Signs the sign-in tokens your server makes | Lets the community check those tokens | | Where it goes | Only your server | Studio, under Public keys | | If someone else gets it | They can sign in to the community as any member. Replace it at once. | Nothing. It can't make tokens. | Before you paste a file, open it in a text editor. If the first line says PUBLIC KEY, paste it. If it says PRIVATE KEY, stop. TribeCrafter never makes, sees or stores your private key. Studio refuses a private key. Make the key pair Use ES256 (an EC P-256 key). RS256 also works, with an RSA key of at least 2048 bits. # ES256 openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out tc-sso-private.pem openssl pkey -in tc-sso-private.pem -pubout -out tc-sso-public.pem # or RS256 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out tc-sso-private.pem openssl pkey -in tc-sso-private.pem -pubout -out tc-sso-public.pem Run the commands on your own computer or server. They work in a terminal on macOS and Linux, and in Git Bash or WSL on Windows. Without OpenSSL, Node.js makes the same ES256 pair: node -e "const {generateKeyPairSync}=require('crypto');const fs=require('fs');const k=generateKeyPairSync('ec',{namedCurve:'P-256',privateKeyEncoding:{type:'pkcs8',format:'pem'},publicKeyEncoding:{type:'spki',format:'pem'}});fs.writeFileSync('tc-sso-private.pem',k.privateKey);fs.writeFileSync('tc-sso-public.pem',k.publicKey)" Then: 1. Store tc-sso-private.pem with your server's other secrets (an environment variable or a secrets manager). Don't email it, commit it to a repository, or put it in a browser or mobile app. 2. Pick a key name, for example acme-2026. Your server puts it in the kid header of every token, so the community knows which key to check. 3. Give the owner the key name, the type (ES256 or RS256) and the contents of tc-sso-public.pem. Add the public key in Studio 1. In Studio, go to Settings → Sign-in & SSO. 2. Under Public keys, click Add a public key. Settings → Sign-in & SSO, Public keys 3. In Key name (kid), enter the key name, exactly as your server sends it in kid (up to 100 characters). 4. Under Key name (kid), pick ES256 or RS256, the same as your key. 5. In Public key, paste the whole public key, from -----BEGIN PUBLIC KEY----- to -----END PUBLIC KEY-----. 6. Click Save changes. You see "Changes saved successfully." After you save, the key shows its name, its type and the date it was added. Studio does not show the key text again. To change a key, remove it and add a new one. Rules - At most 2 keys at once. Add a public key is hidden when there are 2. - Each key name is used once. - An ES256 key must be an EC P-256 public key. An RS256 key must be an RSA public key of at least 2048 bits. - Testing and Live need at least one key. Replace a key without downtime 1. Make a new pair with a new key name. 2. The owner adds the new public key in Studio, next to the old one, and clicks Save changes. 3. Switch your server to the new private key and the new kid. 4. The owner clicks the trash icon next to the old key, then clicks Save changes. If the private key is lost or leaked - Lost (for example, a server was rebuilt): make a new pair and add the new public key. SSO sign-ins fail until your server signs with a key the community has. - Leaked, or sent to anyone by mistake: make a new pair, add the new public key, and remove the old key in Studio straight away. If something goes wrong | Message | What to do | |---|---| | "Paste the public key (it starts with -----BEGIN PUBLIC KEY-----)." | You pasted something else, often the private key. Paste the contents of the public key file. | | "Add a public key before you turn SSO on." | Add a key, then pick Testing or Live. | | "Sign-in link invalid" (on the community, after a sign-in) | The kid or alg in the token doesn't match a saved key, or the token was signed with another private key. | Related - SSO developer guide - Sign-in and single sign-on (SSO)

Last updated on Nov 05, 2026