概要
前回
でOpenFGAについて紹介しました。
今回は実際にアプリケーションでどのように実装するかを説明します。
環境
- OpenFGA Server 1.8.4
- Go 1.24.0
全体イメージ
アプリケーションサーバ、認証サーバ、認可サーバ(OpenFGA)の関係図は次のようになります。

- ユーザ登録時・更新時にユーザ属性情報をOpenFGAにも登録しておく
- ログイン時にユーザ情報をJWTに含める
- リソースAPIを呼ぶ際に↑のJWTからOpenFGAで権限があるかチェックする
- 本番ではインメモリキャッシュするので都度通信は発生しない
というステップでAPIアクセスの可否を行います。
構築
今回は簡単のため認証サーバは用意しない(アプリケーション内で認証をしたと見做す)形で構築します。
docker-compose.yaml
docker composeでコンポーネントが立ち上がるようにします。
version: "3.8" services: mysql: image: mysql:8 container_name: mysql environment: MYSQL_ROOT_PASSWORD: secret MYSQL_DATABASE: openfga networks: - openfga # マイグレーション用 openfga-migrate: image: openfga/openfga container_name: openfga-migrate command: > migrate --datastore-engine mysql --datastore-uri 'root:secret@tcp(mysql:3306)/openfga?parseTime=true' depends_on: - mysql networks: - openfga openfga: image: openfga/openfga container_name: openfga command: > run --datastore-engine mysql --datastore-uri 'root:secret@tcp(mysql:3306)/openfga?parseTime=true' depends_on: - mysql ports: - "3000:3000" - "8080:8080" - "8081:8081" networks: - openfga networks: openfga: driver: bridge
MySQLをDBとしてOpenFGAを起動します。
OpenFGA用のテーブル等を用意する必要があるため、マイグレーション処理を行うコンテナも実行します。
OpenFGAの設定
次にOpenFGAに認可モデルやタプルを登録します。
http://localhost:3000/playground
にアクセスすれば管理画面になるのでそこから登録します。

※この管理面自体のアクセス制御はないので本番運用では避けましょう
認可モデルスキーマ
今回はシンプルにユーザに直接権限を付与するモデルにします。
model schema 1.1 type user type api relations define owner: [user] define editor: [user] or owner define viewer: [user] or editor
タプル設定
次のようなタプルを登録します。
{ "user": "user:alice", "relation": "viewer", "object": "api:article" }, { "user": "user:alice", "relation": "viewer", "object": "api:item" }, { "user": "user:bob", "relation": "editor", "object": "api:article" }
イメージとしては以下です。
api:articleがあればArticle APIにアクセス可能api:itemがあればItem APIにアクセス可能relationで権限の強さを表現
アプリケーション実装
アプリケーションサーバの実装です。
全体
次のようにミドルウェアで認可処理を行います。
func main() { r := chi.NewRouter() // 簡単のため、本来リクエストから取得すべきJWTオブジェクトを埋め込む r.Use(embedJWT) r.Route("/articles", func(r chi.Router) { r.Use(preauthorize("article")) r.Use(checkAuthorization) r.Get("/", list) r.Post("/", create) r.Route("/{id}", func(r chi.Router) { r.Get("/", get) r.Put("/", update) r.Delete("/", del) }) }) r.Route("/items", func(r chi.Router) { r.Use(preauthorize("item")) r.Use(checkAuthorization) r.Get("/", list) r.Post("/", create) r.Route("/{id}", func(r chi.Router) { r.Get("/", get) r.Put("/", update) r.Delete("/", del) }) }) err := http.ListenAndServe(":9000", r) if err != nil { fmt.Println(err) } }
認証処理
簡単のため、ヘッダーで渡されたユーザ情報をを使うようにします。
通常はJWTの処理を行い、JWTのsubなどを使います。
func embedJWT(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userName := r.Header.Get("X-User") claims := jwt.MapClaims{ "sub": userName, "iss": "example.com", "exp": time.Now().Add(time.Hour * 72).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) ctx := context.WithValue(r.Context(), "jwt", token) next.ServeHTTP(w, r.WithContext(ctx)) }) }
権限マッピング
リソースと権限の強さのマッピングはシンプルに
- viewerならGET
- editorならGET、POST、PUT
- ownerならGET、POST、PUT、DELETE
という形にしています。
func preauthorize(resource string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Context().Value("jwt").(*jwt.Token) claims := token.Claims.(jwt.MapClaims) user := claims["sub"].(string) ctx := context.WithValue(r.Context(), "user", user) var relation string switch r.Method { case "GET": relation = "viewer" case "POST", "PUT": relation = "editor" case "DELETE": relation = "owner" default: relation = "owner" } ctx = context.WithValue(ctx, "relation", relation) object := "api:" + resource ctx = context.WithValue(ctx, "object", object) next.ServeHTTP(w, r.WithContext(ctx)) }) } }
認可処理
認可ミドルウェアの実装は次のようにしています。
func checkAuthorization(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fgaClient, err := NewSdkClient(&ClientConfiguration{ ApiUrl: apiURL, StoreId: storeID, }) if err != nil { http.Error(w, "Unable to build OpenFGA client", http.StatusServiceUnavailable) return } user := r.Context().Value("user").(string) relation := r.Context().Value("relation").(string) object := r.Context().Value("object").(string) body := ClientCheckRequest{ User: "user:" + user, Relation: relation, Object: object, } data, err := fgaClient.Check(context.Background()).Body(body).Execute() if err != nil { http.Error(w, "Unable to check for authorization", http.StatusServiceUnavailable) return } if !(*data.Allowed) { http.Error(w, "Forbidden to access document", http.StatusForbidden) return } next.ServeHTTP(w, r) }) }
動作確認
- alice
- bob
- charlie(未登録ユーザ)
で検証します。
alice
ユーザaliceはタプルで閲覧権限が登録されているのでarticleの取得が可能です。
$ curl -H "X-User:alice" http://localhost:9000/articles/100 got 100
item権限もあるので同様に取得できます。
$ curl -H "X-User:alice" http://localhost:9000/items/itemA got itemA
一方でeditorではないので、PUTやPOSTはコケます。
$ curl -X PUT -H "X-User:alice" http://localhost:9000/items/itemA Forbidden to access document
bob
bobはarticlesリソースのeditor権限があるので通ります。
$ curl -X PUT -H "X-User:bob" http://localhost:9000/articles/100 updated 100
モデル設計上viewerも含んでいるので、閲覧もできます。
$ curl -H "X-User:bob" http://localhost:9000/articles/100 got 100
一方itemはどんな権限も持っていないのでコケます。
$ curl -H "X-User:bob" http://localhost:9000/items/itemA Forbidden to access document
charlie
charlieは何の権限も持ってないので、どのAPIリソースにもアクセスできません。
$ curl -H "X-User:charlie" http://localhost:9000/articles/100 Forbidden to access document
その他
サンプルコード
今回の検証コードはこちらです。
まとめ
OpenFGAを使ってAPIの認可処理を行う簡単な実装例を紹介しました。
管理画面など、権限設計に柔軟性が求められるユースケースで非常に有効なソリューションだと思います。