# 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](https://www.mocco.club/docs/en/articles/zp45zj-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](https://www.mocco.club/docs/en/articles/ky2kfv-contact-us)). 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

1. A visitor chooses **Sign in** on a board page, say `https://example.com/help/12?sort=new`.
2. 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=en`
3. Your endpoint checks `return`, signs the visitor in if they aren't yet, and answers `302` back to the page with the token in the fragment: `https://example.com/help/12?sort=new#token=<jwt>`.
4. 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:

```text
GET <sign-in URL>?return=<URL>[&prompt=none]&lang=<ko|en>
```

The parameters are added to any query your sign-in URL already has.

- `return` is 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](https://www.mocco.club/docs/en/articles/a2t8mn-own-site), your own address. It keeps the page's query, without any token, and has no fragment.
- `prompt=none` means "don't show a sign-in screen". It is explained below, under checking silently.
- `lang` is the page's language, `ko` or `en`, 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`, answer `302` to `return#signin=none` at 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`](https://github.com/panva/jose) to sign the token. `getSignedInUser` stands for however your app reads its session; replace it with yours.

```ts
// 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 the `NEXT_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**.

1. Type your endpoint's address in **Sign-in URL** and choose **Save**. It must use `https` and be at most 500 characters, without a user name, password or `#`. A query is allowed.
2. Copy the addresses under **Return addresses to allow** into your endpoint. They are your help site's addresses: each [path on your own site](https://www.mocco.club/docs/en/articles/a2t8mn-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.
3. The section also says whether the project has an identity secret, with a link to set up the messenger when it doesn't.
4. Choose **Try it**. It opens your sign-in URL with a `return` on 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 to `https://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](https://www.mocco.club/docs/en/articles/2yiscf-audit-log).

Saving refreshes the board pages, so they show the new setting at once.

## From an agent or a script

- [Agents over MCP](https://www.mocco.club/docs/en/articles/frhj3w-connect) read the sign-in URL and the return addresses with `mocco_help_site_get` (`signInUrl` and `returnPrefixes`), and change it with `mocco_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-in` returns `{ signInUrl, returnPrefixes }`, and `PUT /v1/help/sign-in` with `{ "signInUrl": "https://example.com/api/help/enter" }` sets it (`null` removes it). A URL that breaks a rule is refused with `400` and the rule in `detail`.

## 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.
