# Connecter votre application en SSO

Le SSO pealHQ connecte les utilisateurs de votre application à votre portail de feedback. Votre serveur authentifie l’utilisateur, signe un jeton JWT avec le secret de votre workspace et redirige son navigateur vers pealHQ. Le compte du portail est créé ou mis à jour à la connexion.

Ce guide peut être partagé avec votre équipe technique ou un agent de développement. Les valeurs des exemples sont fictives. Le secret réel reste dans vos réglages et dans les variables d’environnement de votre serveur.

## Avant de commencer

- Vous devez administrer un workspace dont le forfait permet la fonctionnalité SSO personnalisé. Sa disponibilité est affichée dans les réglages.
- Votre application doit déjà authentifier ses utilisateurs côté serveur.
- Ce mécanisme utilise un JWT signé en **HS256** avec un secret propre au workspace. Il ne s’agit pas d’un connecteur SAML ou OpenID Connect.
- Le SSO donne accès au **portail client** et authentifie les membres déjà autorisés de l’équipe pour ce workspace. Il ne crée aucun administrateur et ne modifie aucun rôle. Invitez vos collaborateurs depuis la gestion de l’équipe pealHQ.

Si l’adresse email du compte connecté par SSO correspond à un unique membre déjà présent dans l’équipe de ce workspace, le bouton **Administration** ouvre directement son administration. Le portail et l’administration partagent la même session : aucun mot de passe ni code de double authentification Peal supplémentaire n’est demandé. Votre application prend en charge l’authentification, y compris son éventuel second facteur. Les commentaires du membre apparaissent comme des réponses de l’équipe ; son profil client conserve ses votes, suggestions et abonnements.

Cet accès reste limité à ce workspace. Il ne connecte pas le membre à son compte central Peal, à d’autres workspaces ou à l’administration plateforme. Une invitation non acceptée ne suffit pas. Le retrait d’un membre supprime immédiatement son accès d’équipe. La déconnexion ferme le portail et l’administration de ce workspace ; elle ne ferme pas une connexion centrale Peal indépendante. Après expiration de la session, une nouvelle connexion SSO est nécessaire pour retrouver les droits d’équipe, même si le portail se souvient du profil client.

## Configurer le workspace

1. Ouvrez **Paramètres → Connexion des clients** dans l’administration pealHQ.
2. Sélectionnez **SSO (votre backend gère l’identité)**.
3. Renseignez l’**URL de connexion depuis votre produit**, puis enregistrez. C’est la page authentifiée de votre application qui générera un jeton neuf et redirigera le client vers le callback pealHQ. Lorsqu’un client ouvre directement le portail sans session, le bouton « Se connecter à partir de {nom du workspace} » le renvoie vers cette URL.
4. Copiez l’**URL de callback**, l’**audience (slug du workspace)** et le **secret partagé** affichés.
5. Enregistrez ces valeurs dans la configuration serveur de votre application. Utilisez un secret distinct pour chaque workspace et environnement.

Exemple de configuration dans **votre application** :

```dotenv
PEAL_SSO_CALLBACK_URL=https://acme.pealhq.com/sso/callback
PEAL_SSO_AUDIENCE=acme
PEAL_SSO_SECRET=remplacer-par-le-secret-affiche-dans-pealhq
```

Ces noms de variables sont une convention pour les exemples, pas des variables à ajouter au serveur pealHQ. Utilisez le secret tel qu’il est affiché, sans décodage Base64.

Selon l’environnement, le callback peut aussi ressembler à `https://preview.example/w/acme/sso/callback`. **Copiez toujours l’URL des réglages**, sans la reconstruire à partir du domaine. L’audience reste `acme` dans les deux cas.

## Parcours de connexion

1. L’utilisateur clique sur « Donner mon avis » dans votre application.
2. Votre backend vérifie sa session et son autorisation d’accéder au workspace concerné.
3. Votre backend prend l’identifiant, le nom et l’email dans le compte authentifié, puis génère un nouveau JWT valable 60 secondes.
4. Il répond au navigateur par une redirection HTTP vers `URL_DE_CALLBACK?token=JWT_ENCODE`.
5. pealHQ vérifie le jeton, ouvre la session du workspace avec les droits du membre existant, le cas échéant, et redirige vers la destination choisie dans le même workspace, ou vers la liste des demandes par défaut.

La redirection doit être suivie par **le navigateur de l’utilisateur**, afin qu’il reçoive les cookies de session. Un appel HTTP effectué uniquement par votre serveur ne connecte pas son navigateur. Ouvrez le portail comme une page de premier niveau.

Pour choisir la page d’arrivée, ajoutez le paramètre de requête facultatif `redirect` à l’URL du callback, à côté de `token` :

```text
https://acme.pealhq.com/sso/callback?token=JWT_ENCODE&redirect=/changelog
https://acme.pealhq.com/sso/callback?token=JWT_ENCODE&redirect=/roadmap
```

| Valeur de `redirect` | Page d’arrivée après connexion |
| --- | --- |
| `/changelog` | Les nouveautés du workspace. |
| `/roadmap` | La roadmap du workspace. |
| `/requests`, paramètre absent ou autre valeur | La liste des demandes du workspace. |

Les valeurs sont exactes, sans domaine, préfixe de workspace, query string ni fragment. En mode chemin, utilisez toujours `redirect=/changelog` ou `redirect=/roadmap` : pealHQ ajoute lui-même `/w/{slug}` à la destination. Les URLs externes et les chemins arbitraires ne sont pas suivis. `redirect` est un paramètre du callback, pas un champ du JWT ; il ne modifie pas les règles de validation du jeton.

L’expiration du JWT limite le temps disponible pour se connecter ; elle n’est pas la durée de la session du portail. Se déconnecter de votre application ne ferme pas automatiquement la session pealHQ.

## Contrat du jeton

Le format est `base64url(header).base64url(payload).base64url(signature)`, sans padding `=`. La signature est le HMAC SHA-256 binaire de `header_encode.payload_encode`, calculé avec le secret partagé, puis encodé en Base64URL.

Utilisez exactement cet en-tête, avec les clés dans cet ordre et sans champ supplémentaire :

```json
{"alg":"HS256","typ":"JWT"}
```

Le vérificateur actuel exige cet ordre (`alg`, puis `typ`) et ces deux champs seulement. Si vous utilisez une bibliothèque JWT, vérifiez l’en-tête qu’elle produit ; n’ajoutez pas de `kid`.

Tous les champs suivants sont obligatoires :

| Champ | Type | Valeur attendue |
| --- | --- | --- |
| `aud` | Chaîne | Slug exact du workspace, par exemple `acme`. Ce n’est ni une URL ni un tableau. |
| `sub` | Chaîne non vide | Identifiant stable de l’utilisateur dans votre application, par exemple `user-482`. Convertissez les identifiants numériques en chaîne. |
| `email` | Chaîne | Email valide et de confiance du compte authentifié. |
| `name` | Chaîne non vide | Nom affiché dans le portail. |
| `iat` | Entier | Date d’émission Unix en secondes, sans millisecondes. |
| `exp` | Entier | Date d’expiration Unix en secondes, postérieure à l’émission et à l’heure de réception. |
| `jti` | Chaîne non vide | Identifiant aléatoire unique pour chaque tentative de connexion, par exemple un UUID. |

La durée `exp - iat` ne doit pas dépasser **120 secondes**. Les exemples utilisent 60 secondes. L’heure d’émission peut avoir au plus 5 secondes d’avance sur le serveur pealHQ ; synchronisez les horloges de vos serveurs.

Un jeton ne peut être utilisé qu’une fois. Générez un nouveau `jti` et un nouveau jeton à chaque clic ou nouvelle tentative. Un jeton validé peut être consommé même si une erreur de rapprochement de comptes survient ensuite.

N’utilisez pas de champ `admin` ou `role` pour accorder des droits : seuls `sub`, `email` et `name` servent à identifier le client après validation.

## Exemple PHP

Cette fonction fonctionne sans bibliothèque JWT. Appelez-la depuis une route serveur qui a déjà authentifié et autorisé l’utilisateur. Les clés `id`, `email` et `name` viennent de votre base de comptes, jamais de paramètres fournis librement par le navigateur.

```php
<?php

function pealSsoUrl(array $user, string $redirect = '/requests'): string
{
    $callback = getenv('PEAL_SSO_CALLBACK_URL');
    $audience = getenv('PEAL_SSO_AUDIENCE');
    $secret = getenv('PEAL_SSO_SECRET');

    if (! $callback || ! $audience || ! $secret || strlen($secret) < 32) {
        throw new RuntimeException('Configuration SSO pealHQ manquante ou invalide.');
    }

    $encode = static fn (string $value): string =>
        rtrim(strtr(base64_encode($value), '+/', '-_'), '=');

    $now = time();
    $header = $encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'], JSON_THROW_ON_ERROR));
    $payload = $encode(json_encode([
        'aud' => $audience,
        'sub' => (string) $user['id'],
        'email' => $user['email'],
        'name' => $user['name'],
        'iat' => $now,
        'exp' => $now + 60,
        'jti' => bin2hex(random_bytes(16)),
    ], JSON_THROW_ON_ERROR));
    $signature = $encode(hash_hmac('sha256', "$header.$payload", $secret, true));
    $token = "$header.$payload.$signature";
    $separator = str_contains($callback, '?') ? '&' : '?';

    return $callback.$separator.http_build_query([
        'token' => $token,
        'redirect' => $redirect,
    ], '', '&', PHP_QUERY_RFC3986);
}
```

Dans votre contrôleur, après vérification de la session, redirigez avec l’URL retournée : `return redirect()->away(pealSsoUrl($user));` sous Laravel (adaptez votre modèle en tableau), ou `header('Location: '.pealSsoUrl($user), true, 302); exit;` en PHP natif. Si votre framework met les variables d’environnement en cache, injectez les valeurs via son système de configuration plutôt que `getenv`.

Pour ouvrir les nouveautés, appelez `pealSsoUrl($user, '/changelog')` ; pour la roadmap, `pealSsoUrl($user, '/roadmap')`.

## Exemple Node.js

Ce module ESM utilise uniquement `node:crypto`. Gardez-le côté serveur, dans une route accessible à un utilisateur déjà authentifié et autorisé.

```javascript
import { createHmac, randomUUID } from 'node:crypto';

export function pealSsoUrl(user, redirect = '/requests') {
    const callback = process.env.PEAL_SSO_CALLBACK_URL;
    const audience = process.env.PEAL_SSO_AUDIENCE;
    const secret = process.env.PEAL_SSO_SECRET;

    if (!callback || !audience || !secret || secret.length < 32) {
        throw new Error('Configuration SSO pealHQ manquante ou invalide.');
    }

    const encode = (value) => Buffer.from(JSON.stringify(value)).toString('base64url');
    const now = Math.floor(Date.now() / 1000);
    const header = encode({ alg: 'HS256', typ: 'JWT' });
    const payload = encode({
        aud: audience,
        sub: String(user.id),
        email: user.email,
        name: user.name,
        iat: now,
        exp: now + 60,
        jti: randomUUID(),
    });
    const signature = createHmac('sha256', secret)
        .update(`${header}.${payload}`)
        .digest('base64url');
    const url = new URL(callback);
    url.searchParams.set('token', `${header}.${payload}.${signature}`);
    url.searchParams.set('redirect', redirect);

    return url.toString();
}
```

Par exemple, dans un contrôleur Express protégé par votre middleware d’authentification : `res.redirect(302, pealSsoUrl(req.user));`. Adaptez les noms de propriétés à votre modèle utilisateur. Le secret et la fonction de signature ne doivent jamais être inclus dans le bundle JavaScript envoyé au navigateur.

Utilisez `pealSsoUrl(req.user, '/changelog')` ou `pealSsoUrl(req.user, '/roadmap')` pour choisir la page d’arrivée.

## Identité et comptes existants

pealHQ recherche d’abord un client portant le même `sub` dans le workspace, puis un client portant le même email. Un compte existant avec cet email peut être rattaché au SSO s’il n’est pas déjà lié à un autre identifiant SSO. Sinon, un nouveau compte client est créé.

Le nom et l’email sont mis à jour à chaque connexion. Gardez le `sub` stable lorsque l’email change et ne réutilisez jamais l’identifiant d’un utilisateur supprimé pour une autre personne. Votre serveur doit garantir que l’utilisateur contrôle l’email transmis : ce rapprochement peut donner accès à un compte du portail et aux droits d’un membre d’équipe déjà autorisé dans ce workspace.

Si l’identifiant et l’email désignent deux comptes différents, ou si l’email appartient déjà à un autre identifiant SSO, la connexion est refusée. Corrigez l’identité dans votre application ou faites examiner les comptes concernés avant de réessayer avec un nouveau jeton.

## Diagnostiquer une erreur

Le callback est une page de connexion avec redirection, pas une API JSON. Le texte affiché peut être générique ; les libellés ci-dessous correspondent aux causes de rejet côté serveur.

| Statut | Cause possible | Vérification |
| --- | --- | --- |
| 403 | `Missing SSO token.` | Transmettre le JWT dans le paramètre de requête `token`. |
| 403 | `SSO is not configured for this organization.` | Activer le SSO et récupérer le secret du bon workspace. |
| 403 | `Invalid SSO token.` | Vérifier le secret, HS256, l’ordre des champs de l’en-tête et l’encodage Base64URL sans padding. |
| 403 | `Invalid or expired SSO token.` | Vérifier l’audience, les champs obligatoires, les types et les dates en secondes. Générer un jeton neuf. |
| 403 | `SSO token has already been used.` | Recommencer depuis votre application avec un nouveau `jti`. Ne pas recharger une ancienne URL de callback. |
| 403 | `SSO identity conflicts with an existing account.` | Vérifier la cohérence entre `sub`, email et comptes existants. |
| 403 | Workspace suspendu | Vérifier l’état du workspace auprès de son administrateur. |
| 404 | SSO désactivé, workspace inconnu ou URL incorrecte | Reprendre le callback affiché dans les réglages du workspace actif. |
| 429 | Trop de tentatives | Respecter `Retry-After` avant une nouvelle tentative avec un jeton neuf. Le callback est limité à 20 requêtes par minute. |

## Vérifier votre intégration

Dans un workspace de test configuré en SSO :

1. Connectez un utilisateur de test : il doit arriver sur la liste des demandes avec son identité client.
2. Reconnectez-le avec un jeton neuf : aucun compte supplémentaire ne doit être créé.
3. Changez son nom ou son email en conservant le `sub` : vérifiez la mise à jour du compte.
4. Réutilisez le même jeton, puis essayez un jeton expiré, une mauvaise audience et un mauvais secret : chacun doit être refusé.
5. Vérifiez qu’un utilisateur non authentifié dans votre application ne peut pas obtenir de jeton, et qu’aucun champ d’identité n’est accepté directement depuis une saisie utilisateur non vérifiée.
6. Avec un client ordinaire, vérifiez l’absence du bouton Administration et le refus des actions administrateur. Avec un membre déjà autorisé, vérifiez l’accès direct à l’administration du workspace et les commentaires d’équipe, sans accès aux autres workspaces ni à l’administration plateforme.
7. Déconnectez-vous depuis le portail puis depuis l’administration : les deux accès du workspace doivent être fermés.

## Protéger et renouveler le secret

Utilisez HTTPS. Stockez le secret dans la configuration privée de votre backend. Un JWT signé est lisible : il n’est pas chiffré. Évitez de journaliser les URLs contenant `token` et ne partagez ni secret ni jeton réel dans un ticket ou un outil de diagnostic tiers. Pour un agent, utilisez des valeurs fictives et injectez le secret au déploiement.

Le bouton **Régénérer le secret** invalide immédiatement les jetons signés avec l’ancien secret. Mettez à jour votre backend au même moment ; aucune période de chevauchement n’est prévue. La rotation du secret ne ferme pas les sessions du workspace déjà ouvertes.

Pour un autre guide ou sa version destinée aux agents, consultez [l’index de la documentation](/docs) et [l’index Markdown](/docs/index.md).
