
# Protéger une API Go

Ce guide de démarrage rapide protège une API Go `net/http` avec des jetons d'accès KeenAuth. Vous ajoutez le middleware `protect` du SDK Go, exigez une autorisation sur une route et testez l'API hors ligne avec le kit de test du SDK. Le middleware n'utilise que la bibliothèque standard et fonctionne avec tout routeur qui accepte un `http.Handler`.

L'API comporte trois points de terminaison :

| Point de terminaison | Exige |
|---|---|
| `GET /api/public` | aucun |
| `GET /api/private` | un jeton d'accès valide pour votre API |
| `GET /api/messages` | un jeton d'accès valide avec l'autorisation `messages:read` |

## Avant de commencer

- Un environnement KeenAuth. Son émetteur ressemble à `https://your-environment.eu.keenauth.app`.
- Une API enregistrée dans l'environnement ([Créer une API](/docs/api/management/apis/create)). Son `identifier`, par exemple `https://api.example.com`, correspond à l'audience des jetons acceptés par votre API.
- L'autorisation `messages:read` sur cette API, accordée à l'application qui l'appelle.
- Go 1.26 ou ultérieur.

## 1. Installer le SDK

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

Le SDK n'est pas encore publié. En attendant, l'exemple se compile avec les sources du SDK grâce à une directive `replace` dans `go.mod`.

## 2. Configurer

L'API lit sa configuration uniquement à partir de variables d'environnement :

| Variable | Obligatoire | Signification |
|---|---|---|
| `KEENAUTH_ISSUER` | oui | L'émetteur de votre environnement |
| `KEENAUTH_AUDIENCE` | oui | L'identifiant de votre API |
| `PUBLIC_BASE_URL` | oui | L'URL appelée par les clients (voir [Derrière un proxy](#derrière-un-proxy)) |
| `KEENAUTH_WEBHOOK_SECRET` | recommandé | Le secret de signature `idp_whsec_` du point de terminaison webhook qui livre `token.revoked` (deux valeurs séparées par des virgules pendant la rotation). Sans lui, l'API n'est jamais informée des jetons révoqués |
| `ADDR` | non | Adresse d'écoute, par défaut `:8080` |

## 3. Protéger vos routes

`NewHandler` crée le middleware et les routes. `m.Handler` exige un jeton valide ; `m.Require` exige en plus des autorisations. Le gestionnaire lit les revendications vérifiées avec `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` crée le vérificateur réel à partir de la configuration. Le vérificateur récupère les clés de signature de votre environnement la première fois qu'il voit un jeton.

```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. Exécuter et appeler 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 .
```

Pour un premier appel, obtenez un jeton avec le flux d'identifiants client d'une application machine à machine qui dispose de l'autorisation `messages:read` sur votre 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. Tester sans réseau

Le paquet du kit de test `keenauthtest` émet des jetons signés avec une clé de test publique et crée un vérificateur qui n'accepte que ces jetons. Les tests transmettent ce vérificateur à `NewHandler` ; ils n'ont donc besoin ni de réseau ni de compte KeenAuth. Voir [Kit de test hors ligne](/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 ./...
```

Les tests de l'exemple vérifient qu'un jeton valide est accepté et qu'un jeton absent, expiré ou destiné à une autre audience, une autorisation manquante, un jeton dans la requête de l'URL et un jeton révoqué sont rejetés avec les statuts ci-dessous. Ils envoient aussi un jeton lié à DPoP avec une preuve pour `PUBLIC_BASE_URL`. Les tests envoient également des livraisons `token.revoked` signées : une livraison valide révoque le jeton, une livraison falsifiée, altérée ou périmée reçoit `400` et ne révoque rien, et une livraison répétée n'a qu'un seul effet.

## Réponses

| Requête | Statut | `code` dans le corps |
|---|---|---|
| En-tête `Authorization` absent | `401` | `missing_token` |
| Jeton expiré, jeton destiné à une autre API, signature invalide, jeton révoqué | `401` | `invalid_token` |
| Jeton sans l'autorisation requise | `403` | `insufficient_scope` |
| Jeton dans la requête de l'URL (`?access_token=`) | `400` | `invalid_request` |

Le corps est `application/problem+json` et l'en-tête `WWW-Authenticate` transporte le défi standard. Le tableau complet se trouve dans [Protéger votre API avec un middleware](/docs/sdk/protect-api#réponses).

## Derrière un proxy

Si les clients utilisent DPoP, le middleware vérifie que chaque preuve a été créée pour l'URL appelée par le client. Définissez `PUBLIC_BASE_URL` sur l'URL externe appelée par les clients, par exemple `https://api.example.com`, y compris derrière un proxy ou un répartiteur de charge. Cette variable est obligatoire et doit utiliser `https` (`http` uniquement pour `localhost`, `127.0.0.1` ou `[::1]`) ; l'application refuse de démarrer sans elle. Le middleware ne fait jamais confiance aux en-têtes `X-Forwarded-*`.

## Jetons révoqués

Un jeton d'accès reste valide jusqu'à son expiration ; KeenAuth informe donc votre API lorsque des jetons sont révoqués. L'exemple transmet au middleware un cache de révocation en mémoire et expose un webhook `token.revoked` à `POST /webhooks/keenauth` (`main.go, webhook.go`) qui l'alimente.

1. Dans le tableau de bord, créez un point de terminaison webhook avec l'URL `https://<your host>/webhooks/keenauth` qui s'abonne à `token.revoked`, puis exportez son secret de signature sous le nom `KEENAUTH_WEBHOOK_SECRET`.
2. Le gestionnaire vérifie la signature avec le SDK sur le corps brut et répond `400` lorsqu'elle est invalide ou que l'horodatage s'écarte de plus de cinq minutes. Il associe le sélecteur de l'événement (`access_token`, `session`, `user`, `client`, `org`, `org_user`, `client_user`) à un marqueur de révocation et répond `204` ; le middleware rejette alors tout jeton correspondant émis avant `not_before`.
3. KeenAuth livre au moins une fois, donc le gestionnaire applique chaque `webhook-id` une seule fois. Si le marqueur ne peut pas être enregistré, il répond `503` et KeenAuth livre à nouveau l'événement. Il ignore les autres types d'événements et les sélecteurs qu'aucun jeton d'accès ne peut correspondre (`refresh_family`, `api`).

Le gestionnaire journalise l'identifiant de l'événement, le type de sélecteur et la raison, jamais le corps, une signature ni le secret. Chaque processus possède son propre cache ; avec plusieurs instances, placez les marqueurs dans un stockage partagé derrière la même méthode `Revoke`. Voir [Cache de révocation](/docs/sdk/protect-api#cache-de-révocation-tokenrevoked).

## Prochaines étapes

- [Protéger votre API avec un middleware](/docs/sdk/protect-api) : exigences d'élévation, DPoP et toutes les options.
- [Vérifier les jetons](/docs/sdk/token-verification) : le vérificateur sous le middleware.
- [Kit de test hors ligne](/docs/sdk/testing).
