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 の場合にのみ許可されます。
オプション:
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 回のトークンリクエストを共有します。
- ダッシュボードで管理アプリケーションを作成し、その公開鍵を登録します(JWKS URL または直接指定。有効なキーは最大 3 件)。
- 秘密鍵はサーバー上に保管します。たとえばシークレットファイルや 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 が送信する内容:
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 です。このエラーにアサーションやトークンが含まれることはありません:
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 をたどり、すべての項目を順に返します。次のページは、到達したときにのみ取得されるため、ループを抜けると以降のリクエストは行われません。通常どおり、リクエストにページサイズとフィルターを設定します:
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.
}
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 マップに対して呼び出してください:
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"