
# Sessioni Go net/http

Il pacchetto `github.com/keenauth/keenauth-go/bff` aggiunge l'accesso a un'applicazione Go `net/http`. Il tuo server è il client OAuth e conserva tutti i token lato server. Il browser riceve solo un cookie di sessione cifrato e HttpOnly e non vede mai un access token, un refresh token o un ID token. Questo è il pattern backend-for-frontend (BFF). Il pacchetto usa solo la libreria standard. Non è ancora pubblicato.

Per proteggere invece un'API che riceve bearer token, consulta [Proteggere la tua API](/docs/sdk/protect-api).

## Prima di iniziare

1. Nella dashboard, crea un'applicazione web nel tuo ambiente.
2. Scegli l'autenticazione del client: un client secret (`bff.ClientSecretBasic`) oppure `private_key_jwt` (`bff.PrivateKeyJWT`). Per `private_key_jwt`, registra la chiave pubblica dell'applicazione e conserva la chiave privata sul tuo server.
3. Aggiungi il redirect URI `https://app.acme.example/auth/callback` e il post-logout redirect URI `https://app.acme.example/`.
4. Genera un cookie secret di almeno 32 caratteri, ad esempio con `openssl rand -base64 32`. Conservalo nel tuo secret store, non nel repository.

## Configurazione

```go
package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/keenauth/keenauth-go/bff"
)

func main() {
	auth, err := bff.New(bff.Options{
		Issuer:     "https://acme.eu.keenauth.app", // your environment's issuer, exactly
		ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
		ClientAuth: bff.ClientSecretBasic{Secret: os.Getenv("KEENAUTH_CLIENT_SECRET")},
		AppBaseURL: "https://app.acme.example",
		Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
	})
	if err != nil {
		panic(err) // configuration errors stop the app at start-up
	}

	mux := http.NewServeMux()
	mux.Handle("GET /orders", auth.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s := bff.FromContext(r.Context()).Session
		fmt.Fprintf(w, "Signed in as %s", s.User.Email)
	})))

	http.ListenAndServe(":8080", auth.Middleware(mux))
}
```

Per usare `private_key_jwt` invece di un client secret, passa la chiave privata. Il pacchetto firma una nuova client assertion per ogni richiesta di token e di PAR (`iss` e `sub` sono il tuo client ID, `aud` è l'issuer, durata 2 minuti, un nuovo `jti` ogni volta):

```go
pemBytes, err := os.ReadFile("/run/secrets/keenauth-app-key.pem")
if err != nil {
	panic(err)
}
key, err := assertion.ParsePrivateKeyPEM(pemBytes) // import "github.com/keenauth/keenauth-go/assertion"
if err != nil {
	panic(err)
}
auth, err := bff.New(bff.Options{
	Issuer:     "https://acme.eu.keenauth.app",
	ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
	ClientAuth: bff.PrivateKeyJWT{Key: key, KeyID: "app-key-2026-11"},
	AppBaseURL: "https://app.acme.example",
	Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
})
```

Sono supportati ES256 (P-256), EdDSA (Ed25519), RS256 e PS256 (RSA, 2048 bit o più); l'algoritmo segue la chiave, e `Algorithm: "PS256"` seleziona PSS per una chiave RSA. `Key` può essere qualsiasi `crypto.Signer`, quindi funziona anche una chiave conservata in un KMS o HSM. Se firmi altrove, imposta invece `bff.PrivateKeyJWT{Assertion: f}`, dove `f` restituisce una nuova assertion firmata a ogni chiamata. Imposta uno solo tra `Key` e `Assertion`, non entrambi.

`AppBaseURL` è l'indirizzo pubblico della tua app. Il pacchetto costruisce da lì il redirect URI e ogni redirect, mai dagli header `Host` o `X-Forwarded-*` della richiesta. Il protocollo `http` è accettato solo per `localhost`.

## Route

`Middleware` espone tre route e legge la sessione su ogni altra richiesta. Puoi anche montare le sole route con `mux.Handle("/auth/", auth)`.

| Route | Cosa fa |
|---|---|
| `GET /auth/login` | Avvia l'accesso. Parametri query facoltativi: `returnTo` (dove arriva l'utente) e `organization` (accedi a un'organizzazione). |
| `GET /auth/callback` | Completa l'accesso e imposta il cookie di sessione. |
| `GET` o `POST /auth/logout` | Disconnette in KeenAuth e riporta alla tua app. |

L'accesso usa il flusso authorization code con PKCE (S256). La richiesta viene inviata al server (PAR) quando il tuo ambiente la supporta. Nel callback il pacchetto verifica `state`, `iss` e l'ID token, incluso il suo `nonce`. Ogni accesso riuscito crea una nuova sessione; quella precedente viene eliminata.

`returnTo` deve essere un percorso della tua app, come `/orders?tab=open`. Qualsiasi altro valore, come `//evil.example` o un URL completo, viene ignorato e l'utente arriva su `/`.

## Proteggere gli handler

`auth.RequireAuth(handler)` lascia passare una richiesta solo con una sessione:

- una richiesta di pagina (`GET` o `HEAD`) senza sessione viene reindirizzata a `/auth/login` con `returnTo`;
- una richiesta a `/api/…`, oppure con un altro metodo, riceve `401` con un documento problem. Passa `bff.GuardOptions{API: true}` per rispondere `401` su qualsiasi percorso.

Per `POST`, `PUT`, `PATCH`, `DELETE` e gli altri metodi non sicuri, il browser deve indicare che la richiesta proviene dalla tua app: `Sec-Fetch-Site: same-origin`, oppure un header `Origin` uguale all'origine della tua app. Le altre richieste ricevono `403`. Questo protegge dal cross-site request forgery in aggiunta a `SameSite=Lax`.

## Usare la sessione e chiamare la tua API

`bff.FromContext(r.Context())` restituisce:

- `Session`: `nil`, oppure l'utente con accesso effettuato. `Session.User` ha claim tipizzati dell'ID token come `Subject`, `Email`, `Name`, `OrgID` e `OrgRoles`; tutti i claim sono in `Session.User.Claims`. La sessione non contiene mai un token.
- `AccessToken`: un access token valido per le chiamate lato server, oppure `""`. Non inviarlo al browser.

Quando l'access token scade entro 60 secondi, il pacchetto lo rinnova prima che il tuo handler venga eseguito. Ogni rinnovo ruota il refresh token e imposta il nuovo cookie di sessione nella risposta. Se il server rifiuta il refresh token, la sessione termina. Un errore temporaneo del server lascia la sessione invariata. Le richieste concorrenti in un processo condividono un unico rinnovo.

## Cambiare organizzazione

`auth.SwitchOrganizationPath("org_01j9…", "/dashboard")` restituisce un link a `/auth/login` che fa accedere di nuovo l'utente in quell'organizzazione. KeenAuth verifica l'appartenenza. Dopo l'accesso, `Session.User.OrgID` contiene l'organizzazione.

## Disconnettersi

Invia una richiesta `POST` a `/auth/logout` da un form. Il pacchetto elimina il cookie di sessione (e la sessione memorizzata, se usi uno store), revoca il refresh token e manda il browser all'endpoint di logout del tuo ambiente, che lo riporta alla tua app. Una richiesta che il browser segnala come proveniente da un altro sito viene rifiutata. I browser che non inviano `Sec-Fetch-Site` possono disconnettersi solo con un `POST` dalle tue pagine.

## Archiviazione della sessione

Per impostazione predefinita l'intera sessione è nel cookie, cifrata e autenticata con AES-256-GCM usando una chiave derivata dal secret. Un cookie modificato o copiato viene rifiutato. Il cookie si chiama `__Host-keenauth_session.0` e viene impostato con `Secure`, `HttpOnly`, `SameSite=Lax` e `Path=/`. Su `http://localhost` il prefisso `__Host-` e `Secure` vengono omessi. Le sessioni grandi vengono divise in massimo tre cookie.

Per conservare i token sul server, imposta `Options.Store` su un `bff.SessionStore` (`Get`, `Set`, `Delete`), ad esempio basato su Redis. Il cookie contiene allora solo un ID di sessione casuale cifrato. Ogni accesso crea un nuovo ID ed elimina il record precedente. Lo store contiene token, quindi limitane l'accesso. `bff.MemoryStore` serve solo per sviluppo e test.

Una sessione dura al massimo 7 giorni (`SessionMaxAge`); il rinnovo non la prolunga. Per cambiare il secret, inserisci prima il nuovo: `Secrets: []string{newSecret, oldSecret}`. Rimuovi il vecchio dopo `SessionMaxAge`.

## Limiti attuali

- I token vincolati con DPoP e il back-channel logout non sono ancora gestiti.
- I rinnovi sono condivisi solo all'interno di un singolo processo server. Con più istanze, l'impostazione dell'applicazione `refresh_reuse_interval_s` (predefinita 5 secondi) permette a un rinnovo parallelo di riuscire; se la imposti a `0`, il secondo rinnovo termina la sessione.
