AladiaDocs
Sviluppatori

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
  1. 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.
  2. Il prodotto autentica l'utente come fa sempre.
  3. Il server del prodotto firma il JWT e risponde con un redirect a https://<accademia>/sso?jwt=<token>&return_to=<percorso>.
  4. Aladia verifica il token, apre la sessione (un cookie httpOnly dell'accademia) e porta l'utente a return_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.

ParametroObbligatorioCosa
jwtsìil token firmato
return_tonodove 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:

MetodoAlgoritmoCosa configuri
Segreto (consigliato per iniziare)HS256Aladia genera un segreto di 32 byte e te lo mostra una volta. Si usa così com'è, come stringa (non decodificarlo da base64).
Chiave pubblicaRS256 o ES256incolli la chiave pubblica PEM; la privata resta nel prodotto
URL JWKSRS256 o ES256l'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

ClaimObbligatorioTipoCosa
subsìstringa (≤ 255)l'id dell'utente nel tuo prodotto, stabile. È la chiave del collegamento: cambia sub = utente nuovo
audsìstringa o arrayl'host dell'accademia: academy.prodotto.com (anche https://academy.prodotto.com); vale anche nome.aladia.io
iatsìsecondi Unixquando l'hai firmato; tollerati 3 minuti nel futuro
expsìsecondi Unixal massimo 5 minuti dopo iat; consigliati 2
jtisìstringa (≤ 255)unico per ogni token (un UUID): ogni token vale una volta
emailnostringal'email dell'utente
email_verifiednobooleanofalse = Aladia non la usa come email dell'account
name, given_name, family_namenostringhenome e cognome; aggiornano il profilo a ogni accesso
picturenoURL httpsla foto, se il profilo non ne ha una
localenostringait o en (anche it-IT): la lingua del nuovo account
rolenostudent o teacherpredefinito student. Admin e Proprietario mai dal JWT
coursesnoarray di id (≤ 100)iscrizione ai corsi dell'accademia (vedi sotto)
replace_coursesnobooleanotrue = toglie i corsi dati in passato dall'SSO e non più in lista
groupsnoarray di nomi o id (≤ 50)i team dell'accademia, solo per teacher
nbfnosecondi Unixse 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: 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. Ricopia return_to nel 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 di courses. 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ù in courses. Le iscrizioni fatte a mano o acquistate non si toccano.
  • groups: i team dell'accademia, per nome esatto o per id; solo per i teacher (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.

CasoCosa succede
Email nuova per Aladia, email_verified non falseaccount 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 accesol'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: falseaccount 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.

CodiceCausaCosa fare
sso_not_configurednessun segreto o chiaveconfigura la firma
sso_disabledmodalità "Solo Aladia" o funzione non attivascegli "Solo dal prodotto" o "Misto"
token_missing, token_malformedjwt assente o non nel formato a.b.ccontrolla il redirect
alg_not_allowedalgoritmo diverso da quello configuratofirma con l'algoritmo scelto in Aladia
kid_unknownkid sconosciuto, ruotato o revocatousa il segreto corrente e il suo kid
signature_invalidfirma sbagliatasegreto o chiave errati; il segreto si usa come stringa
key_invalid, jwks_unavailablePEM o JWKS non validi o irraggiungibilicontrolla la chiave o l'URL (https, pubblico)
aud_mismatchaud non è l'host dell'accademiaaud = academy.prodotto.com
exp_missing, iat_missingmancano i tempiaggiungi iat ed exp
token_expiredscaduto (60 s di tolleranza)firma il token un attimo prima del redirect
lifetime_too_longexp - iat oltre 5 minutiusa 2 minuti
iat_in_future, not_yet_validorologio del server avantisincronizza con NTP
jti_missing, jti_reusedjti assente o già usatoun jti nuovo per ogni token; non riusare i link
sub_missing, claim_invalid, role_not_allowedclaim mancanti o non validivedi la tabella dei claim
account_blockedl'account Aladia è bloccatocontatta l'accademia
link_expired, link_password_invalid, link_password_missingconferma del collegamentorifare l'accesso dal prodotto; password giusta; impostarla con "Password dimenticata?"
rate_limitedpiù di 30 accessi al minuto dallo stesso IPriprova 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.
  • exp a 2 minuti, jti casuale per ogni token, aud = host dell'accademia, kid nell'header.
  • Mai role: "admin" né dati di permesso che non controlli tu.
  • sub stabile e non riassegnabile (non l'email, se gli utenti possono cambiarla).
  • email_verified: true solo se hai davvero verificato l'email.
  • Valida return_to nel 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.

In questa pagina