
# Eine Go-API schützen

Diese Schnellstartanleitung schützt eine Go-`net/http`-API mit KeenAuth-Zugriffstokens. Sie fügen die `protect`-Middleware des Go-SDKs hinzu, verlangen einen Scope auf einer Route und testen die API offline mit dem Test-Kit des SDKs. Die Middleware verwendet ausschließlich die Standardbibliothek und funktioniert mit jedem Router, der ein `http.Handler` akzeptiert.

Die API hat drei Endpunkte:

| Endpunkt | Erfordert |
|---|---|
| `GET /api/public` | nichts |
| `GET /api/private` | ein gültiges Zugriffstoken für Ihre API |
| `GET /api/messages` | ein gültiges Zugriffstoken mit dem Scope `messages:read` |

## Bevor Sie beginnen

- Eine KeenAuth-Umgebung. Ihr Issuer sieht aus wie `https://your-environment.eu.keenauth.app`.
- Eine in der Umgebung registrierte API ([API anlegen](/docs/api/management/apis/create)). Ihr `identifier`, zum Beispiel `https://api.example.com`, ist die Zielgruppe der Tokens, die Ihre API akzeptiert.
- Der Scope `messages:read` für diese API, gewährt an die Anwendung, die sie aufruft.
- Go 1.26 oder neuer.

## 1. Das SDK installieren

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

Das SDK ist noch nicht veröffentlicht. Bis dahin baut das Beispiel mit dem SDK-Quellcode über eine `replace`-Direktive in `go.mod`.

## 2. Konfigurieren

Die API liest ihre Konfiguration ausschließlich aus Umgebungsvariablen:

| Variable | Pflicht | Bedeutung |
|---|---|---|
| `KEENAUTH_ISSUER` | ja | Der Issuer Ihrer Umgebung |
| `KEENAUTH_AUDIENCE` | ja | Der Bezeichner Ihrer API |
| `PUBLIC_BASE_URL` | ja | Die URL, die Clients aufrufen (siehe [Hinter einem Proxy](#hinter-einem-proxy)) |
| `KEENAUTH_WEBHOOK_SECRET` | empfohlen | Das `idp_whsec_`-Signiergeheimnis des Webhook-Endpunkts, der `token.revoked` zustellt (zwei, durch Kommas getrennt, während Sie es rotieren). Ohne dieses erfährt die API nie von widerrufenen Tokens |
| `ADDR` | nein | Listen-Adresse, Standard `:8080` |

## 3. Ihre Routen schützen

`NewHandler` erstellt die Middleware und die Routen. `m.Handler` verlangt ein gültiges Token; `m.Require` verlangt zusätzlich Scopes. Der Handler liest die verifizierten Claims mit `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` erstellt aus der Konfiguration den echten Verifier. Der Verifier ruft die Signierschlüssel beim ersten gesehenen Token aus Ihrer Umgebung ab.

```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. API starten und aufrufen

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

Für einen ersten Aufruf holen Sie sich ein Token über den Client-Credentials-Grant einer Machine-to-Machine-Anwendung, die den Scope `messages:read` für Ihre API besitzt:

```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. Ohne Netzwerk testen

Das Test-Kit-Paket `keenauthtest` erstellt Tokens, die mit einem öffentlichen Testschlüssel signiert sind, und baut einen Verifier, der nur diese Tokens akzeptiert. Die Tests übergeben diesen Verifier an `NewHandler`, sodass sie weder Netzwerk noch ein KeenAuth-Konto benötigen. Siehe [Offline-Test-Kit](/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 ./...
```

Die Tests des Beispiels prüfen, dass ein gültiges Token akzeptiert wird und dass ein fehlendes, abgelaufenes oder für eine andere Zielgruppe ausgestelltes Token, ein fehlender Scope, ein Token in der URL-Abfrage und ein widerrufenes Token mit den unten stehenden Statuscodes abgelehnt werden. Außerdem senden sie ein DPoP-gebundenes Token mit einem Nachweis für `PUBLIC_BASE_URL`. Die Tests senden zudem signierte `token.revoked`-Zustellungen: Eine gültige widerruft das Token, eine gefälschte, manipulierte oder veraltete erhält `400` und widerruft nichts, und eine wiederholte hat genau eine Wirkung.

## Antworten

| Anfrage | Status | `code` im Body |
|---|---|---|
| Kein `Authorization`-Header | `401` | `missing_token` |
| Abgelaufenes Token, Token für eine andere API, ungültige Signatur, widerrufenes Token | `401` | `invalid_token` |
| Token ohne den erforderlichen Scope | `403` | `insufficient_scope` |
| Token in der URL-Abfrage (`?access_token=`) | `400` | `invalid_request` |

Der Body ist `application/problem+json`, und der `WWW-Authenticate`-Header enthält die Standard-Challenge. Die vollständige Tabelle finden Sie unter [Ihre API mit Middleware schützen](/docs/sdk/protect-api#antworten).

## Hinter einem Proxy

Wenn Clients DPoP verwenden, prüft die Middleware, dass jeder Nachweis für die URL erstellt wurde, die der Client aufgerufen hat. Setzen Sie `PUBLIC_BASE_URL` auf die externe URL, die Clients aufrufen, zum Beispiel `https://api.example.com`, auch hinter einem Proxy oder Load Balancer. Die Variable ist erforderlich und muss `https` verwenden (`http` nur für `localhost`, `127.0.0.1` oder `[::1]`); ohne sie startet die App nicht. Die Middleware vertraut `X-Forwarded-*`-Headern nie.

## Widerrufene Tokens

Ein Zugriffstoken bleibt bis zu seinem Ablauf gültig, daher teilt KeenAuth Ihrer API mit, wann Tokens widerrufen werden. Das Beispiel übergibt der Middleware einen In-Memory-Widerrufs-Cache und stellt einen `token.revoked`-Webhook unter `POST /webhooks/keenauth` (`main.go, webhook.go`) bereit, der diesen Cache füllt.

1. Erstellen Sie im Dashboard einen Webhook-Endpunkt mit der URL `https://<your host>/webhooks/keenauth`, der `token.revoked` abonniert, und exportieren Sie sein Signiergeheimnis als `KEENAUTH_WEBHOOK_SECRET`.
2. Der Handler prüft die Signatur mit dem SDK auf dem rohen Body und antwortet mit `400`, wenn sie ungültig ist oder der Zeitstempel mehr als fünf Minuten abweicht. Er ordnet den Selektor des Ereignisses (`access_token`, `session`, `user`, `client`, `org`, `org_user`, `client_user`) einer Widerrufsmarkierung zu und antwortet mit `204`; die Middleware lehnt danach jedes passende Token ab, das vor `not_before` ausgestellt wurde.
3. KeenAuth liefert mindestens einmal aus, daher wendet der Handler jede `webhook-id` genau einmal an. Kann die Markierung nicht gespeichert werden, antwortet er mit `503`, und KeenAuth stellt das Ereignis erneut zu. Andere Ereignistypen sowie Selektoren, die auf kein Zugriffstoken passen (`refresh_family`, `api`), ignoriert er.

Der Handler protokolliert die Ereignis-ID, den Selektortyp und den Grund, niemals den Body, eine Signatur oder das Geheimnis. Jeder Prozess hat seinen eigenen Cache; bei mehreren Instanzen legen Sie die Markierungen in einem gemeinsam genutzten Speicher hinter derselben `Revoke`-Methode ab. Siehe [Widerrufs-Cache](/docs/sdk/protect-api#widerrufs-cache-tokenrevoked).

## Nächste Schritte

- [Ihre API mit Middleware schützen](/docs/sdk/protect-api): Step-up-Anforderungen, DPoP und alle Optionen.
- [Tokens verifizieren](/docs/sdk/token-verification): der Verifier unter der Middleware.
- [Offline-Test-Kit](/docs/sdk/testing).
