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.

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 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

1. Installa

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:

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

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:

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.

        <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:

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:

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:

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:

      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

      <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:

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.

  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");
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