
# Proteggi un'API Go

Questa guida rapida protegge un'API Go `net/http` con token di accesso KeenAuth. Aggiungi il middleware `protect` dell'SDK Go, richiedi uno scope su una route e testa l'API offline con il test kit dell'SDK. Il middleware usa solo la libreria standard e funziona con qualsiasi router che accetti un `http.Handler`.

L'API ha tre endpoint:

| Endpoint | Richiede |
|---|---|
| `GET /api/public` | nulla |
| `GET /api/private` | un token di accesso valido per la tua API |
| `GET /api/messages` | un token di accesso valido con lo scope `messages:read` |

## Prima di iniziare

- Un ambiente KeenAuth. Il suo issuer ha questo aspetto: `https://your-environment.eu.keenauth.app`.
- Un'API registrata nell'ambiente ([Crea un'API](/docs/api/management/apis/create)). Il suo `identifier`, per esempio `https://api.example.com`, è l'audience dei token che la tua API accetta.
- Lo scope `messages:read` su quell'API, concesso all'applicazione che la chiama.
- Go 1.26 o successivo.

## 1. Installa l'SDK

```sh
go get github.com/keenauth/keenauth-go
```

L'SDK non è ancora pubblicato. Finché non lo sarà, l'esempio viene compilato con il sorgente dell'SDK tramite una direttiva `replace` in `go.mod`.

## 2. Configura

L'API legge la configurazione solo da variabili d'ambiente:

| Variabile | Obbligatoria | Significato |
|---|---|---|
| `KEENAUTH_ISSUER` | sì | L'issuer del tuo ambiente |
| `KEENAUTH_AUDIENCE` | sì | L'identificatore della tua API |
| `PUBLIC_BASE_URL` | sì | L'URL che i client chiamano (vedi [Dietro un proxy](#dietro-un-proxy)) |
| `KEENAUTH_WEBHOOK_SECRET` | consigliata | Il segreto di firma `idp_whsec_` dell'endpoint webhook che consegna `token.revoked` (due, separati da virgola, durante la rotazione). Senza di esso l'API non viene mai a sapere dei token revocati |
| `ADDR` | no | Indirizzo di ascolto, predefinito `:8080` |

## 3. Proteggi le route

`NewHandler` crea il middleware e le route. `m.Handler` richiede un token valido; `m.Require` richiede anche degli scope. Il gestore legge i claim verificati con `protect.ClaimsFrom`.

```go
// NewHandler builds the API. The verifier is a parameter so that tests can pass the offline test kit's verifier;
// main passes a real one. The revocation cache is fed from the token.revoked webhook.
func NewHandler(v protect.AccessTokenVerifier, publicBaseURL string, revocation protect.RevocationChecker) (http.Handler, error) {
	m, err := protect.New(v, protect.Options{
		PublicBaseURL: publicBaseURL,
		Revocation:    revocation,
		OnError: func(r *http.Request, err error) {
			// The error never contains the token or claim values; never log the Authorization or DPoP header.
			log.Printf("rejected %s %s: %v", r.Method, r.URL.Path, err)
		},
	})
	if err != nil {
		return nil, err
	}

	mux := http.NewServeMux()
	// No token needed.
	mux.HandleFunc("GET /api/public", func(w http.ResponseWriter, _ *http.Request) {
		writeJSON(w, map[string]string{"message": "Hello from a public endpoint."})
	})
	// Any valid access token for this API.
	mux.Handle("GET /api/private", m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		claims, _ := protect.ClaimsFrom(r.Context())
		writeJSON(w, map[string]string{"message": "Hello from a private endpoint.", "sub": claims.Subject})
	})))
	// A valid access token that also carries the messages:read scope.
	mux.Handle("GET /api/messages", m.Require(protect.Requirement{Scopes: []string{"messages:read"}})(
		http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			writeJSON(w, map[string]any{"messages": []string{"Hello from a scoped endpoint."}})
		})))
	return mux, nil
}
```

`main` costruisce il verificatore reale dalla configurazione. Il verificatore recupera le chiavi di firma dal tuo ambiente la prima volta che vede un token.

```go
func main() {
	cfg, err := ConfigFromEnv(os.Getenv)
	if err != nil {
		log.Fatal(err)
	}
	// verify.New refuses an issuer under the reserved .invalid domain, so a test token can never pass here.
	v, err := verify.New(verify.Config{Issuer: cfg.Issuer, Audience: cfg.Audience})
	if err != nil {
		log.Fatal(err)
	}
	revocation := protect.NewMemoryRevocationCache(0, nil)
	api, err := NewHandler(v, cfg.PublicBaseURL, revocation)
	if err != nil {
		log.Fatal(err)
	}
	h := api
	if len(cfg.WebhookSecrets) > 0 {
		wv, err := webhook.NewVerifier(cfg.WebhookSecrets) // the error never contains the secret
		if err != nil {
			log.Fatalf("KEENAUTH_WEBHOOK_SECRET: %v", err)
		}
		h = WithWebhook(api, NewWebhookHandler(wv, revocation, nil))
	} else {
		log.Print("KEENAUTH_WEBHOOK_SECRET is not set: token.revoked events are not received, so revoked tokens stay valid until they expire")
	}
	srv := &http.Server{Addr: cfg.Addr, Handler: h, ReadHeaderTimeout: 10 * time.Second}
	log.Printf("listening on %s", cfg.Addr)
	log.Fatal(srv.ListenAndServe())
}
```

## 4. Avvia e chiama l'API

```sh
export KEENAUTH_ISSUER=https://your-environment.eu.keenauth.app
export KEENAUTH_AUDIENCE=https://api.example.com
export PUBLIC_BASE_URL=https://api.example.com
go run .
```

Per una prima chiamata, ottieni un token con il grant client credentials di un'applicazione machine-to-machine che ha lo scope `messages:read` sulla tua API:

```sh
ACCESS_TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d resource=https://api.example.com \
  -d scope=messages:read \
  https://your-environment.eu.keenauth.app/oauth2/token | jq -r .access_token)

curl -i http://localhost:8080/api/messages -H "Authorization: Bearer $ACCESS_TOKEN"
```

## 5. Testa senza rete

Il pacchetto del test kit `keenauthtest` genera token firmati con una chiave di test pubblica e costruisce un verificatore che accetta solo quei token. I test passano quel verificatore a `NewHandler`, quindi non richiedono rete né un account KeenAuth. Vedi [Test kit offline](/docs/sdk/testing).

```go
func newTestAPI(t *testing.T) (http.Handler, *protect.MemoryRevocationCache) {
	t.Helper()
	v, err := keenauthtest.NewVerifier(verify.Config{Audience: audience})
	if err != nil {
		t.Fatal(err)
	}
	cache := protect.NewMemoryRevocationCache(0, nil)
	h, err := NewHandler(v, baseURL, cache)
	if err != nil {
		t.Fatal(err)
	}
	return h, cache
}
```

```sh
go test ./...
```

I test dell'esempio verificano che un token valido venga accettato e che un token mancante, scaduto o per un'altra audience, uno scope mancante, un token nella query dell'URL e un token revocato vengano rifiutati con gli stati indicati sotto. Inviano anche un token legato a DPoP con una prova per `PUBLIC_BASE_URL`. I test inviano inoltre consegne `token.revoked` firmate: una valida revoca il token, una contraffatta, alterata o obsoleta riceve `400` e non revoca nulla, e una ripetuta ha un solo effetto.

## Risposte

| Richiesta | Stato | `code` nel body |
|---|---|---|
| Nessun header `Authorization` | `401` | `missing_token` |
| Token scaduto, token per un'altra API, firma non valida, token revocato | `401` | `invalid_token` |
| Token senza lo scope richiesto | `403` | `insufficient_scope` |
| Token nella query dell'URL (`?access_token=`) | `400` | `invalid_request` |

Il body è `application/problem+json` e l'header `WWW-Authenticate` contiene la sfida standard. La tabella completa è in [Proteggere la tua API con il middleware](/docs/sdk/protect-api#risposte).

## Dietro un proxy

Se i client usano DPoP, il middleware verifica che ogni prova sia stata fatta per l'URL che il client ha chiamato. Imposta `PUBLIC_BASE_URL` sull'URL esterno che i client chiamano, per esempio `https://api.example.com`, anche dietro un proxy o un load balancer. È obbligatorio e deve usare `https` (`http` solo per `localhost`, `127.0.0.1` o `[::1]`); l'app rifiuta di avviarsi senza. Il middleware non si fida mai degli header `X-Forwarded-*`.

## Token revocati

Un token di accesso resta valido fino alla scadenza, quindi KeenAuth comunica alla tua API quando i token vengono revocati. L'esempio passa una cache di revoca in memoria al middleware e serve un webhook `token.revoked` su `POST /webhooks/keenauth` (`main.go, webhook.go`) che la alimenta.

1. Nella dashboard, crea un endpoint webhook con l'URL `https://<your host>/webhooks/keenauth` che si iscrive a `token.revoked`, ed esporta il suo segreto di firma come `KEENAUTH_WEBHOOK_SECRET`.
2. Il gestore verifica la firma con l'SDK sul body grezzo e risponde `400` quando la firma non è valida o il timestamp differisce di oltre cinque minuti. Mappa il selettore dell'evento (`access_token`, `session`, `user`, `client`, `org`, `org_user`, `client_user`) su un marcatore di revoca e risponde `204`; il middleware poi rifiuta ogni token corrispondente emesso prima di `not_before`.
3. KeenAuth consegna almeno una volta, quindi il gestore applica ogni `webhook-id` una sola volta. Se il marcatore non può essere salvato risponde `503` e KeenAuth consegna di nuovo l'evento. Ignora gli altri tipi di evento e i selettori che nessun token di accesso può corrispondere (`refresh_family`, `api`).

Il gestore registra l'id dell'evento, il tipo di selettore e il motivo, mai il body, una firma o il segreto. Ogni processo ha la propria cache; con più istanze, metti i marcatori in un archivio condiviso dietro lo stesso metodo `Revoke`. Vedi [Cache di revoca](/docs/sdk/protect-api#cache-di-revoca-tokenrevoked).

## Prossimi passi

- [Proteggere la tua API con il middleware](/docs/sdk/protect-api): requisiti di step-up, DPoP e ogni opzione.
- [Verificare i token](/docs/sdk/token-verification): il verificatore sotto il middleware.
- [Test kit offline](/docs/sdk/testing).
