Site sign-in
Sign visitors in to your public site with their account in your product, from one route, or by signing the token yourself.
Your public site can sign visitors in with the account they already have in your product. Their site chat and their widget chat become one history, and your tools know who they are. This page is the developer half: the route, the token, and the OpenID Connect details.
To pick a method and set it up in the dashboard, see Choose how visitors sign in in the help center.
Your app
import { currentUser } from "@clerk/nextjs/server";
import { medianIdentity } from "@mediansh/agent-tools";
export const { GET } = medianIdentity(
async () => {
const user = await currentUser();
if (!user) return null;
return {
id: user.id,
email: user.primaryEmailAddress?.emailAddress,
name: user.fullName ?? undefined,
avatarUrl: user.imageUrl,
};
},
{
signIn: (returnTo) =>
`/sign-in?redirect_url=${encodeURIComponent(returnTo)}`,
},
);
This is the route the widget’s identity prop reads. If you have one, add
email and signIn to it.
Add the route
Deploy it with MEDIAN_KEY set on the server. See
Identity.
Paste its URL
Under Site → Sign-in, pick Your app, paste the route’s full https URL, and press Use your app.
Try it
Open your site and press Sign in.
How it works
- The site sends the visitor to your route with
?median_request=<id>. - Signed in, the route signs a token and sends them to Median. Signed out,
it sends them to
signIn(returnTo), and your login brings them back to the route. - Median checks the token and returns the visitor to the page they started on, signed in.
Without the query parameter, the route answers the widget as before.
Resolver
| Field | Required | Detail |
|---|---|---|
id |
Yes | Your id for the person. The widget’s signed id for them must match |
email |
Yes | The board counts one vote per email |
name |
No | Shown in the site header and to your team |
avatarUrl |
No | https only |
Options
| Option | Default | Detail |
|---|---|---|
signIn |
None | (returnTo) => string. Your login page, given this route’s full URL to come back to. Without it, a signed out visitor goes back to the site with Sign in to Acme first |
key |
MEDIAN_KEY |
The Median key to sign with |
apiUrl |
MEDIAN_API_URL, then https://api.median.sh |
Median’s API origin |
Other servers
medianSiteRedirect builds the redirect for a server without Web Request
and Response:
import { medianSiteRedirect } from "@mediansh/agent-tools";
app.get("/median/sign-in", async (req, res) => {
const user = req.session.user;
if (!user) {
return res.redirect(`/login?next=${encodeURIComponent(req.originalUrl)}`);
}
res.set("cache-control", "no-store");
res.redirect(
await medianSiteRedirect(String(req.query.median_request), {
id: user.id,
email: user.email,
name: user.name,
}),
);
});
medianSiteRedirect(request, null) sends a signed out visitor back to the
site instead.
| Export | Use |
|---|---|
medianIdentity |
The route above. Answers the widget, and signs visitors in to the site |
medianSiteRedirect(request, user, options?) |
Async. The redirect URL for one sign-in, on any server |
signMedianSiteToken(request, user, options?) |
Async. The token alone, when you build the redirect yourself |
medianSiteSigningKey(key) |
Async. The key tokens are signed with, worked out from a Median key |
MEDIAN_SITE_REQUEST_PARAM |
"median_request", the query parameter the site sends |
MedianSiteSignInOptions |
The options type: key and apiUrl |
The token
For a language without the SDK. Redirect to
https://api.median.sh/sites/sign-in?token=<jwt>, or to
https://api.median.sh/sites/sign-in?request=<median_request>&error=signed_out
when nobody is signed in.
| Part | Value |
|---|---|
| Algorithm | HS256 |
| Header | { "alg": "HS256", "typ": "JWT", "kid": "<median_pk_ id>" } |
| Claims | aud: "median-site", req, sub, email, name?, picture?, iat, exp |
req |
The median_request value |
| Lifetime | exp - iat of 600 seconds or less. 300 is typical |
| Identity secret | "median_sig_" + hex(SHA-256("median:identity:" + MEDIAN_KEY)) |
| Signing key | hex(HMAC-SHA256(identity secret, "median-site-sign-in")), used as a UTF-8 string |
median_pk_ is publicKeyFromMedianKey(MEDIAN_KEY). Each token works once.
OpenID Connect
Setup happens at your provider and on Site → Sign-in. The steps and the issuer URL for each provider are in the help center. What Median does with the provider:
| Fact | Detail |
|---|---|
| Discovery | Read from <issuer>/.well-known/openid-configuration when you save. Its issuer must match what you entered |
| Redirect URI | https://api.median.sh/sites/oidc/callback |
| Scopes | openid email profile |
| Flow | Authorization code with PKCE. The secret goes as client_secret_basic when the provider supports it, else client_secret_post |
| Person | sub is their id. email is required and refused when email_verified is false. name and picture are used when present |
| Linked to the widget | When your app signs the widget’s id with the same sub |
Sessions
| Fact | Detail |
|---|---|
| Session | 30 days, in a cookie on the site’s address |
| Sign out | Ends the session and starts a fresh chat in that browser |
| Before signing in | Anything the browser said in the chat joins the signed in person’s history |
| Addresses | Works on <slug>.median.website and a live custom domain |
| Time limit | A sign-in has 15 minutes to finish |
API and CLI
| Action | CLI | REST |
|---|---|---|
| Show | median site sign-in get |
GET /v1/site/sign-in |
| Median accounts | median site sign-in set median |
PATCH /v1/site/sign-in |
| Your app | median site sign-in set app --app-url <url> |
PATCH /v1/site/sign-in |
| OpenID Connect | median site sign-in set oidc --issuer <url> --client-id <id> --client-secret <secret> |
PATCH /v1/site/sign-in |
MCP: median.site.signIn() and setSignIn(...). The client secret is never
returned.
Errors from your route
| The visitor sees | Cause | Fix |
|---|---|---|
| Sign in to Acme first | Your route found nobody signed in, and has no signIn |
Add signIn to medianIdentity |
| No email address | The resolver sent no email | Return email |
| That didn’t work | The token did not verify, the sign-in took over 15 minutes, or it was opened in another tab | Check MEDIAN_KEY on the route is a current key from this organization, then try again in one tab |
Median: site sign-in needs the person's email. Return { id, email } from your resolver. |
The resolver returned no email | Return email |
Problems visitors and admins see, like a provider refusing the sign-in, are in Fix public site problems.