Go サーバー SDK

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

net/http アプリでのサインインについては、Go net/http セッション を参照してください。ご自身の API を保護する方法については、API の保護 を参照してください。

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

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 の場合にのみ許可されます。

オプション:

private_key_jwt で認証する

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

  1. ダッシュボードで管理アプリケーションを作成し、その公開鍵を登録します(JWKS URL または直接指定。有効なキーは最大 3 件)。
  2. 秘密鍵はサーバー上に保管します。たとえばシークレットファイルや KMS などです。
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 が送信する内容:

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

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

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

再試行と冪等性キー

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

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 をたどり、すべての項目を順に返します。次のページは、到達したときにのみ取得されるため、ループを抜けると以降のリクエストは行われません。通常どおり、リクエストにページサイズとフィルターを設定します:

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_…)を渡し、受信した生の本文をそのまま渡してください:

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 は、検証済みの本文を解析します。型スイッチを使うと、特定のタイプのフィールドを取得できます:

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

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

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 とともにそれを返します。未知のコードはそのまま返されるため、サーバーに新しいコードが追加されてもクライアントが壊れることはありません:

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、トークン、キーが含まれることはなく、呼び出し自体も影響を受けません。

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