
# Go API を保護する

このクイックスタートでは、KeenAuth アクセストークンで Go `net/http` API を保護します。Go SDK の `protect` ミドルウェアを追加し、1 つのルートにスコープを要求して、SDK のテストキットで API をオフラインでテストします。このミドルウェアは標準ライブラリのみを使用し、`http.Handler` を受け付けるあらゆるルーターで動作します。

この API には 3 つのエンドポイントがあります:

| エンドポイント | 必要条件 |
|---|---|
| `GET /api/public` | なし |
| `GET /api/private` | API 用の有効なアクセストークン |
| `GET /api/messages` | `messages:read` スコープを持つ有効なアクセストークン |

## 始める前に

- KeenAuth の環境。その発行者は `https://your-environment.eu.keenauth.app` のような形式です。
- 環境に登録された API（[API を作成する](/docs/api/management/apis/create)）。その `identifier`（例: `https://api.example.com`）が、API が受け付けるトークンの対象（audience）です。
- その API 上の `messages:read` スコープ。これを呼び出すアプリケーションに付与されていること。
- Go 1.26 以降。

## 1. SDK をインストールする

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

SDK はまだ公開されていません。公開されるまでは、`go.mod` の `replace` ディレクティブを使って、サンプルは SDK のソースに対してビルドされます。

## 2. 設定する

API は設定を環境変数からのみ読み込みます:

| 変数 | 必須 | 意味 |
|---|---|---|
| `KEENAUTH_ISSUER` | はい | 環境の発行者 |
| `KEENAUTH_AUDIENCE` | はい | API の識別子 |
| `PUBLIC_BASE_URL` | はい | クライアントが呼び出す URL（[プロキシの背後](#プロキシの背後)を参照） |
| `KEENAUTH_WEBHOOK_SECRET` | 推奨 | `token.revoked` を配信する Webhook エンドポイントの `idp_whsec_` 署名シークレット（ローテーション中は 2 つをカンマ区切りで指定）。これがないと API は取り消されたトークンを検知できません |
| `ADDR` | いいえ | リッスンアドレス。既定値は `:8080` |

## 3. ルートを保護する

`NewHandler` はミドルウェアとルートを作成します。`m.Handler` は有効なトークンを要求し、`m.Require` はさらにスコープも要求します。ハンドラーは `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` は設定から実際の検証器を構築します。検証器は最初のトークンを受け取ったときに、環境から署名鍵を取得します。

```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 を実行して呼び出す

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

最初の呼び出しとして、API 上で `messages:read` スコープを持つマシン間（machine-to-machine）アプリケーションの client credentials グラントでトークンを取得します:

```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. ネットワークなしでテストする

テストキットのパッケージ `keenauthtest` は、公開テスト鍵で署名したトークンを発行し、それらのトークンだけを受け付ける検証器を構築します。テストはその検証器を `NewHandler` に渡すため、ネットワークも KeenAuth アカウントも不要です。詳しくは [オフラインテストキット](/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 ./...
```

サンプルのテストでは、有効なトークンが受け付けられること、また、トークンがない・期限切れ・対象 API が異なる・スコープがない・URL クエリにトークンがある・取り消されている場合に、以下のステータスで拒否されることを確認します。さらに、`PUBLIC_BASE_URL` の証明を付けた DPoP バインドトークンも送信します。テストでは署名付きの `token.revoked` 配信も送信します。有効な配信はトークンを取り消し、偽造・改ざん・古い配信は `400` を返して何も取り消さず、同じ配信を繰り返した場合は効果が 1 回だけ生じます。

## レスポンス

| リクエスト | ステータス | 本文の `code` |
|---|---|---|
| `Authorization` ヘッダーがない | `401` | `missing_token` |
| 期限切れのトークン、別の API 向けのトークン、不正な署名、取り消されたトークン | `401` | `invalid_token` |
| 必要なスコープがないトークン | `403` | `insufficient_scope` |
| URL クエリ内のトークン（`?access_token=`） | `400` | `invalid_request` |

本文は `application/problem+json` で、`WWW-Authenticate` ヘッダーには標準のチャレンジが含まれます。完全な表は [ミドルウェアで API を保護する](/docs/sdk/protect-api#応答) にあります。

## プロキシの背後

クライアントが DPoP を使用する場合、ミドルウェアは各証明がクライアントの呼び出した URL に対して作成されたことを確認します。`PUBLIC_BASE_URL` には、プロキシやロードバランサーの背後にあっても、クライアントが呼び出す外部 URL（例: `https://api.example.com`）を設定してください。この設定は必須で、`https` を使用する必要があります（`http` は `localhost`、`127.0.0.1`、`[::1]` の場合のみ可）。設定がない場合、アプリは起動を拒否します。ミドルウェアは `X-Forwarded-*` ヘッダーを決して信頼しません。

## 取り消されたトークン

アクセストークンは期限切れまで有効なままです。そのため KeenAuth は、トークンが取り消されたことを API に通知します。サンプルでは、インメモリの取り消しキャッシュをミドルウェアに渡し、それにデータを供給する `token.revoked` Webhook を `POST /webhooks/keenauth`（`main.go, webhook.go`）で提供しています。

1. ダッシュボードで、URL `https://<your host>/webhooks/keenauth` を持ち `token.revoked` を購読する Webhook エンドポイントを作成し、その署名シークレットを `KEENAUTH_WEBHOOK_SECRET` としてエクスポートします。
2. ハンドラーは生の本文に対して SDK で署名を検証し、署名が無効な場合、またはタイムスタンプが 5 分以上ずれている場合は `400` を返します。イベントのセレクター（`access_token`、`session`、`user`、`client`、`org`、`org_user`、`client_user`）を取り消しマーカーに対応付け、`204` を返します。その後ミドルウェアは、`not_before` より前に発行された一致するすべてのトークンを拒否します。
3. KeenAuth は少なくとも 1 回は配信するため、ハンドラーは各 `webhook-id` を 1 回だけ適用します。マーカーを保存できない場合は `503` を返し、KeenAuth はイベントを再配信します。それ以外のイベントタイプや、どのアクセストークンにも一致しないセレクター（`refresh_family`、`api`）は無視します。

ハンドラーは、イベント ID、セレクターの種類、理由をログに記録しますが、本文・署名・シークレットは決して記録しません。各プロセスは独自のキャッシュを持ちます。複数のインスタンスで運用する場合は、同じ `Revoke` メソッドの背後にある共有ストアにマーカーを保存してください。詳しくは [取り消しキャッシュ](/docs/sdk/protect-api#取り消しキャッシュtokenrevoked) を参照してください。

## 次のステップ

- [ミドルウェアで API を保護する](/docs/sdk/protect-api): ステップアップ要件、DPoP、およびすべてのオプション。
- [トークンの検証](/docs/sdk/token-verification): ミドルウェアの下で動作する検証器。
- [オフラインテストキット](/docs/sdk/testing)。
