
# Go サーバー SDK

モジュール `github.com/keenauth/keenauth-go` は、KeenAuth 管理 API の Go クライアントです。`/v1` の各リソースに対する型付きメソッドは OpenAPI ドキュメントから生成されており（パッケージ `management`、`keenauth.Client` に組み込まれています）、手書きのコアが認証、再試行、冪等性キー、型付きエラー、非推奨の警告を提供します。このモジュールは標準ライブラリのみを使用します。まだ公開されていません。

`net/http` アプリでのサインインについては、[Go net/http セッション](/docs/sdk/go-nethttp) を参照してください。ご自身の API を保護する方法については、[API の保護](/docs/sdk/protect-api) を参照してください。

## API キーでクライアントを作成する

```go
import (
	"context"
	"fmt"
	"os"

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

c, err := keenauth.NewClient("https://api.eu.keenauth.com/v1", os.Getenv("KEENAUTH_API_KEY"))
if err != nil {
	panic(err)
}
user, _, err := c.UsersAPI.GetUser(context.Background(), "usr_01j9zzzzzzzzzzzzzzzzzzzz").Execute()
if err != nil {
	panic(err)
}
fmt.Println(user.GetEmail())
```

ベース URL は、リージョンの API アドレスに `/v1` を付けたものです。キー（`idp_sk_…` または `idp_rk_…`）は `Authorization` ヘッダーでのみ送信され、そのアドレスにのみ送信されます。別のホストへのリダイレクトや、プレーンな `http` へのリダイレクトでは、キーが送信されることはありません。プレーンな `http` は `localhost` の場合にのみ許可されます。

オプション:

- `keenauth.WithHTTPClient(h)`: 独自の `http.Client` を指定します（そのトランスポートはラップされますが、変更はされません）。
- `keenauth.WithTimeout(d)`: 再試行を含む呼び出し全体の制限時間です（デフォルトは 30 秒）。
- `keenauth.WithClientName("my-tool", "1.4.0")`: ツール名を `User-Agent` と `Idp-Client` に追加します。
- `keenauth.WithLogger(l)`: SDK の警告に使用する `*slog.Logger` です（デフォルトは `slog.Default()`）。

## private_key_jwt で認証する

静的なキーの代わりに、管理アプリケーションは独自の鍵ペアを使ってサインインできます。SDK は環境のトークンエンドポイントから管理トークンを取得し、メモリ上に保持して、有効期限の 60 秒前に置き換えます。同時に行われた呼び出しは、1 回のトークンリクエストを共有します。

1. ダッシュボードで管理アプリケーションを作成し、その公開鍵を登録します（JWKS URL または直接指定。有効なキーは最大 3 件）。
2. 秘密鍵はサーバー上に保管します。たとえばシークレットファイルや KMS などです。

```go
import (
	"os"

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

pemBytes, err := os.ReadFile("/run/secrets/keenauth-mgmt-key.pem")
if err != nil {
	panic(err)
}
key, err := assertion.ParsePrivateKeyPEM(pemBytes)
if err != nil {
	panic(err)
}
c, err := keenauth.NewClientWithPrivateKeyJWT("https://api.eu.keenauth.com/v1", keenauth.PrivateKeyJWT{
	Issuer:   "https://acme.eu.keenauth.app", // your environment's issuer, exactly
	ClientID: "app_01j9zzzzzzzzzzzzzzzzzzzz",
	Key:      key,
	KeyID:    "mgmt-key-2026-11", // the kid you registered; optional with a single key
	Scope:    "users:read users:write", // optional; default: the grant's default scopes
})
```

SDK が送信する内容:

- `POST <Issuer>/oauth2/token` を `grant_type=client_credentials`、`client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`、新しい `client_assertion`、および API アドレス（`https://api.eu.keenauth.com`）に設定した `resource` とともに送信します（変更するには `Audience` を使用します）。
- アサーションは、`iss` と `sub` にクライアント ID、`aud` に発行者、ランダムな `jti`、有効期間 2 分を設定した JWT です。ES256（P-256 鍵）、EdDSA（Ed25519）、RS256、または PS256（RSA、2048 ビット以上。PSS を使用する場合は `Algorithm: "PS256"` を設定）で署名されます。

`Key` は任意の `crypto.Signer` なので、鍵を書き出さずに KMS や HSM に保持した鍵でも使用できます。管理トークンの有効期間は 10 分です。API が `401` を返した場合、SDK はキャッシュされたトークンを破棄し、次の呼び出しで新しいトークンを取得します。

トークンリクエストが拒否された場合、HTTP ステータスと OAuth の `error` コードを含む `*keenauth.TokenError` が返されます。たとえば、未知のキーや誤った `KeyID` の場合は `invalid_client` です。このエラーにアサーションやトークンが含まれることはありません:

```go
var te *keenauth.TokenError
if errors.As(err, &te) {
	log.Printf("token request refused: %d %s", te.Status, te.Code)
}
```

## 再試行と冪等性キー

SDK は、次の両方が真の場合にのみ、呼び出しを合計で最大 3 回まで再試行します:

- 応答が `429`、`502`、`503`、`504` のいずれかである、または接続が失敗した（ネットワークエラーであり、証明書エラーではない）。
- メソッドが `GET`、`PUT`、`DELETE` である、または `Idempotency-Key` を指定した `POST` である。

`Idempotency-Key` を受け付ける `POST` 操作（副作用のない `users:search` と `…:preview` の呼び出し、および `users/{id}:unlock` 以外のすべて）には、新しい UUIDv4 キーが付与されます。その呼び出しの再試行はすべて同じキーと同じ本文を送信するため、操作は最大 1 回だけ実行されます。アプリケーションの再起動をまたいで呼び出しを冪等にするには、独自のキー（`A-Z a-z 0-9 _ -` の 16〜64 文字）を指定します。たとえば `.IdempotencyKey(orderKey)` です。SDK がこのキーを置き換えることはありません。`If-Match` を伴う `PATCH`（バージョン管理されたすべての更新）は `429` の場合にのみ再試行されます。レート制限が実行前にリクエストを拒否し、`If-Match` によって変更が二重に適用されないためです。それ以外の `PATCH` は再試行されません。

試行の間、SDK は `Retry-After` が指定する時間（秒数または HTTP の日付）だけ待機します。指定がない場合は、±25 % のジッターを伴いながら、まず 0.5 秒、次に 1 秒待機します。`Retry-After` が 60 秒を超える場合、または待機によってコンテキストの期限を超える場合は、待機せずに応答を返します。コンテキストをキャンセルすると、待機は停止します。

## リストを反復処理する

リストのメソッドは 1 ページを返します。`data` と、`before` および `after` カーソルを含む `list_metadata` です。`keenauth.All` は `after` をたどり、すべての項目を順に返します。次のページは、到達したときにのみ取得されるため、ループを抜けると以降のリクエストは行われません。通常どおり、リクエストにページサイズとフィルターを設定します:

```go
for u, err := range keenauth.All(c.UsersAPI.ListUsers(ctx).Limit(100).Status("active")) {
	if err != nil {
		return err // the iteration ends after an error
	}
	fmt.Println(u.GetId(), u.GetEmail())
}
```

`keenauth.AllBefore` は代わりに `before` をたどります。たとえば `ListLogs` の新しいエントリに向かって進む場合に使用します。生成されたすべてのリストメソッドは両方で動作します。ただし、Events API の `ListEvents` は例外で、`next_cursor` と `has_more` を使ってページングします。range-over-func のループには Go 1.23 以降が必要です。

## Webhook を検証する

パッケージ `github.com/keenauth/keenauth-go/webhook` は、Webhook 配信の署名（Standard Webhooks）を検証します。エンドポイントの署名シークレット（`idp_whsec_…`）を渡し、受信した生の本文をそのまま渡してください:

```go
import (
	"io"
	"net/http"
	"os"

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

verifier, err := webhook.NewVerifier([]string{os.Getenv("KEENAUTH_WEBHOOK_SECRET")})
if err != nil {
	panic(err) // webhook.ErrSecret: the secret is not well formed
}

http.HandleFunc("POST /webhooks/keenauth", func(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
	if err != nil {
		http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
		return
	}
	if err := verifier.Verify(r.Header, body); err != nil {
		http.Error(w, "invalid webhook", http.StatusBadRequest)
		return
	}
	// Handle the event. Store webhook-id and skip a delivery you have already processed.
	w.WriteHeader(http.StatusNoContent)
})
```

`Verify` は、`webhook-timestamp` が現在時刻から前後 5 分以内（両方向）にあり、`webhook-signature` 内の `v1` 署名のいずれかが一致する場合に配信を受け入れます。署名は定数時間で比較され、ほかの署名バージョンは無視されます。エラーには配信が拒否された理由が示されます。各エラーは、`webhook.ErrHeaders`（ヘッダーが不足している）、`webhook.ErrTimestamp`、`webhook.ErrSignature`、`webhook.ErrSecret` のいずれかに対応し、`errors.Is` で判定できます。

シークレットをローテーションする場合は、古いシークレットが使われなくなるまで、新旧両方のシークレットを渡してください: `webhook.NewVerifier([]string{newSecret, oldSecret})`。ローテーション中は、KeenAuth が 1 つのヘッダーに 2 つの署名を含めて送信することもあります。どちらか一方が一致すれば十分です。

## 型付きイベントとクレーム

パッケージ `github.com/keenauth/keenauth-go/events` には、公開イベントスキーマから生成された、顧客向けイベントタイプごとの構造体があります。`webhook.Verifier.VerifyEvent` は配信を検証して解析します。`events.Parse` は、検証済みの本文を解析します。型スイッチを使うと、特定のタイプのフィールドを取得できます:

```go
ev, err := verifier.VerifyEvent(r.Header, body)
if err != nil {
	http.Error(w, "invalid webhook", http.StatusBadRequest) // webhook.Err… or events.ErrParse
	return
}
switch e := ev.(type) {
case *events.UserCreated:
	log.Printf("user %s created", e.Data.Object.ID)
case *events.EmailSendingSuspended:
	log.Printf("sending suspended: %s", e.Data.Object.Reason)
case *events.Unknown:
	// A type this SDK version does not know yet. e.Type and e.Raw() are still available.
}
```

- `Parse` は、エンベロープ（`id`、`type`、`schema_version`、`environment_id`、`occurred_at`、`recorded_at`、`seq`、`data`、`context`）と、タイプのスキーマで宣言されたメンバーを検査します。メンバーが欠けている、または型が誤っている場合は、`data.object.reason` などのパスを含む `*events.ParseError` が返されます。
- 未知のタイプ、またはこの SDK がモデル化していない `schema_version` を持つ既知のタイプは、`*events.Unknown` として返されます。これはエラーではありません。
- スキーマで宣言されていないメンバーは保持されます。`Raw()` はイベント全体を返し、`ObjectJSON()` は `data.object` を返します（フルペイロードモードでは、リソースの管理 API 表現です）。
- 列挙型フィールドの値は既知の値と照合されません。そのため、サーバーから新しい値が届いても解析は失敗しません。

パッケージ `github.com/keenauth/keenauth-go/claims` は、検証済みトークンのクレームに型を付けます。ID トークン（OpenID Connect Core）には `claims.ParseIDToken`、アクセストークン（RFC 9068）には `claims.ParseAccessToken` を使用します。これらはクレームの型を検査しますが、検証は行いません。そのため、パッケージ `verify` が返す `Claims` マップに対して呼び出してください:

```go
at, err := v.VerifyAccessToken(ctx, token, nil)
if err != nil {
	return err
}
c, err := claims.ParseAccessToken(at.Claims)
if err != nil {
	return err
}
log.Printf("client %s, org %s, roles %v", c.ClientID, c.OrgID, c.OrgRoles)
_ = c.Custom["tenant_tier"] // your own claims
```

`aud` は常にスライス（`claims.Audience`）です。契約に含まれないクレームは、独自のクレームとして扱われ、`Custom` に保持されます。

## エラー

API エラーは RFC 9457 の問題詳細ドキュメントです。`keenauth.AsProblem(err)` は、機械可読な `Code` とともにそれを返します。未知のコードはそのまま返されるため、サーバーに新しいコードが追加されてもクライアントが壊れることはありません:

```go
if _, _, err := c.UsersAPI.GetUser(ctx, id).Execute(); err != nil {
	if pe, ok := keenauth.AsProblem(err); ok && pe.Code == "not_found" {
		// …
	}
}
```

## 非推奨の警告

応答に `Deprecation`（RFC 9745）または `Sunset`（RFC 8594）ヘッダーが含まれる場合、SDK はプロセス全体で操作とヘッダー値ごとに 1 回、`WithLogger` で設定したロガーの `WARN` レベルで警告を記録します。警告には、メソッド名と、ID を `{id}` に置き換えたパス、ヘッダー値、そして `rel="deprecation"` または `rel="sunset"` を持つ `Link` が含まれます。ID、トークン、キーが含まれることはなく、呼び出し自体も影響を受けません。

```text
level=WARN msg="keenauth: the API operation is deprecated" operation="GET /users/{id}" deprecation=@1767225600 sunset="Wed, 01 Jul 2026 00:00:00 GMT"
```
