
# Aggiungere l'accesso a un'app React Router

Questa quickstart aggiunge l'accesso KeenAuth a un'app React Router 8 in modalità framework, con il client OpenID Connect standard `openid-client`. Loader e action vengono eseguiti sul server, quindi la tua app è il client OAuth: conserva ogni token in una sessione lato server, e il browser riceve solo l'HTML e un cookie con id di sessione firmato. Aggiungi le route di accesso, proteggi una pagina, chiami un'API protetta con l'access token dell'utente, disconnettiti e testa l'app offline.

Per una single-page app senza server, vedi [Aggiungere l'accesso a una single-page app React](/docs/quickstarts/react).

L'app ha queste route:

| Route | Cosa fa |
|---|---|
| `GET /` | Home page pubblica: un pulsante di accesso, oppure il nome dell'utente e un pulsante di disconnessione |
| `GET /dashboard` | Richiede una sessione. Mostra l'utente e i messaggi restituiti dalla tua API |
| `POST /login` | Avvia l'accesso. Solo da una pagina di questa app |
| `GET /auth/callback` | Verifica la risposta, scambia il codice e avvia la sessione |
| `POST /logout` | Termina la sessione qui e presso KeenAuth. Solo da una pagina di questa app |

## Prima di iniziare

- Un ambiente KeenAuth. Il suo issuer ha la forma `https://your-environment.eu.keenauth.app`.
- Un'applicazione web nell'ambiente ([Creare un'applicazione](/docs/api/management/applications/create)) con un client secret (`client_secret_basic`). In un ambiente `development`, registra il redirect URI `http://localhost/auth/callback` e il post-logout redirect URI `http://localhost/`; un URI di loopback viene registrato senza porta e corrisponde a qualsiasi porta, come `5173`.
- Un'API registrata nell'ambiente ([Creare un'API](/docs/api/management/apis/create)) con lo scope `messages:read`. Il suo `identifier`, per esempio `https://api.example.com`, è l'audience dei suoi access token. La [quickstart Express](/docs/quickstarts/express) crea un'API che accetta questi token.
- Node.js 22.22 o successivo, e un'app React Router in modalità framework (`npx create-react-router@8.5.0`).

## 1. Installare

```sh
npm install --save-exact openid-client@6.8.8
```

`openid-client` implementa il relying party OpenID Connect: discovery, flusso authorization code con PKCE, il controllo `iss` di RFC 9207 e i controlli dell'ID token. L'esempio fissa ogni pacchetto con una versione esatta e installa da `package-lock.json` con `npm ci --ignore-scripts`, quindi durante l'installazione non viene eseguito alcuno script dei pacchetti.

## 2. Configurare

L'app legge la configurazione solo dalle variabili d'ambiente. Inseriscile in `.env`, che `npm run dev` e `npm start` caricano con `node --env-file=.env`, e non committare mai quel file.

| Variabile | Obbligatoria | Significato |
|---|---|---|
| `KEENAUTH_ISSUER` | sì | L'issuer del tuo ambiente, `https` |
| `KEENAUTH_CLIENT_ID` | sì | Il `client_id` dell'applicazione web |
| `KEENAUTH_CLIENT_SECRET` | sì | Il suo client secret |
| `APP_BASE_URL` | sì | L'URL pubblico dell'app. Il redirect URI viene costruito a partire da esso. `http` solo per `localhost`, `127.0.0.1` o `[::1]` |
| `SESSION_SECRET` | sì | Almeno 32 caratteri; firma il cookie di sessione. Generane uno con `openssl rand -base64 32` |
| `API_URL` | sì | L'API che l'app chiama, per esempio `http://localhost:8080` |
| `KEENAUTH_API_AUDIENCE` | sì | L'identificatore di quell'API in KeenAuth |

`loadConfig` in `app/lib/config.server.ts` controlla questi valori alla prima richiesta. Un valore mancante o non utilizzabile interrompe la richiesta, e nessun messaggio di errore contiene un valore:

```ts
  const issuer = url("KEENAUTH_ISSUER", false);
  const appBaseUrl = url("APP_BASE_URL", true).origin;
  const sessionSecret = need("SESSION_SECRET");
  if (sessionSecret.length < 32) throw new Error("SESSION_SECRET must be at least 32 characters");
```

I file che terminano in `.server.ts` non arrivano mai al bundle del browser; React Router interrompe la build se una pagina ne importa uno per il browser.

## 3. Creare il client OpenID Connect

`app/lib/keenauth.server.ts` costruisce il client una sola volta dalla discovery (`<issuer>/.well-known/openid-configuration`):

```ts
export function keenauth(): Promise<client.Configuration> {
  discovered ??= client
    .discovery(config().issuer, config().clientId, undefined, client.ClientSecretBasic(config().clientSecret), {
      // Also check the ID token signature with your environment's keys (jwks_uri), not only the TLS connection.
      execute: [client.enableNonRepudiationChecks],
    })
```

Per impostazione predefinita, `openid-client` considera attendibile un ID token che arriva direttamente dal token endpoint tramite TLS. `enableNonRepudiationChecks` verifica anche la sua firma con le chiavi pubblicate dal tuo ambiente.

## 4. Conservare la sessione sul server

La sessione vive in file sul server; il cookie contiene solo un id di sessione firmato:

```ts
  storage ??= createFileSessionStorage<SessionData>({
    dir: ".sessions",
    cookie: {
      name: "__keenauth_session",
      httpOnly: true,
      sameSite: "lax",
      // Secure whenever the app is served over https; plain http only for localhost.
      secure: config().redirectUri.startsWith("https:"),
      path: "/",
      maxAge: 8 * 60 * 60,
      secrets: [config().sessionSecret],
    },
  });
```

Uno store su file serve un solo server. Con più server, scrivi uno storage di sessione con `createSessionStorage` e uno store condiviso, come Redis. Non usare lo storage di sessione in cookie per i token: due token possono superare il limite di 4 KB del cookie, e il browser li conserverebbe.

## 5. Avviare l'accesso

Le route sono elencate in `app/routes.ts`:

```ts
export default [
  index("routes/home.tsx"),
  route("dashboard", "routes/dashboard.tsx"),
  route("login", "routes/login.ts"),
  route("auth/callback", "routes/callback.ts"),
  route("logout", "routes/logout.ts"),
] satisfies RouteConfig;
```

L'accesso è un form che invia un POST a `/login`:

```tsx
        <Form method="post" action="/login" reloadDocument>
          <button type="submit">Sign in</button>
        </Form>
```

L'action crea un nuovo `state`, `nonce` e verificatore PKCE per ogni accesso e li conserva nella sessione:

```ts
export async function action({ request }: Route.ActionArgs) {
  assertSameOrigin(request);
  const session = await getSession(request);
  const transaction = newTransaction();
  session.set("transaction", transaction);
  const url = await authorizationUrl(transaction);
  return redirect(url.href, { headers: { "Set-Cookie": await sessions().commitSession(session) } });
}
```

La richiesta di autorizzazione invia la challenge S256 del verificatore e l'identificatore della tua API come `resource` (RFC 8707). Senza `resource`, l'access token non è destinato alla tua API e l'API lo rifiuta.

```ts
  return client.buildAuthorizationUrl(await keenauth(), {
    redirect_uri: config().redirectUri,
    // messages:read is a scope of your API; the example API requires it on /api/messages.
    scope: "openid profile email messages:read",
    state: transaction.state,
    nonce: transaction.nonce,
    // PKCE (RFC 7636): S256 of the verifier that only this session knows.
    code_challenge: await client.calculatePKCECodeChallenge(transaction.codeVerifier),
    code_challenge_method: "S256",
    // RFC 8707: an access token for your API.
    resource: config().apiAudience,
  });
```

### Accettare il POST solo dalle tue pagine

React Router rifiuta un POST cross-site verso l'action di una pagina, ma `/login` e `/logout` sono resource route (un'action senza pagina), e per queste il controllo spetta a te. `assertSameOrigin` accetta il POST solo quando il suo header `Origin` (o, in sua assenza, `Referer`) indica `APP_BASE_URL`:

```ts
  const app = new URL(config().redirectUri).origin;
  const source = request.headers.get("Origin") ?? request.headers.get("Referer");
```

## 6. Gestire il callback

Il callback prende per prima cosa la transazione dalla sessione, così uno `state` funziona una sola volta:

```ts
  const old = await getSession(request);
  const transaction = old.get("transaction");
  // The transaction works once: it leaves the session before anything else happens.
  old.unset("transaction");
```

`authorizationCodeGrant` verifica `state` e l'`iss` che il tuo ambiente invia in ogni risposta di autorizzazione (RFC 9207), scambia il codice con il verificatore PKCE e controlla l'ID token: la sua firma, `iss`, `aud`, `exp`, `iat` e il `nonce` di questo accesso. Il redirect URI viene costruito a partire da `APP_BASE_URL`, mai dall'header `Host`:

```ts
  const callbackUrl = new URL(config().redirectUri);
  callbackUrl.search = new URL(request.url).search;
  return client.authorizationCodeGrant(await keenauth(), callbackUrl, {
    pkceCodeVerifier: transaction.codeVerifier,
    expectedState: transaction.state,
    expectedNonce: transaction.nonce,
    idTokenExpected: true,
  });
```

Un controllo non superato risponde con `401` e un codice di errore fisso. Dopo un accesso riuscito, l'app elimina la vecchia sessione e conserva l'utente e i token in una nuova:

```ts
  // A new session id after sign-in prevents session fixation: the old session is deleted, a new one is created.
  await sessions().destroySession(old);
  const session = await sessions().getSession(null);
```

## 7. Proteggere le pagine e chiamare la tua API

Un loader che richiede un utente chiama `requireUser`, che reindirizza alla home page senza sessione. L'esempio non ha un refresh token: quando l'access token scade, l'utente accede di nuovo.

```ts
export async function loader({ request }: Route.LoaderArgs) {
  const { user, accessToken } = await requireUser(request);
  const api = await fetchMessages(accessToken);
  return { user, api };
}
```

Il loader chiama la tua API con l'access token come bearer token. Il token resta sul server; la pagina riceve solo l'utente e i messaggi.

```ts
    response = await fetch(`${config().apiUrl}/api/messages`, {
      headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
    });
```

## 8. Disconnettersi

La disconnessione è un `POST` da una pagina di questa app. L'action termina la sessione locale e manda il browser all'endpoint di logout del tuo ambiente (OpenID Connect RP-Initiated Logout) con `id_token_hint`, `client_id` e `post_logout_redirect_uri`, che lo riporta alla tua app:

```ts
  const session = await getSession(request);
  const idToken = session.get("idToken");
  const destroyed = await sessions().destroySession(session);
  return redirect((await logoutUrl(idToken)).href, { headers: { "Set-Cookie": destroyed } });
```

## 9. Eseguire l'app

Avvia l'API della quickstart Express (o qualsiasi API che accetta i token) ed esegui l'app:

```sh
npm run dev
```

Apri `http://localhost:5173`, scegli **Accedi**, accedi su KeenAuth e apri la dashboard.

## 10. Testare senza rete

I test non richiedono rete né un account KeenAuth. `npm test` compila l'app ed esegue la build tramite il request handler di React Router. Un `fetch` finto simula il tuo ambiente (discovery, un set di chiavi creato per l'esecuzione dei test, il token endpoint) e l'API.

```ts
test("callback: an unknown or reused state is refused before any token request and starts no session", async () => {
  const b = browser();
  await startSignIn(b);
  const wrong = await returnFromKeenAuth(b, { state: "not-the-state" });
```

```sh
npm test
npm run deps    # exact pins, lockfile sources and licences
```

I test verificano che la configurazione venga validata (e che nessun errore contenga un segreto); che l'accesso invii PKCE S256, `state`, `nonce` e `resource` e venga rifiutato da un altro sito o con una `GET`; che il codice venga scambiato con il verificatore corrispondente e con l'autenticazione del client; che l'access token venga inviato all'API nell'header `Authorization` e non arrivi mai al browser; che uno `state` sconosciuto o riutilizzato, un `iss` mancante o errato, un errore di autorizzazione e un ID token di un altro client, con un altro nonce o firmato da un'altra chiave vengano rifiutati senza creare una sessione; che l'accesso avvii un nuovo id di sessione; che una pagina senza sessione reindirizzi alla home page; e che la disconnessione venga rifiutata da un altro sito e vada all'endpoint di logout con `id_token_hint`.

L'accesso completo con un browser reale richiede un ambiente KeenAuth. Non fa parte dei test offline.

## Dietro un proxy o un load balancer

Imposta `APP_BASE_URL` sull'URL pubblico dell'app; il redirect URI e il controllo dell'Origin lo usano, mai l'header `Host`. Se un proxy termina il TLS, `APP_BASE_URL` resta l'URL `https`, quindi il cookie di sessione rimane `Secure`.

## Limiti attuali

- Nessun refresh token e nessun accesso silenzioso: l'utente accede di nuovo quando l'access token scade.
- Lo store di sessione su file conserva i token in file di testo semplice sotto `.sessions/` e non elimina i file scaduti. Proteggi quella directory, oppure usa uno store condiviso che cifri e faccia scadere le sessioni.
- Il client si autentica con un client secret. `openid-client` supporta anche `private_key_jwt`; l'esempio non lo mostra.

## Prossimi passi

- [Proteggere un'API Express](/docs/quickstarts/express): l'API che questa app chiama.
- [Verificare i token](/docs/sdk/token-verification): il verificatore sotto il middleware della tua API.
