Let people sign in on your help site
Anyone can read the feedback boards on your help site. Posting, commenting and voting with one click need to know who the visitor is. Without a sign-in URL, the only way in is a link from your app that carries a token (the public board shows how).
With a sign-in URL, the board pages show Sign in in their header and next to the forms. It sends the visitor to an endpoint on your own site. Your endpoint checks who they are with your app's own sign-in, and sends them back to the page they came from with a short-lived token. Mocco never sees your users' passwords, and your users don't need an account with Mocco.
You build one endpoint and set its address in Mocco. This page shows what the endpoint must do, a complete example for Next.js, and how to set it up.
What you need
The project's identity secret. It is the secret your server signs the token with. Set up messenger on the project's Inbox page creates it and shows it once (In-app contact with Mocco Messenger). If you only use the feedback board, you still set up the messenger once to get the secret.
A server route on your site that can read who is signed in to your app.
To be an owner or admin of the workspace, to set the sign-in URL.
How signing in works
A visitor chooses Sign in on a board page, say
https://example.com/help/12?sort=new.The page sends the whole tab to your sign-in URL, with the page to come back to:
https://example.com/api/help/enter?return=https%3A%2F%2Fexample.com%2Fhelp%2F12%3Fsort%3Dnew&lang=enYour endpoint checks
return, signs the visitor in if they aren't yet, and answers302back to the page with the token in the fragment:https://example.com/help/12?sort=new#token=<jwt>.The page keeps the token for that browser tab, takes it out of the address without reloading, and the visitor can post, comment and vote.
The endpoint contract
The request. Mocco sends the visitor to:
GET <sign-in URL>?return=<URL>[&prompt=none]&lang=<ko|en>The parameters are added to any query your sign-in URL already has.
returnis the absolute https address of the page to come back to. It is the page's public address: under a path of your own site, your own address. It keeps the page's query, without any token, and has no fragment.prompt=nonemeans "don't show a sign-in screen". It is explained below, under checking silently.langis the page's language,kooren, for your endpoint's own screens.
Check return first. Your endpoint must only ever send people back to your help site. Parse return and accept it only when:
it uses
https;its origin is the same as one of the return addresses the console lists, and its path is that address's path or under it, at a
/boundary. An address without a path allows its whole origin.
So with https://example.com/help allowed, https://example.com/help and https://example.com/help/12 pass, while https://example.com/helpers, //evil.example and https://evil.example/help don't. When return doesn't pass, send the visitor to a fixed page of your own, such as your help home, never to return. When it passes, redirect to the address as you parsed it (URL.href in JavaScript), not to the raw text you were given. Without this check, your endpoint would hand your users' tokens to any site that links to it.
Signed in. Answer 302 to return with the token in the fragment, #token=<jwt>, replacing any fragment it had.
Not signed in.
With
prompt=none, answer302toreturn#signin=noneat once. Don't show a sign-in screen.Without it, run your own sign-in, keeping the request so you come back to it, then answer as above. If the person gives up, answer with
#signin=none.
The token. An HS256 JWT signed with the project's identity secret:
| Claim | Value |
|---|---|
sub | Your user's id, as a string. It can't start with email:. |
exp | When it expires: at most one hour from now. Ten minutes is plenty, since the page only needs it while the visitor is there. |
iat | When you signed it. |
name | Optional: the user's name, 1 to 200 characters. |
email | Optional: the user's email address, at most 320 characters. |
The feedback board uses only sub. name and email are read only by Messenger, so your team sees who wrote. A name or email Mocco can't use is left out, not refused. A token without sub or exp, with an exp more than an hour ahead, or signed with another secret, is refused.
Why the token rides in the fragment
The part of an address after # never leaves the browser. It isn't sent to your proxy, to Mocco or to any other server, and it isn't in the Referer header of the next request. So the token doesn't end up in anyone's request logs. The page reads it from the address and removes it at once.
The page keeps a #token= only when that same tab went to your sign-in URL in the last 10 minutes, through Sign in or a silent check. A link that carries a token from anywhere else is dropped, so nobody can sign a visitor in to someone else's account.
Links your app already builds with ?token= keep working.
Checking silently (prompt=none)
After someone has signed in on a board, the browser remembers that they did, without keeping any secret. When they come back later in a new tab, or their token expires while they are on a page, the page goes to your sign-in URL once with prompt=none:
If they are still signed in to your app, your endpoint sends them back with a fresh token, and they are signed in without doing anything. If their token expired while they were writing a post or a comment, what they wrote is kept.
If not, your endpoint answers
#signin=none. The page forgets that they were signed in and shows Sign in.
A page tries this at most once every 5 minutes in a tab, so an endpoint that misbehaves can't make it loop. Because the visitor is passing through, answer prompt=none quickly and never with a screen.
Sign out on a board page forgets the token in that browser only. It doesn't sign the person out of your app.
A Next.js example
This endpoint lives at pages/api/help/enter.ts, so its URL is https://example.com/api/help/enter. Don't put it under a path your site forwards to Mocco (such as /help or /docs), or the request would reach Mocco instead of your code.
It uses jose to sign the token. getSignedInUser stands for however your app reads its session; replace it with yours.
// pages/api/help/enter.ts
import { SignJWT } from 'jose';
import { getSignedInUser } from '@/lib/auth';
import type { NextApiRequest, NextApiResponse } from 'next';
// Copied from Help center → Signing in in Mocco's console.
const RETURN_ADDRESSES = ['https://example.com/help', 'https://orbit.help.mocco.club'];
// Where anyone with a return address you don't allow ends up.
const FALLBACK = 'https://example.com/help';
// Your app's own sign-in page. It sends the person back to `next` afterwards.
const SIGN_IN_PAGE = '/login';
const TOKEN_SECONDS = 10 * 60;
/** `raw` as parsed when it is a page under one of the return addresses, else null. */
function allowedReturn(raw: string | string[] | undefined): URL | null {
if (typeof raw !== 'string') {
return null;
}
let target: URL;
try {
target = new URL(raw);
} catch {
return null;
}
if (target.protocol !== 'https:' || target.username !== '' || target.password !== '') {
return null;
}
if (target.pathname.startsWith('//')) {
return null;
}
const isAllowed = RETURN_ADDRESSES.some(address => {
const base = new URL(address);
const path = base.pathname.replace(/\/+$/, '');
return (
target.origin === base.origin &&
(path === '' || target.pathname === path || target.pathname.startsWith(`${path}/`))
);
});
return isAllowed ? target : null;
}
export default async function enter(req: NextApiRequest, res: NextApiResponse) {
res.setHeader('Cache-Control', 'no-store');
const secret = process.env.MOCCO_IDENTITY_SECRET;
if (secret === undefined || secret === '') {
res.status(500).end('MOCCO_IDENTITY_SECRET is not set');
return;
}
const back = allowedReturn(req.query.return);
if (back === null) {
res.redirect(302, FALLBACK);
return;
}
const user = await getSignedInUser(req);
if (user === null) {
if (req.query.prompt === 'none') {
back.hash = 'signin=none';
res.redirect(302, back.href);
return;
}
// Sign in first, then come back to this same request.
res.redirect(302, `${SIGN_IN_PAGE}?next=${encodeURIComponent(req.url ?? '/api/help/enter')}`);
return;
}
const now = Math.floor(Date.now() / 1000);
const token = await new SignJWT({ name: user.name ?? undefined, email: user.email ?? undefined })
.setProtectedHeader({ alg: 'HS256' })
.setSubject(String(user.id))
.setIssuedAt(now)
.setExpirationTime(now + TOKEN_SECONDS)
.sign(new TextEncoder().encode(secret));
back.hash = `token=${token}`;
res.redirect(302, back.href);
}back.href is the address as parsed, so a return that only looked allowed can't slip through. Setting back.hash replaces any fragment it had. Without the secret the endpoint answers 500 rather than sign a token nobody can check.
The SIGN_IN_PAGE?next= redirect is only safe if your sign-in page checks next and only sends people on to paths of your own site.
If signing in seems to do nothing, check that the token is signed with the identity secret from the messenger setup: the page drops a token Mocco refuses.
If your sign-in page has its own language setting, pass req.query.lang along to it, so a visitor coming from a Korean page signs in in Korean.
Keep the identity secret on your server
The identity secret signs every one of your users' identities to Mocco. Anyone who has it can sign in as any of your users on your help site and in Messenger.
Keep it in a server-only environment variable, such as
MOCCO_IDENTITY_SECRET. In Next.js, never give it theNEXT_PUBLIC_prefix, and only read it in API routes or server code.Don't put it in your app, your web pages or your repository. Leave it blank in committed env files, and set it where your server is deployed.
Mocco never returns it over the API or to an agent. If it leaks, rotate it on the Inbox page. Tokens and messenger signatures made with the old one stop working at once.
Set the sign-in URL in the console
On the project's Help center page, find Signing in.
Type your endpoint's address in Sign-in URL and choose Save. It must use
httpsand be at most 500 characters, without a user name, password or#. A query is allowed.Copy the addresses under Return addresses to allow into your endpoint. They are your help site's addresses: each path on your own site that is active, and the help site's own address. When you add a path later, it joins the list once it is active, so add it to your endpoint too.
The section also says whether the project has an identity secret, with a link to set up the messenger when it doesn't.
Choose Try it. It opens your sign-in URL with a
returnon your help site, so you can see your endpoint send you back with#token=. The page doesn't keep a token it didn't ask for, so after Try it it still shows Sign in; choose it there to see the whole sign-in. Try a foreign address asks it to return tohttps://example.com/; your endpoint should send you to its fixed page instead.
Clear removes the sign-in URL. Board pages then stop showing Sign in, and links from your app that carry a token keep working. Members of the project can see the setting, but only owners and admins can change it. Every change is in the audit log.
Saving refreshes the board pages, so they show the new setting at once.
From an agent or a script
Agents over MCP read the sign-in URL and the return addresses with
mocco_help_site_get(signInUrlandreturnPrefixes), and change it withmocco_help_site_sign_in_url_set. The change is confirmed in your client first, by an owner or admin.With a secret API key that has
help:write,GET /v1/help/sign-inreturns{ signInUrl, returnPrefixes }, andPUT /v1/help/sign-inwith{ "signInUrl": "https://example.com/api/help/enter" }sets it (nullremoves it). A URL that breaks a rule is refused with400and the rule indetail.
Board pages in Korean
The board pages' own words (the header, buttons, statuses and Sign in) follow the language your help center is written in: Korean for a Korean help center, English otherwise. Your posts and their text are shown as written.