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 を作成する)。その
identifier(例:https://api.example.com)が、API が受け付けるトークンの対象(audience)です。 - その API 上の
messages:readスコープ。これを呼び出すアプリケーションに付与されていること。 - Go 1.26 以降。
1. SDK をインストールする
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 で検証済みのクレームを読み取ります。
// 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 は設定から実際の検証器を構築します。検証器は最初のトークンを受け取ったときに、環境から署名鍵を取得します。
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 を実行して呼び出す
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 グラントでトークンを取得します:
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 アカウントも不要です。詳しくは オフラインテストキット を参照してください。
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
}
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 を保護する にあります。
プロキシの背後
クライアントが 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)で提供しています。
- ダッシュボードで、URL
https://<your host>/webhooks/keenauthを持ちtoken.revokedを購読する Webhook エンドポイントを作成し、その署名シークレットをKEENAUTH_WEBHOOK_SECRETとしてエクスポートします。 - ハンドラーは生の本文に対して SDK で署名を検証し、署名が無効な場合、またはタイムスタンプが 5 分以上ずれている場合は
400を返します。イベントのセレクター(access_token、session、user、client、org、org_user、client_user)を取り消しマーカーに対応付け、204を返します。その後ミドルウェアは、not_beforeより前に発行された一致するすべてのトークンを拒否します。 - KeenAuth は少なくとも 1 回は配信するため、ハンドラーは各
webhook-idを 1 回だけ適用します。マーカーを保存できない場合は503を返し、KeenAuth はイベントを再配信します。それ以外のイベントタイプや、どのアクセストークンにも一致しないセレクター(refresh_family、api)は無視します。
ハンドラーは、イベント ID、セレクターの種類、理由をログに記録しますが、本文・署名・シークレットは決して記録しません。各プロセスは独自のキャッシュを持ちます。複数のインスタンスで運用する場合は、同じ Revoke メソッドの背後にある共有ストアにマーカーを保存してください。詳しくは 取り消しキャッシュ を参照してください。
次のステップ
- ミドルウェアで API を保護する: ステップアップ要件、DPoP、およびすべてのオプション。
- トークンの検証: ミドルウェアの下で動作する検証器。
- オフラインテストキット。