Preskočiť na obsah

Blog ·

Sign your users in to an embedded widget with one signed token

How Ozvi knows who is reporting a bug without a second login: a short-lived HS256 token from your backend, in Node.js, Python or PHP, and what to put in it.

A feedback widget is only useful if it knows who is talking. “Someone” reported a bug on the invoice page is a dead end; “Sarah from Pilates House, on the Pro plan” is a conversation. But nobody wants to log in a second time to report a bug, and you don't want a third-party account system next to yours.

Ozvi solves it the way most embedded tools should: your backend, which already knows who is signed in, signs a short statement (“this is user u-42, sarah@…, role member”) and the widget passes it along. Ozvi checks the signature and trusts it. No redirect, no password, no Ozvi account for your users.

The whole flow

  1. The widget calls an endpoint in your app, the token-url.
  2. Your endpoint checks your own session and returns { "token": "…" }.
  3. The widget sends the token with every request to Ozvi and caches it until shortly before it expires.
HTML
<script src="https://feedback.your-app.com/widget.js" defer></script>
<ozvi-widget project="your-app" token-url="/api/ozvi-token"></ozvi-widget>

The secret that signs the token is shared between your server and Ozvi (board settings → Sign-in). It never goes to the browser.

The token is a plain JWT

No proprietary format: a JSON Web Token signed with HMAC-SHA256 (HS256), valid for ten minutes. That means any language can make one with its standard library.

FieldWhat it is
idRequired. The person's stable id in your app. Ozvi keys on it, never on the e-mail alone.
emailOptional. Used for replies (“fixed in 3.9.2, does it work for you?”).
nameOptional. Shown to your team.
rolemember (default), team (can triage) or admin (can change settings).
traitsOptional, team-only: company, plan, mrr in cents… so you can sort ideas by revenue.
iat, expIssued at and expiry, in seconds. Keep it short-lived.

Node.js

With the MIT-licensed SDK (no dependencies):

npm i @ozvi/sdk
import { signOzviToken } from "@ozvi/sdk";

app.get("/api/ozvi-token", requireLogin, (req, res) => {
  res.json({ token: signOzviToken(
    { id: req.user.id, email: req.user.email, name: req.user.name,
      traits: { company: req.org.name, plan: req.org.plan } },
    process.env.OZVI_SSO_SECRET) });
});

In Next.js the same goes into app/api/ozvi-token/route.ts:

app/api/ozvi-token/route.ts
import { signOzviToken } from "@ozvi/sdk";
import { auth } from "@/auth"; // your session

export async function GET() {
  const session = await auth();
  if (!session?.user) return new Response(null, { status: 401 });
  const { id, email, name } = session.user;
  return Response.json({ token: signOzviToken({ id, email, name }, process.env.OZVI_SSO_SECRET!) });
}

Python (standard library only)

ozvi_token.py
import base64, hashlib, hmac, json, os, time

def ozvi_token(user: dict, secret: str = os.environ["OZVI_SSO_SECRET"]) -> str:
    b64 = lambda o: base64.urlsafe_b64encode(json.dumps(o, separators=(",", ":")).encode()).rstrip(b"=").decode()
    now = int(time.time())
    body = f'{b64({"alg": "HS256", "typ": "JWT"})}.{b64({**user, "iat": now, "exp": now + 600})}'
    sig = hmac.new(secret.encode(), body.encode(), hashlib.sha256).digest()
    return f"{body}.{base64.urlsafe_b64encode(sig).rstrip(b'=').decode()}"

PHP (no Composer package)

ozvi_token.php
function ozvi_token(array $user, ?string $secret = null): string {
    $secret ??= getenv('OZVI_SSO_SECRET');
    $b64 = fn($s) => rtrim(strtr(base64_encode($s), '+/', '-_'), '=');
    $now = time();
    $body = $b64(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])) . '.'
          . $b64(json_encode($user + ['iat' => $now, 'exp' => $now + 600], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    return $body . '.' . $b64(hash_hmac('sha256', $body, $secret, true));
}

All three produce tokens that Ozvi's own verifier accepts; we test them against it.

Getting it right

  • Keep it short-lived. Ten minutes is plenty, the widget asks for a new one when it needs it. A leaked token is useless soon after.
  • Sign only for signed-in users. Check your own session first and return 401 without it; never sign whatever the browser sends you.
  • Use your stable id, not the e-mail, as id. People change e-mails.
  • Roles come from you. Give team only to your staff: they see every report and the customer data in traits.
  • Don't paste real tokens into online decoders. They carry your users' e-mails.

More

Everything the widget can do, from the JavaScript API to React and Vue components: widget docs. Prefer OpenID Connect? Ozvi supports that too (board settings → Sign-in).