Appearance
Identitäts-Token
Mit einem Identitäts-Token teilt Ihre Anwendung PollYou mit, welcher angemeldete Nutzer antwortet – ohne dass PollYou die Nutzer-ID im Klartext speichert. Einsatzzwecke:
- „Eine Antwort pro Kunde“ (Dedup-Strategie
identity, siehe Mehrfachabstimmung), - Umfragen nur für angemeldete Nutzer (Embed-Konfiguration mit „Signierte Identität“),
- signierte Zusatzdaten (
meta), die der Browser nicht fälschen kann.
Aufbau
Das Token ist ein JWT mit dem Algorithmus HS256, signiert mit dem Embed-Secret Ihrer Organisation:
| Claim | Pflicht | Bedeutung |
|---|---|---|
sub | ja | Nutzer-ID in Ihrer Anwendung (1–255 Zeichen). Gespeichert wird nur ein geheimer Hash. |
exp | ja | Ablauf (Unix-Zeit), höchstens 1 Stunde in der Zukunft (60 s Toleranz für Uhrabweichungen). |
meta | nein | Objekt mit Text-, Zahl- oder Ja/Nein-Werten, Schlüssel [a-z0-9_]{1,32}, max. 20; ungültige Einträge werden verworfen. Überschreibt gleichnamige meta-Werte des Browsers. |
nbf | nein | Nicht gültig vor (Unix-Zeit). |
Erzeugen Sie das Token auf Ihrem Server – das Secret darf nie in den Browser gelangen – und geben Sie es an das Embed weiter:
html
<div data-pollyou-survey="AbC123xyz0" data-identity-token="eyJhbGciOiJIUzI1NiIs…"></div>PHP (Symfony)
Mit lcobucci/jwt (composer require lcobucci/jwt) als Service, das Secret aus einer Umgebungsvariablen (POLLYOU_EMBED_SECRET, in Produktion als Symfony-Secret):
php
namespace App\Survey;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Psr\Clock\ClockInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final readonly class PollYouIdentityToken
{
private Configuration $jwt;
public function __construct(
#[Autowire(env: 'POLLYOU_EMBED_SECRET')] #[\SensitiveParameter] string $secret,
private ClockInterface $clock,
) {
$this->jwt = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($secret));
}
/**
* @param array<string, string|int|float|bool> $meta
*/
public function issue(string $userId, array $meta = []): string
{
$now = $this->clock->now();
$builder = $this->jwt->builder()
->relatedTo($userId)
->issuedAt($now)
->expiresAt($now->modify('+10 minutes')); // höchstens 1 Stunde
if ([] !== $meta) {
$builder = $builder->withClaim('meta', $meta);
}
return $builder->getToken($this->jwt->signer(), $this->jwt->signingKey())->toString();
}
}Im Controller erzeugen und ins Template geben:
php
return $this->render('account/feedback.html.twig', [
'identityToken' => $identityTokens->issue((string) $user->getId(), ['plan' => $user->getPlan()]),
]);twig
<div data-pollyou-survey="AbC123xyz0" data-identity-token="{{ identityToken }}"></div>InMemory::plainText() verlangt einen Schlüssel von mindestens 256 Bit – das Embed-Secret (pyes_ + 43 Zeichen) erfüllt das.
PHP (ohne Framework)
Mit firebase/php-jwt:
php
use Firebase\JWT\JWT;
$token = JWT::encode([
'sub' => (string) $user->getId(),
'exp' => time() + 600, // höchstens 3600 Sekunden
'meta' => ['plan' => $user->getPlan()],
], $_ENV['POLLYOU_EMBED_SECRET'], 'HS256');Ohne Bibliothek:
php
function pollyouIdentityToken(string $secret, string $userId, int $ttl = 600): string
{
$encode = static fn (string $data): string => rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
$unsigned = $encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']))
.'.'.$encode(json_encode(['sub' => $userId, 'exp' => time() + $ttl]));
return $unsigned.'.'.$encode(hash_hmac('sha256', $unsigned, $secret, true));
}Node.js
Mit jose:
js
import { SignJWT } from 'jose'
const secret = new TextEncoder().encode(process.env.POLLYOU_EMBED_SECRET)
const token = await new SignJWT({ meta: { plan: user.plan } })
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setSubject(String(user.id))
.setExpirationTime('10m') // höchstens 1 h
.sign(secret)Mit jsonwebtoken, z. B. in einer Express-Route:
js
import jwt from 'jsonwebtoken'
app.get('/feedback', requireLogin, (req, res) => {
const identityToken = jwt.sign(
{ meta: { plan: req.user.plan } },
process.env.POLLYOU_EMBED_SECRET,
{
algorithm: 'HS256',
subject: String(req.user.id),
expiresIn: '10m', // höchstens 1 h
},
)
res.render('feedback', { identityToken })
})Ohne Bibliothek (node:crypto):
js
import { createHmac } from 'node:crypto'
function pollyouIdentityToken(secret, userId, ttlSeconds = 600) {
const encode = (value) => Buffer.from(JSON.stringify(value)).toString('base64url')
const unsigned = `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode({
sub: String(userId),
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
})}`
return `${unsigned}.${createHmac('sha256', secret).update(unsigned).digest('base64url')}`
}Zusatzdaten (meta) und Vorrang
Die meta-Werte des Tokens werden mit den meta-Werten des Browsers (Attribut data-meta, Prop meta) zusammengeführt und an der Antwort gespeichert. Bei gleichem Schlüssel gewinnt das Token: Werte aus dem Browser kann jeder Besucher ändern, signierte nicht. Beispiel:
Browser (meta) | Token (meta) | Gespeichert |
|---|---|---|
{"plan": "free", "page": 3} | {"plan": "pro"} | {"plan": "pro", "page": 3} |
Ungültige Einträge im Token (falscher Schlüssel, verschachtelte Werte, Texte über 200 Zeichen) werden stillschweigend verworfen; das Token selbst bleibt gültig. Zusammen gelten höchstens 20 Schlüssel aus dem Browser plus 20 aus dem Token.
Token übergeben
| Einbettung | Übergabe |
|---|---|
| Script | data-identity-token="…" bzw. PollYou.open({ surveyId, identityToken }) |
| Web Component | Attribut identity-token oder Property identityToken von <pollyou-survey> |
| React, Angular | Prop/Input identityToken (React, Angular) |
| Vue, AngularJS | identity-token (Vue, AngularJS) |
| iframe, Direktlink, QR-Code | nicht möglich – Umfragen mit Identitätspflicht sind dort gesperrt |
Das Token wird beim Start der Sitzung geprüft. Läuft es danach ab, kann der Nutzer die begonnene Umfrage trotzdem abschließen; nur ein neuer Sitzungsstart braucht wieder ein gültiges Token. Erzeugen Sie das Token deshalb bei jedem Seitenaufruf neu. Der Einbettungs-Dialog im Dashboard fügt bei Umfragen mit Identitätspflicht den Platzhalter IDENTITY_TOKEN in den Code ein.
Embed-Secret
Im Dashboard unter den Organisationseinstellungen (Rolle Admin oder Owner, Berechtigung api_keys.manage):
| Endpunkt | Ergebnis |
|---|---|
GET /api/organizations/{id}/embed-secret | {secret, createdAt, previousValidUntil}; beim ersten Abruf wird es erzeugt |
POST /api/organizations/{id}/embed-secret/rotate | neues Secret, gleiche Form; das bisherige bleibt 24 Stunden gültig |
Das Secret (pyes_…) wird verschlüsselt gespeichert. Nach einer Rotation haben Sie 24 Stunden Zeit, es in Ihrer Anwendung auszutauschen; bis dahin werden Tokens mit beiden Secrets angenommen. Rotationen stehen im Audit-Log (embed_secret.rotated, ohne das Secret).
Fehler
Der Sitzungsstart (POST /api/collect/surveys/{publicId}/sessions, Feld identityToken) antwortet mit
403 {code: "identity_invalid"}– Token vorhanden, aber falsch signiert, abgelaufen, zu lange gültig oder ohnesub;403 {code: "identity_required"}– kein Token, obwohl die Embed-Konfiguration eine signierte Identität verlangt oder die Dedup-Strategieidentityist.
Datenschutz
PollYou speichert sub nie im Klartext: In response.external_user_ref steht ein geheimer, organisationsbezogener Hash (sub:<hmac>), mit dem sich die Antworten eines Nutzers innerhalb Ihrer Organisation zuordnen lassen; für die Dedup-Prüfung ein zweiter, umfragebezogener Hash (id:<hmac>).