Login
To comment, the person has to be logged in. There are two ways, and you can turn on one or both in the dashboard's Settings tab.
| Your site's login | Qorum account | |
|---|---|---|
| For whom | Sites that already have users and a login | Sites without a login, or people without an account on your site |
| What the person does | Nothing: they're already in with your site's account | Creates a username and password inside the widget |
| What you do | A route on your server that creates a token | Nothing: just leave the option on |
| Name and photo | Come from your site, the way you send them | The Qorum account's username and display name |
With both on, the widget uses your site's login when it exists and offers a Qorum account to whoever isn't logged in.
Your site's login
The idea: your server, which already knows who is logged in, creates a token saying "this is Ana, id 123". The token is signed with your secret key, so Qorum knows you created it and nobody tampered with it.
- The page asks your server for a token
Through a route of yours, like
/qorum-token, only for logged-in people. - Your server signs it and sends it back
With the secret key, which never leaves the server.
- The page hands the token to the widget
In the
tokenoption. The widget sends the token to Qorum on every request. - Qorum checks the signature
If everything is right, the person comments with the name and photo you sent.
What goes inside the token
It's a JWT signed with HS256 and the site's secret key. There are JWT libraries for every language.
| Field | Required | What to put |
|---|---|---|
sub | yes | The user's id on your site, as text. Up to 64 characters: letters, numbers and _ . : @ -. This id must never change for the same person. |
aud | yes | The site's public key (pk_...). Yes, the public one: it says which site the token is for. |
iat | yes | When the token was created, in seconds since 1970. Most libraries fill it in for you. |
exp | yes | When the token expires. At most 25 hours after iat. Recommended: 12 hours. |
name | yes | The name shown on comments (up to 120 characters). |
card | no | An object with the person's look. See The card. |
mod | no | true for people on your team: they can approve, delete, pin and ban from the widget. |
1. The route on your server
It returns {"token": "..."} for logged-in people and 401 for everyone else. Keep the secret key in an environment variable, never in code that goes to the browser.
// npm install jsonwebtoken
const jwt = require('jsonwebtoken');
app.get('/qorum-token', (req, res) => {
const user = req.user; // the user logged in to your site
if (!user) return res.sendStatus(401);
const token = jwt.sign(
{
name: user.name,
card: { username: user.username, avatar: user.photoUrl },
mod: user.isAdmin === true,
},
process.env.QORUM_SECRET, // sk_...
{
algorithm: 'HS256',
subject: String(user.id),
audience: 'pk_YOUR_PUBLIC_KEY',
expiresIn: '12h',
}
);
res.json({ token });
});
<?php
// composer require firebase/php-jwt
use Firebase\JWT\JWT;
session_start();
$user = $_SESSION['user'] ?? null; // the user logged in to your site
if (!$user) { http_response_code(401); exit; }
$now = time();
$token = JWT::encode([
'sub' => (string) $user['id'],
'aud' => 'pk_YOUR_PUBLIC_KEY',
'iat' => $now,
'exp' => $now + 12 * 3600,
'name' => $user['name'],
'card' => ['username' => $user['username'], 'avatar' => $user['photo_url']],
'mod' => !empty($user['is_admin']),
], getenv('QORUM_SECRET'), 'HS256');
header('Content-Type: application/json');
echo json_encode(['token' => $token]);
# pip install PyJWT
import os, time, jwt
from flask import jsonify, abort
from flask_login import current_user
@app.get("/qorum-token")
def qorum_token():
if not current_user.is_authenticated:
abort(401)
now = int(time.time())
token = jwt.encode({
"sub": str(current_user.id),
"aud": "pk_YOUR_PUBLIC_KEY",
"iat": now,
"exp": now + 12 * 3600,
"name": current_user.name,
"card": {"username": current_user.username, "avatar": current_user.photo_url},
"mod": bool(current_user.is_admin),
}, os.environ["QORUM_SECRET"], algorithm="HS256")
return jsonify(token=token)
// go get github.com/golang-jwt/jwt/v5
func qorumToken(w http.ResponseWriter, r *http.Request) {
u := loggedInUser(r) // the user logged in to your site
if u == nil {
http.Error(w, "", http.StatusUnauthorized)
return
}
now := time.Now()
claims := jwt.MapClaims{
"sub": strconv.Itoa(u.ID),
"aud": "pk_YOUR_PUBLIC_KEY",
"iat": now.Unix(),
"exp": now.Add(12 * time.Hour).Unix(),
"name": u.Name,
"card": map[string]any{"username": u.Username, "avatar": u.PhotoURL},
"mod": u.IsAdmin,
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(os.Getenv("QORUM_SECRET")))
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": token})
}
2. The page code
The page fetches the token and hands it to the widget. When it expires, the widget says so through onExpired and the page fetches another:
<div id="comentarios"></div>
<script src="https://useqorum.com/widget.js"></script>
<script>
let token = null;
async function fetchToken() {
const r = await fetch('/qorum-token', { credentials: 'same-origin' });
token = r.ok ? (await r.json()).token : null; // 401 = nobody logged in
return token;
}
fetchToken().then(() => {
const comments = Qorum.montar(document.getElementById('comentarios'), {
key: 'pk_YOUR_PUBLIC_KEY',
ref: 'post-42',
token: () => token,
onLogin: () => { location.href = '/login?back=' + encodeURIComponent(location.href); },
onExpired: async () => { comments.definirToken(await fetchToken()); }
});
});
</script>
The card
The card is free-form: Qorum stores it and returns it the way you sent it, in the user field of each comment. If you use the API you can put whatever your design needs there. The ready-made widget understands these fields:
| Field | What the widget shows |
|---|---|
username | Used when there's no displayName. |
displayName | The highlighted name on the comment. |
avatar | The https address of the photo. With no photo, the name's initial shows up. |
rank | A badge with a name, color and optional icon: {"name": "Subscriber", "nameColor": "#7c3aed", "icon": "https://..."}. The color also paints the name. |
isStaff | Shows the "Staff" badge. |
isVip | Shows a highlight badge. |
avatarFrame, avatarFrameScale | A decorative image over the photo and its size relative to the photo (default 1.2). |
Would you rather Qorum fetch the card from your site? See Site data. Without it, the card is updated whenever the person uses a new token. To update it right away (the person changed their photo, for example), use PUT /v1/site/users/:id/card in the management API.
Security
- The secret key stays on the server only. If it ever shows up somewhere public, rotate it right away in Settings.
- Create the token only for logged-in people, and always with the logged-in person's
sub, never with an id that came from the page. - Use a short lifetime (up to 12 hours) and renew it with
onExpired. - Serve the token route over HTTPS only.
Checking the token
In the dashboard's Test connection tab, paste a token created by your server. The test says exactly what is wrong, like "aud must be this site's public key" or "token expired", and shows the content Qorum read. The management API runs the same test.
Qorum account
With the option on, the widget shows the Log in button to whoever isn't logged in. The person chooses between logging in and signing up:
- Username: 3 to 30 characters, lowercase letters, numbers,
_and dot. That's what they log in with (they can type it with a leading@). - Password: 8 to 72 characters.
- Display name: optional. Without it, the username shows up.
There's no e-mail and no password recovery. The account works on every site that uses Qorum, but the person logs in separately on each site. The login lasts 30 days. On comments, this person's id shows up as q: followed by a number, like q:12.
People you add as moderators in the dashboard become moderators in the widget when they log in with their Qorum account.
Building your own interface? The log in and sign up routes are in the public API.