Skip to content

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:

ClaimPflichtBedeutung
subjaNutzer-ID in Ihrer Anwendung (1–255 Zeichen). Gespeichert wird nur ein geheimer Hash.
expjaAblauf (Unix-Zeit), höchstens 1 Stunde in der Zukunft (60 s Toleranz für Uhrabweichungen).
metaneinObjekt 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.
nbfneinNicht 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
Scriptdata-identity-token="…" bzw. PollYou.open({ surveyId, identityToken })
Web ComponentAttribut identity-token oder Property identityToken von <pollyou-survey>
React, AngularProp/Input identityToken (React, Angular)
Vue, AngularJSidentity-token (Vue, AngularJS)
iframe, Direktlink, QR-Codenicht 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):

EndpunktErgebnis
GET /api/organizations/{id}/embed-secret{secret, createdAt, previousValidUntil}; beim ersten Abruf wird es erzeugt
POST /api/organizations/{id}/embed-secret/rotateneues 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 ohne sub;
  • 403 {code: "identity_required"} – kein Token, obwohl die Embed-Konfiguration eine signierte Identität verlangt oder die Dedup-Strategie identity ist.

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