Go net/http セッション

パッケージ github.com/keenauth/keenauth-go/bff は、Go の net/http アプリケーションにサインイン機能を追加します。サーバーが OAuth クライアントとなり、すべてのトークンをサーバー側で保持します。ブラウザが受け取るのは暗号化された HttpOnly のセッション Cookie だけで、アクセストークン、リフレッシュトークン、ID トークンが届くことはありません。これは Backend-for-Frontend(BFF)パターンです。パッケージは標準ライブラリのみを使用します。まだ公開されていません。

ベアラートークンを受け取る API を保護する場合は、API の保護 を参照してください。

始める前に

  1. ダッシュボードで、環境内に Web アプリケーションを作成します。
  2. クライアント認証方式を選択します。クライアントシークレット(bff.ClientSecretBasic)または private_key_jwt(bff.PrivateKeyJWT)のいずれかです。private_key_jwt を使う場合は、アプリケーションの公開鍵を登録し、秘密鍵はサーバー上で保管してください。
  3. リダイレクト URI https://app.acme.example/auth/callback と、サインアウト後のリダイレクト URI https://app.acme.example/ を追加します。
  4. 32 文字以上の Cookie 用シークレットを生成します。たとえば openssl rand -base64 32 を使用します。シークレットはリポジトリではなく、シークレットストアに保管してください。

セットアップ

package main

import (
	"fmt"
	"net/http"
	"os"

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

func main() {
	auth, err := bff.New(bff.Options{
		Issuer:     "https://acme.eu.keenauth.app", // your environment's issuer, exactly
		ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
		ClientAuth: bff.ClientSecretBasic{Secret: os.Getenv("KEENAUTH_CLIENT_SECRET")},
		AppBaseURL: "https://app.acme.example",
		Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
	})
	if err != nil {
		panic(err) // configuration errors stop the app at start-up
	}

	mux := http.NewServeMux()
	mux.Handle("GET /orders", auth.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s := bff.FromContext(r.Context()).Session
		fmt.Fprintf(w, "Signed in as %s", s.User.Email)
	})))

	http.ListenAndServe(":8080", auth.Middleware(mux))
}

クライアントシークレットの代わりに private_key_jwt を使うには、秘密鍵を渡します。パッケージは、トークンリクエストと PAR リクエストごとに新しいクライアントアサーションを署名します(iss と sub はクライアント ID、aud は発行者、有効期間は 2 分、jti は毎回新しい値になります)。

pemBytes, err := os.ReadFile("/run/secrets/keenauth-app-key.pem")
if err != nil {
	panic(err)
}
key, err := assertion.ParsePrivateKeyPEM(pemBytes) // import "github.com/keenauth/keenauth-go/assertion"
if err != nil {
	panic(err)
}
auth, err := bff.New(bff.Options{
	Issuer:     "https://acme.eu.keenauth.app",
	ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
	ClientAuth: bff.PrivateKeyJWT{Key: key, KeyID: "app-key-2026-11"},
	AppBaseURL: "https://app.acme.example",
	Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
})

ES256(P-256)、EdDSA(Ed25519)、RS256 および PS256(RSA、2048 ビット以上)に対応しています。アルゴリズムは鍵に従って決まり、RSA 鍵で PSS を使う場合は Algorithm: "PS256" を指定します。Key には任意の crypto.Signer を指定できるため、KMS や HSM で保管している鍵も使用できます。別の場所で署名する場合は、代わりに bff.PrivateKeyJWT{Assertion: f} を設定してください。f は呼び出しのたびに新しい署名済みアサーションを返す関数です。Key と Assertion は両方ではなく、いずれか一方のみを設定してください。

AppBaseURL はアプリの公開アドレスです。パッケージはリダイレクト URI と、すべてのリダイレクトをこの値から組み立てます。リクエストの Host ヘッダーや X-Forwarded-* ヘッダーからは組み立てません。平文の http は localhost の場合にのみ許可されます。

ルート

Middleware は 3 つのルートを提供し、それ以外のすべてのリクエストでセッションを読み取ります。mux.Handle("/auth/", auth) を使って、ルートだけを単独でマウントすることもできます。

ルート 役割
GET /auth/login サインインを開始します。省略可能なクエリパラメータは returnTo(ユーザーが戻る先)と organization(組織にサインインする)です。
GET /auth/callback サインインを完了させ、セッション Cookie を設定します。
GET または POST /auth/logout KeenAuth でサインアウトし、アプリに戻します。

サインインには PKCE(S256)を用いた認可コードフローを使用します。環境がサポートしている場合は、リクエストをサーバーにプッシュ(PAR)します。コールバックでは、パッケージが state、iss、および ID トークン(nonce を含む)を検証します。サインインが成功するたびに新しいセッションが作成され、古いセッションは破棄されます。

returnTo にはアプリ内のパスを指定する必要があります(例: /orders?tab=open)。//evil.example や完全な URL など、それ以外の値は無視され、ユーザーは / に移動します。

ハンドラーの保護

auth.RequireAuth(handler) は、セッションがあるリクエストのみを通過させます。

POST、PUT、PATCH、DELETE など、安全でないメソッドでは、ブラウザがリクエストはアプリから送られたものであることを示す必要があります。具体的には、Sec-Fetch-Site: same-origin を送るか、アプリのオリジンと一致する Origin ヘッダーを送ります。それ以外のリクエストには 403 が返されます。これは SameSite=Lax に加えて、クロスサイトリクエストフォージェリから保護するためのものです。

セッションの利用と API の呼び出し

bff.FromContext(r.Context()) は次の値を返します。

アクセストークンの有効期限が 60 秒以内に迫ると、ハンドラーの実行前にパッケージがトークンを更新します。更新のたびにリフレッシュトークンもローテーションされ、新しいセッション Cookie がレスポンスに設定されます。サーバーがリフレッシュトークンを拒否した場合、セッションは終了します。一時的なサーバーエラーの場合は、セッションは変更されません。1 つのプロセス内で同時に発生したリクエストは、1 回のリフレッシュを共有します。

組織の切り替え

auth.SwitchOrganizationPath("org_01j9…", "/dashboard") は、/auth/login へのリンクを返します。このリンクを使うと、ユーザーは指定した組織で再度サインインします。KeenAuth はメンバーシップを確認します。サインイン後、Session.User.OrgID にその組織が格納されます。

サインアウト

フォームから /auth/logout に POST を送信します。パッケージはセッション Cookie を削除し(ストアを使用している場合は保存済みセッションも削除し)、リフレッシュトークンを取り消して、ブラウザを環境のログアウトエンドポイントに送ります。そこからアプリに戻されます。ブラウザが別のサイトからのリクエストとして示したリクエストは拒否されます。Sec-Fetch-Site を送らないブラウザでは、自社のページからの POST によってのみサインアウトできます。

セッションの保存

デフォルトでは、セッション全体が Cookie に格納されます。この Cookie は、シークレットから導出した鍵を用いて AES-256-GCM で暗号化され、認証されます。変更された Cookie や複製された Cookie は拒否されます。Cookie の名前は __Host-keenauth_session.0 で、Secure、HttpOnly、SameSite=Lax、Path=/ が設定されます。http://localhost では、__Host- プレフィックスと Secure は付与されません。大きなセッションは最大 3 つの Cookie に分割されます。

トークンをサーバー側に保持するには、Options.Store に bff.SessionStore(Get、Set、Delete を持つ型)を設定します。たとえば Redis を使用する実装などです。この場合、Cookie には暗号化されたランダムなセッション ID のみが格納されます。サインインのたびに新しい ID が作成され、以前のレコードは削除されます。ストアにはトークンが含まれるため、アクセスを制限してください。bff.MemoryStore は開発環境とテストでのみ使用してください。

セッションの有効期間は最長 7 日です(SessionMaxAge)。リフレッシュしても有効期間は延長されません。シークレットを変更するには、新しいシークレットを先頭に指定します: Secrets: []string{newSecret, oldSecret}。古いシークレットは SessionMaxAge の経過後に削除してください。

現在の制限