
# Aggiungi l'accesso a un'applicazione React single-page

Questa quickstart aggiunge l'accesso con KeenAuth a un'applicazione React single-page creata con Vite, usando il browser SDK `@keenauth/js`. L'app accede gli utenti con un redirect, chiama un'API protetta con un access token vincolato tramite DPoP e disconnette gli utenti. Puoi testarla anche offline. L'SDK è descritto in [SDK browser JavaScript](/docs/sdk/js).

In una single-page app i token vivono nel browser. L'SDK li mantiene solo in memoria e li lega a una chiave non estraibile con DPoP, quindi un token rubato non può essere usato senza quella chiave. Se la tua app ha un server, la [Quickstart Next.js](/docs/quickstarts/nextjs) mantiene invece ogni token sul server (backend for frontend), che è la scelta più robusta.

L'app ha questi percorsi:

| Percorso | Cosa fa |
|---|---|
| `/` | Un pulsante di accesso oppure l'utente, un pulsante che chiama la tua API e un pulsante di disconnessione |
| `/callback` | Il redirect URI. L'SDK completa qui l'accesso e l'app torna alla pagina da cui l'utente è partito |

## Prima di iniziare

- Un ambiente KeenAuth. Il suo issuer ha la forma `https://your-environment.eu.keenauth.app`.
- Un'applicazione single-page nell'ambiente ([Crea un'applicazione](/docs/api/management/applications/create)) con `app_type` `spa` e `token_endpoint_auth_method` `none` (un client pubblico non ha segreto). In un ambiente `development`, registra:
  - il redirect URI `http://localhost/callback` (un redirect URI loopback viene registrato senza porta e corrisponde a qualsiasi porta);
  - il post-logout redirect URI `http://localhost/`;
  - l'origine web consentita `http://localhost:5173`, così il browser può chiamare l'endpoint dei token.
- Un'API registrata nell'ambiente ([Crea 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.18 o successivo.

## 1. Installa

```sh
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install @keenauth/js
```

L'SDK non è ancora pubblicato. Finché non lo sarà, l'esempio collega il sorgente dell'SDK nel suo `package.json`. L'esempio fissa `vite@8.3.4`, `@vitejs/plugin-react@6.1.2`, `react@19.3.1` e `react-dom@19.3.1`.

## 2. Configura

L'app legge la configurazione solo da variabili d'ambiente. Mettile in `.env.local`, che Vite carica, e non committare mai quel file. Vite incorpora ogni variabile `VITE_` nel JavaScript che il browser scarica, quindi non mettere lì nessun segreto. Questa app non ne ha.

| Variabile | Obbligatoria | Significato |
|---|---|---|
| `VITE_KEENAUTH_ISSUER` | sì | L'issuer del tuo ambiente |
| `VITE_KEENAUTH_CLIENT_ID` | sì | Il `client_id` dell'applicazione single-page |
| `VITE_KEENAUTH_API_AUDIENCE` | sì | L'identificatore della tua API in KeenAuth |
| `VITE_API_URL` | no | URL base della tua API. Vuoto significa l'origine dell'app |
| `API_PROXY_TARGET` | no | Solo sviluppo: dove il dev server inoltra `/api`; predefinito `http://localhost:8080` |

`loadConfig` legge e verifica questi valori. Se una variabile manca, ferma l'app con un errore che ne indica il nome:

```ts
export function loadConfig(env: Readonly<Record<string, unknown>>): AppConfig {
  const need = (name: string): string => {
    const v = env[name];
    if (typeof v !== "string" || v.trim() === "") throw new Error(`${name} is required (see .env.example)`);
    return v.trim();
  };
```

## 3. Crea il client SDK

```ts
export function createAuth(config: AppConfig, origin: string, inject: Injectables = {}): KeenAuth {
  return new KeenAuth({
    issuer: config.issuer,
    clientId: config.clientId,
    redirectUri: `${origin}${CALLBACK_PATH}`,
    postLogoutRedirectUri: `${origin}/`,
    // The access token is for this API: its aud is the API identifier (RFC 8707).
    resource: config.apiAudience,
    // messages:read is a scope of that API; the example API requires it on /api/messages.
    scope: "openid profile email offline_access messages:read",
    ...inject,
  });
}
```

`resource` è l'identificatore della tua API (RFC 8707). L'SDK lo invia nella richiesta di autorizzazione, nello scambio del codice e in ogni refresh, e l'audience dell'access token è la tua API. Senza, l'access token non è destinato alla tua API e l'API lo rifiuta. L'SDK verifica ogni opzione alla creazione del client; un issuer o un `resource` non validi lanciano un `KeenAuthError` con codice `config_invalid`.

Crea un solo client per la pagina e passalo a un provider:

```tsx
const config = loadConfig(import.meta.env);
const auth = createAuth(config, window.location.origin);

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <AuthProvider auth={auth}>
      <App apiUrl={config.apiUrl} />
    </AuthProvider>
  </StrictMode>,
);
```

## 4. Accedi e gestisci il callback

Il pulsante di accesso avvia l'accesso con redirect: authorization code flow con PKCE (S256), `state` e `nonce`. `appState` è il percorso a cui tornare.

```tsx
        <button onClick={() => auth.signInRedirect({ appState: window.location.pathname })}>Sign in</button>
```

Quando KeenAuth reindirizza a `/callback`, il provider completa l'accesso. L'SDK verifica `state`, `iss` e l'ID token, e scambia il codice con una prova DPoP. Una risposta di autorizzazione può essere usata una sola volta, e `StrictMode` di React esegue due volte gli effect in sviluppo, quindi `completeSignIn` ricorda il primo risultato:

```ts
export function completeSignIn(auth: KeenAuth, href: string): Promise<string | null> {
  const url = new URL(href);
  if (url.pathname !== CALLBACK_PATH || !(url.searchParams.has("code") || url.searchParams.has("error"))) {
    return Promise.resolve(null);
  }
  let done = completions.get(auth);
  if (!done) {
    done = auth.handleRedirectCallback(href).then((r) => safeReturnPath(r.appState));
    completions.set(auth, done);
  }
  return done;
}
```

Il provider poi rimuove `code` e `state` dalla barra degli indirizzi. Come percorso di ritorno viene usato solo un percorso interno alla tua app, così un `appState` costruito ad arte non può mandare l'utente su un altro sito:

```ts
export function safeReturnPath(path: string | undefined): string {
  return path !== undefined && /^\/(?![/\\])/.test(path) ? path : "/";
}
```

Servi `index.html` per `/callback` anche in produzione (il dev server di Vite lo fa già).

## 5. Chiama la tua API

`authorizedFetch` invia `Authorization: DPoP <token>` con una prova nuova legata al token, e rinnova il token quando sta per scadere:

```ts
export async function fetchMessages(auth: KeenAuth, apiUrl: string): Promise<unknown> {
  const res = await auth.authorizedFetch(`${apiUrl}/api/messages`, { headers: { Accept: "application/json" } });
  if (!res.ok) throw new Error(`The API answered ${res.status}`);
  return res.json();
}
```

In sviluppo, il dev server di Vite inoltra `/api` alla tua API, così il browser la chiama sull'origine dell'app e l'API non ha bisogno di CORS:

```ts
      proxy: { "/api": env.API_PROXY_TARGET || "http://localhost:8080" },
```

L'API verifica la prova DPoP rispetto all'URL che il browser ha chiamato. Quando esegui l'API della Quickstart Express dietro questo proxy, imposta il suo `PUBLIC_BASE_URL` a `http://localhost:5173`. In produzione, servi l'API sulla stessa origine oppure abilita CORS per l'origine della tua app, e imposta `VITE_API_URL`.

## 6. Disconnettiti

```tsx
      <button onClick={() => auth.signOut()}>Sign out</button>
```

`signOut` elimina i token, revoca il refresh token e invia il browser all'endpoint di logout del tuo ambiente, che lo riporta a `/`.

## 7. Avvia l'app

Avvia l'API della Quickstart Express con `PUBLIC_BASE_URL=http://localhost:5173` e `KEENAUTH_AUDIENCE` impostato all'identificatore della tua API, poi avvia l'app:

```sh
cp .env.example .env.local   # then fill it in
npm run dev
```

Apri `http://localhost:5173`, scegli **Sign in**, accedi su KeenAuth e scegli **Call the API**.

L'SDK mantiene i token solo in memoria. Dopo un ricaricamento della pagina l'utente risulta disconnesso nell'app e deve accedere di nuovo; la sessione KeenAuth di solito rende questo un redirect senza richiesta di credenziali.

## 8. Testa senza rete

I test non richiedono rete né un account KeenAuth. Un `fetch` finto fa da authorization server e da API, e l'ID token è firmato con una chiave creata per il test. Node esegue direttamente i test TypeScript.

```ts
  assert.deepEqual(await fetchMessages(s.auth, ""), { messages: ["hello"] });
  const call = s.seen.find((c) => c.url === `${ORIGIN}/api/messages`)!;
  assert.equal(call.headers.get("Authorization"), "DPoP access-token-1");
```

```sh
npm test
npm run typecheck
npm run deps    # pinned versions, lockfile sources and licences
npm run build
```

I test verificano che la configurazione venga validata, che l'SDK rifiuti un issuer o un `resource` non validi, che l'accesso invii PKCE S256 e la tua API come `resource`, che il callback venga completato una sola volta e invii `resource` con il codice, che uno `state` falsificato venga rifiutato prima di qualsiasi richiesta di token, che dopo l'accesso venga usato solo un percorso interno all'app, che nessun token venga scritto nello storage e che la chiamata API porti il token vincolato a DPoP e la sua prova.

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

## Passi successivi

- [SDK browser JavaScript](/docs/sdk/js): ogni opzione, dove vengono conservati i token e i limiti attuali.
- [SDK React](/docs/sdk/react): componenti incorporati per accesso, iscrizione e account.
- [Proteggi un'API Express](/docs/quickstarts/express): l'API che questa app chiama.
