SSO dei clienti, guida per lo sviluppatore
Come il tuo prodotto fa entrare i suoi utenti nell'accademia Aladia con lo stesso account, firmando un JWT. Flusso, claim, esempi Node.js, Python e PHP (HS256 e RS256/JWKS), deep link, remote login, logout, iscrizioni ai corsi, errori e checklist di sicurezza.
Bozza · in prova sui workspace pilota (06/10/2026)
Questa guida è per chi sviluppa il prodotto (per esempio un SaaS come ClickUp) che integra un'accademia Aladia sul suo dominio, academy.prodotto.com, e vuole che i suoi utenti entrino senza una seconda password. Chi amministra l'accademia trova la configurazione in SSO dei clienti.
Il modello è quello di Featurebase, Thinkific e Zendesk: il server del prodotto, che sa già chi è l'utente, firma un JWT di breve durata e manda il browser su https://academy.prodotto.com/sso. Aladia verifica il token, crea o aggiorna l'utente, lo iscrive dove serve e apre la sessione.
Il flusso
sequenceDiagram
participant U as Browser dell'utente
participant P as Server del prodotto
participant A as Academy (academy.prodotto.com)
U->>A: apre /courses/123 senza sessione
A-->>U: 302 al login del prodotto ?return_to=https://academy.prodotto.com/courses/123
U->>P: login del prodotto (o sessione già aperta)
P->>P: firma il JWT (sub, aud, iat, exp, jti, …)
P-->>U: 302 https://academy.prodotto.com/sso?jwt=…&return_to=/courses/123
U->>A: GET /sso?jwt=…
A->>A: verifica firma, alg, aud, exp/iat, jti monouso
A->>A: crea o aggiorna l'utente, iscrizioni JIT
A-->>U: 302 /courses/123 con la sessione (cookie httpOnly), JWT tolto dall'URL
- L'utente arriva sull'accademia senza sessione. Se l'accademia ha l'URL di login del prodotto (remote login), Aladia lo manda lì con
return_to= l'indirizzo completo della pagina richiesta. - Il prodotto autentica l'utente come fa sempre.
- Il server del prodotto firma il JWT e risponde con un redirect a
https://<accademia>/sso?jwt=<token>&return_to=<percorso>. - Aladia verifica il token, apre la sessione (un cookie
httpOnlydell'accademia) e porta l'utente areturn_to, senza il token nell'URL.
Il JWT si firma solo sul server: mai nel browser e mai in un'app mobile, dove il segreto sarebbe leggibile.
L'indirizzo di accesso
https://<host dell'accademia>/sso, per esempio https://academy.prodotto.com/sso o https://nome.aladia.io/sso. Lo trovi, con il tasto Copia, in Impostazioni > Accesso e sicurezza.
| Parametro | Obbligatorio | Cosa |
|---|---|---|
jwt | sì | il token firmato |
return_to | no | dove andare dopo l'accesso: un percorso (/courses/123) o un URL dello stesso host; qualsiasi altro valore porta alla home |
/sso senza jwt porta al login del prodotto (utile come link "Entra nell'accademia").
Firma
Scegli in Impostazioni > Accesso e sicurezza > Firma del token:
| Metodo | Algoritmo | Cosa configuri |
|---|---|---|
| Segreto (consigliato per iniziare) | HS256 | Aladia genera un segreto di 32 byte e te lo mostra una volta. Si usa così com'è, come stringa (non decodificarlo da base64). |
| Chiave pubblica | RS256 o ES256 | incolli la chiave pubblica PEM; la privata resta nel prodotto |
| URL JWKS | RS256 o ES256 | l'indirizzo https del tuo JWKS; Aladia sceglie la chiave dal kid |
Aladia accetta solo l'algoritmo configurato: l'alg dell'header non decide niente (niente none, niente HS256 al posto di RS256). RSA almeno 2048 bit, EC sulla curva P-256.
kid nell'header. Il segreto ha un identificativo (kid, per esempio k_1a2b3c4d5e6f). Mettilo sempre nell'header: dopo una rotazione Aladia sa subito quale segreto usare. Con un JWKS di più chiavi è obbligatorio.
I claim
| Claim | Obbligatorio | Tipo | Cosa |
|---|---|---|---|
sub | sì | stringa (≤ 255) | l'id dell'utente nel tuo prodotto, stabile. È la chiave del collegamento: cambia sub = utente nuovo |
aud | sì | stringa o array | l'host dell'accademia: academy.prodotto.com (anche https://academy.prodotto.com); vale anche nome.aladia.io |
iat | sì | secondi Unix | quando l'hai firmato; tollerati 3 minuti nel futuro |
exp | sì | secondi Unix | al massimo 5 minuti dopo iat; consigliati 2 |
jti | sì | stringa (≤ 255) | unico per ogni token (un UUID): ogni token vale una volta |
email | no | stringa | l'email dell'utente |
email_verified | no | booleano | false = Aladia non la usa come email dell'account |
name, given_name, family_name | no | stringhe | nome e cognome; aggiornano il profilo a ogni accesso |
picture | no | URL https | la foto, se il profilo non ne ha una |
locale | no | stringa | it o en (anche it-IT): la lingua del nuovo account |
role | no | student o teacher | predefinito student. Admin e Proprietario mai dal JWT |
courses | no | array di id (≤ 100) | iscrizione ai corsi dell'accademia (vedi sotto) |
replace_courses | no | booleano | true = toglie i corsi dati in passato dall'SSO e non più in lista |
groups | no | array di nomi o id (≤ 50) | i team dell'accademia, solo per teacher |
nbf | no | secondi Unix | se c'è, rispettato (3 minuti di tolleranza) |
Un claim non valido (per esempio role: "admin", courses non array) rifiuta il token con un codice preciso: meglio un errore chiaro che un permesso sbagliato.
Esempi: segreto HS256
Variabili d'ambiente del server del prodotto: ALADIA_SSO_SECRET (il segreto), ALADIA_SSO_KID (il suo kid), ALADIA_ACADEMY_HOST (per esempio academy.prodotto.com).
Node.js (jsonwebtoken)
import { randomUUID } from 'node:crypto'
import jwt from 'jsonwebtoken'
/* Dopo il login del prodotto: user è l'utente autenticato, returnTo arriva da ?return_to. */
export function academyRedirect(user, returnTo = '/') {
const host = process.env.ALADIA_ACADEMY_HOST
const token = jwt.sign(
{
sub: String(user.id),
email: user.email,
email_verified: user.emailVerified,
given_name: user.firstName,
family_name: user.lastName,
locale: user.locale,
role: user.isTeacher ? 'teacher' : 'student',
courses: user.academyCourseIds, // id dei corsi Aladia
},
process.env.ALADIA_SSO_SECRET, // la stringa così com'è
{ algorithm: 'HS256', keyid: process.env.ALADIA_SSO_KID, audience: host, expiresIn: 120, jwtid: randomUUID() },
)
const path = safePath(returnTo, host)
return `https://${host}/sso?jwt=${token}&return_to=${encodeURIComponent(path)}`
}
/* return_to: solo un percorso o un URL della tua accademia. */
function safePath(value, host) {
try {
const url = new URL(value, `https://${host}`)
return url.host === host ? `${url.pathname}${url.search}` : '/'
} catch {
return '/'
}
}Python (PyJWT)
import os, time, uuid
from urllib.parse import quote, urlparse
import jwt # pip install pyjwt
def academy_redirect(user, return_to="/"):
host = os.environ["ALADIA_ACADEMY_HOST"]
now = int(time.time())
token = jwt.encode(
{
"sub": str(user.id),
"aud": host,
"iat": now,
"exp": now + 120,
"jti": str(uuid.uuid4()),
"email": user.email,
"email_verified": user.email_verified,
"name": user.full_name,
"role": "teacher" if user.is_teacher else "student",
},
os.environ["ALADIA_SSO_SECRET"], # la stringa così com'è
algorithm="HS256",
headers={"kid": os.environ["ALADIA_SSO_KID"]},
)
return f"https://{host}/sso?jwt={token}&return_to={quote(safe_path(return_to, host))}"
def safe_path(value, host):
"""return_to: solo un percorso o un URL della tua accademia."""
parsed = urlparse(value)
if parsed.netloc:
return (parsed.path or "/") if parsed.netloc == host else "/"
return value if value.startswith("/") and not value.startswith("//") else "/"PHP (firebase/php-jwt)
<?php
use Firebase\JWT\JWT; // composer require firebase/php-jwt
function academyRedirect(array $user, string $returnTo = '/'): string
{
$host = getenv('ALADIA_ACADEMY_HOST');
$now = time();
$token = JWT::encode([
'sub' => (string) $user['id'],
'aud' => $host,
'iat' => $now,
'exp' => $now + 120,
'jti' => bin2hex(random_bytes(16)),
'email' => $user['email'],
'email_verified' => true,
'given_name' => $user['first_name'],
'family_name' => $user['last_name'],
'role' => $user['is_teacher'] ? 'teacher' : 'student',
], getenv('ALADIA_SSO_SECRET'), 'HS256', getenv('ALADIA_SSO_KID'));
$path = (str_starts_with($returnTo, '/') && !str_starts_with($returnTo, '//')) ? $returnTo : '/';
return "https://{$host}/sso?jwt={$token}&return_to=" . rawurlencode($path);
}Esempi: RS256 con chiave pubblica o JWKS
Genera una coppia di chiavi, tieni la privata nel prodotto e dai ad Aladia la pubblica (PEM) oppure l'URL del tuo JWKS.
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out aladia-sso.key
openssl pkey -in aladia-sso.key -pubout -out aladia-sso.pub # da incollare in "Chiave pubblica"Node.js
import { createPublicKey, randomUUID } from 'node:crypto'
import fs from 'node:fs'
import jwt from 'jsonwebtoken'
const privateKey = fs.readFileSync(process.env.ALADIA_SSO_PRIVATE_KEY_PATH)
const KID = '2026-10'
export const token = (user, host) =>
jwt.sign({ sub: String(user.id), email: user.email, role: 'student' }, privateKey, {
algorithm: 'RS256',
keyid: KID,
audience: host,
expiresIn: 120,
jwtid: randomUUID(),
})
/* Il tuo JWKS (es. GET /.well-known/aladia-jwks.json): solo chiavi pubbliche. */
export const jwks = () => ({
keys: [{ ...createPublicKey(privateKey).export({ format: 'jwk' }), kid: KID, alg: 'RS256', use: 'sig' }],
})Python
import os, time, uuid
import jwt # pip install "pyjwt[crypto]"
PRIVATE_KEY = open(os.environ["ALADIA_SSO_PRIVATE_KEY_PATH"]).read()
def token(user, host):
now = int(time.time())
return jwt.encode(
{"sub": str(user.id), "aud": host, "iat": now, "exp": now + 120, "jti": str(uuid.uuid4()), "role": "student"},
PRIVATE_KEY,
algorithm="RS256",
headers={"kid": "2026-10"},
)PHP
<?php
use Firebase\JWT\JWT;
$privateKey = file_get_contents(getenv('ALADIA_SSO_PRIVATE_KEY_PATH'));
$now = time();
$token = JWT::encode([
'sub' => (string) $user['id'], 'aud' => $host, 'iat' => $now, 'exp' => $now + 120,
'jti' => bin2hex(random_bytes(16)), 'role' => 'student',
], $privateKey, 'RS256', '2026-10');Rotazione con il JWKS. Pubblica la chiave nuova nel JWKS (con un kid nuovo) prima di usarla; firma con la nuova; togli la vecchia dopo qualche minuto. Aladia tiene il JWKS in cache 10 minuti e lo riscarica subito (al massimo una volta al minuto) quando arriva un kid che non conosce.
Rotazione del segreto. In Aladia Rigenera crea un segreto nuovo con un kid nuovo; quello di prima vale ancora 24 ore. Aggiorna ALADIA_SSO_SECRET e ALADIA_SSO_KID nel prodotto entro le 24 ore. In caso di fuga del segreto: Rigenera con "Revoca subito il segreto attuale".
Deep link e remote login
- Deep link: per portare l'utente su una pagina precisa usa
return_to:https://academy.prodotto.com/sso?jwt=…&return_to=/courses/6ab516ab857fed8289e3f276. - Remote login: con l'URL di login del prodotto configurato, chi apre l'accademia senza sessione arriva su
https://<tuo login>?return_to=https%3A%2F%2Facademy.prodotto.com%2Fcourses%2F123. Il tuo endpoint: se l'utente ha già la sessione del prodotto firma subito il JWT, altrimenti mostra il login e poi firma. Ricopiareturn_tonel redirect verso/sso, dopo aver controllato che l'host sia la tua accademia.
Logout
Esci su Aladia chiude la sessione dell'accademia e, se hai configurato l'URL di uscita, porta lì l'utente (per esempio per chiudere anche la sessione del prodotto). Senza URL di uscita, in modalità "Solo dal prodotto" l'utente arriva al login dell'accademia, non alla home: dalla home tornerebbe al prodotto e, con la sessione del prodotto ancora aperta, rientrerebbe subito.
Le due sessioni sono indipendenti: uscire dal prodotto non chiude da solo la sessione dell'accademia.
Iscrizioni ai corsi e ai team (JIT)
- Ruolo:
student= Studente dell'accademia;teacher= Membro (lo staff dei docenti) e Docente nei corsi dicourses. Il JWT promuove da Studente a Docente, ma non retrocede mai e non tocca Admin e Proprietario: per togliere permessi si usa Persone in Aladia. courses: gli id dei corsi dell'accademia (l'id è nell'URL del corso,/courses/<id>). Ogni accesso aggiunge i corsi che mancano; un id che non è dell'accademia finisce negli avvisi del registro, senza bloccare l'accesso. Gli slug dei corsi non esistono ancora.replace_courses: true: toglie i corsi in cui l'utente era stato iscritto dall'SSO e che non sono più incourses. Le iscrizioni fatte a mano o acquistate non si toccano.groups: i team dell'accademia, per nome esatto o per id; solo per iteacher(i team sono dello staff). Per gli studenti finisce un avviso nel registro.
Account già esistenti
sub è la chiave: la prima volta Aladia crea il collegamento (accademia, sub) → account; le volte dopo usa sempre quello, anche se l'email cambia.
| Caso | Cosa succede |
|---|---|
Email nuova per Aladia, email_verified non false | account nuovo con quell'email, già verificato, senza password |
| Email già su Aladia, collegamento spento (predefinito) | account nuovo e separato, con un indirizzo interno, solo per questa accademia. Mai un collegamento automatico |
| Email già su Aladia, collegamento acceso | l'utente vede "Collega il tuo account" e conferma con la password di quell'account Aladia (5 tentativi in 10 minuti); poi il collegamento resta |
Nessuna email o email_verified: false | account con un indirizzo interno |
Gli account nati dall'SSO entrano solo dal prodotto: niente password Aladia, niente "Password dimenticata?". Gli account con l'indirizzo interno non ricevono email da Aladia.
Errori
L'utente vede la pagina Accesso non riuscito col codice; tu lo trovi anche nel registro in Accesso e sicurezza, con il dettaglio, e puoi riprodurlo con Testa token.
| Codice | Causa | Cosa fare |
|---|---|---|
sso_not_configured | nessun segreto o chiave | configura la firma |
sso_disabled | modalità "Solo Aladia" o funzione non attiva | scegli "Solo dal prodotto" o "Misto" |
token_missing, token_malformed | jwt assente o non nel formato a.b.c | controlla il redirect |
alg_not_allowed | algoritmo diverso da quello configurato | firma con l'algoritmo scelto in Aladia |
kid_unknown | kid sconosciuto, ruotato o revocato | usa il segreto corrente e il suo kid |
signature_invalid | firma sbagliata | segreto o chiave errati; il segreto si usa come stringa |
key_invalid, jwks_unavailable | PEM o JWKS non validi o irraggiungibili | controlla la chiave o l'URL (https, pubblico) |
aud_mismatch | aud non è l'host dell'accademia | aud = academy.prodotto.com |
exp_missing, iat_missing | mancano i tempi | aggiungi iat ed exp |
token_expired | scaduto (60 s di tolleranza) | firma il token un attimo prima del redirect |
lifetime_too_long | exp - iat oltre 5 minuti | usa 2 minuti |
iat_in_future, not_yet_valid | orologio del server avanti | sincronizza con NTP |
jti_missing, jti_reused | jti assente o già usato | un jti nuovo per ogni token; non riusare i link |
sub_missing, claim_invalid, role_not_allowed | claim mancanti o non validi | vedi la tabella dei claim |
account_blocked | l'account Aladia è bloccato | contatta l'accademia |
link_expired, link_password_invalid, link_password_missing | conferma del collegamento | rifare l'accesso dal prodotto; password giusta; impostarla con "Password dimenticata?" |
rate_limited | più di 30 accessi al minuto dallo stesso IP | riprova tra un minuto |
Checklist di sicurezza
- Firma il JWT solo sul server; il segreto in una variabile d'ambiente o in un secret manager, mai nel codice o nel repository.
expa 2 minuti,jticasuale per ogni token,aud= host dell'accademia,kidnell'header.- Mai
role: "admin"né dati di permesso che non controlli tu. substabile e non riassegnabile (non l'email, se gli utenti possono cambiarla).email_verified: truesolo se hai davvero verificato l'email.- Valida
return_tonel prodotto: solo l'host della tua accademia. - Non mettere il JWT nei log del prodotto; Aladia lo toglie dall'URL subito e risponde con
Referrer-Policy: no-referrer. - Ruota il segreto quando cambia chi lo conosce; in caso di fuga, "Revoca subito".
Domande frequenti
Posso provare senza far entrare nessuno? Sì: Testa token in Accesso e sicurezza verifica tutto come un accesso vero, ma non crea sessioni e non consuma il jti.
Staff e amministratori? Proprietario, Admin e docenti del workspace entrano sempre anche con l'account Aladia da https://<accademia>/login, anche in "Solo dal prodotto": è l'accesso d'emergenza.
Funziona su nome.aladia.io e sul dominio personalizzato? Sì, su tutti e due; aud può essere l'uno o l'altro.
Funziona nell'app mobile di Aladia? Non ancora: l'SSO è per il web dell'accademia.
OIDC o SAML? Non in questa fase: il JWT firmato copre il caso "il mio prodotto ha già i suoi utenti". L'SSO per lo staff (SAML/OIDC) è nelle fasi successive.
TikTok
Collegare il pixel TikTok al sito dell'accademia per le campagne TikTok, gli eventi standard che inviamo, come verificarli con Test Events e Pixel Helper e come evitare il doppio conteggio.
API d'iscrizione, guida per lo sviluppatore
Iscrivere una persona a un corso dopo una vendita fuori da Aladia (CRM, il tuo Stripe, un gestionale): POST /v2/integrations/enrollments con la chiave API dell'accademia, idempotenza, errori, esempi curl, Node.js e Python, il webhook enrollment.created e le iscrizioni dall'SSO.