Ory Hydraでweb3ゲームにサードパーティー向けAPI提供を開始した話

Ory Hydraでweb3ゲームにサードパーティー向けAPI提供を開始した話

更新: 2026年1月6日

運営中のWeb3ゲーム。https://bravefrontierheroes.com/ja に、Ory Hydraで既存のWebゲームサービスに認可の仕組みを導入。ユーザーが自分で必要となるアプリケーションを提供できるような仕組みを作成

新年は動画見ながらワインを飲んでAIをしばきながらピザを作っていたmogettaです。2025年9月ででかい案件が終わって、(これに関してはとても表に出せないほど面白いので聞きたい方は直接お声がけください)今は小さい仕事をぽつぽつ請け負っていたのですがそろそろ本格的な次の仕事を募集しているのでおもろい仕事あるで!って方はお声がけください。

背景

 今お手伝いしているWebゲームでユーザーにAPIを公開し、Web3ゲームのデータを、外部のデベロッパーやコミュニティが活用できるようにしたいとという話を聞き、OAuth2を使う側はやったことがあるがプラットフォームとして提供する側はやったことがないな。と思い、一通り調べた結果、Ory Hydraが今回の要件にあっていた。ということで、そこら辺のまとめ。

 はじめはβ版公開として、APIキーを発行し、限られたデータを渡す形を検討していたが、すでに公開されている情報とはいえユーザーの情報の閲覧の認可や短い期間にAPIの仕様がコロコロ変わってしまうのは開発者としてやりにくかろう。と判断し、年内中に認可の仕様を実装して組み込んでしまおうということで12月から走り出した。

 個人的には本来Web3ではコントラクトがすべて公開されており、公開されたコントラクトを利用したアプリケーション等のエコシステムがどんどん拡張されていくみたいな世界を期待していました。例えばEveOnlineのhttps://zkillboard.com/https://evemaps.dotlan.net/ Destiny2のhttps://destinyitemmanager.com/ のようなものです。

 しかしながら、Web3の資産性だけが過剰に取り沙汰されたり、また通常の開発者ではWeb3の特性が参入障壁になり、簡単にアプリが開発しにくい。という状況が生まれていました。今回の取組はそういった障壁を取り除き、ゲーム参加者が思う、こういうのがあればいいな。を実現できる最初の1歩となります。

 また、AI駆動開発がどんどん本格化している今、swaggerなどを公開して、ユーザー自身がテーラーメイドできるような取り組みは今後必要になってくるのかなとも思っております。

というわけで状況等を整理し、以下のような形でAPI提供を実現しました。

今回作成したAPI ドキュメント https://alabaster-gazelle-9ba.notion.site/Brave-Frontier-Heroes-Developer-Documentation-Forge-API-2d8bb46579d180bdafe0c9fb28d1cc83

swagger https://api.bravefrontierheroes.com/swagger/index.html

clientIDとsecretがあればログイン処理を行い、swagger上で動作確認できる仕組みまで整えてあるのでプログラミングに詳しくないAIコーダーなピーポーにもわかりやすく学べてもらえるはず。

 画像の説明

課題

  • なるべく最速での実装、公開
  • すでにある認証の実装はそのまま使う
  • 元のバックエンドサーバーの実装は変えない
  • コストは最低限
  • AIに聞けばすぐアプリを作れる様な情報を公開しておく

解決策

 AIと壁うちし、妥当な解決策を検討していくうち、こういうったものは自前実装よりも、すでにデファクトスタンダードとなっているOSSに乗っていくほうが良いだろう。という結論に至る。そこでOry Hydraを利用させていただくことにする。   OAuth2.0 / OpenID Connectの「認可」に特化した Ory Hydra を導入し、標準的なアクセストークン(JWT)によるAPI利用サーバーを用意する。認証は既存の仕組みを利用する。

主要なコンポーネント:

  1. フロントエンド - React + Vite - RainbowKit + wagmi v2によるウォレット接続
  2. バックエンド - Go製APIサーバー - Login/Consent Provider - トークンマッピング管理 - APIプロキシ
  3. Ory Hydra - OAuth2/OIDC認証サーバー - Public API (4444) / Admin API (4445)
  4. PostgreSQL - Hydraのデータベース - コンテナ内部でのみアクセス可能
  5. 外部サービス - MCH Plus Auth: ウォレット署名検証・トークン発行 - BFH Game Server: ゲームデータ取得 (gRPC) - AMS: アセット管理システム

Docker Compose Network (hydra-network)

Client Applications

External Services

Database

Hydra Service :4444/:4445

Backend Service :3000

Frontend Service :5173

HTTP :5173

OAuth2 Flow

OAuth2 Flow

API Requests

Admin API :4445

OAuth2 :4444

Redirects

SQL

Signature Verification

Token Exchange

gRPC

with MCH Auth Token

Asset API

User Units/Spheres

MCH Plus Auth API

auth.mch.plus

BFH Game Server

gRPC :5022

AMS API

Asset Management

Web Browser

Test Client

:8080

3rd Party App

OAuth2 Client

React + Vite

RainbowKit + wagmi v2

Go Server

- Login/Consent Provider

- API Proxy

- Token Mapping Store

- Session Management

Ory Hydra v2.2

OAuth2/OIDC Server

:4444 Public

:4445 Admin

PostgreSQL :5432

Hydra DB

苦労ポイント

  • cors
    • 関係するコンポーネントが多くて細かいミスが増えるので関係図を作っておきましょう。
    • client登録時にcorsも設定するように
  • バックエンド側の検証時、IP制限かけたら検証に失敗するように
    • hydraのpublic key取得ミスによるエラー。terraform等修正
  • ゲームサーバー側にほとんど改修をかけずに作るのでAPIサーバー側でゲームサーバー向けのトークン保持等を行っている。

導線

以下は認可処理を開始してから最終的にAPIを叩けるようになるまでの導線となります。簡易的にコードなども記載。

1 認可リクエスト開始

エントリーポイント。ここで今回の認証のセッション生成等を行う


  hydraサーバー/oauth2/auth

  リクエスト:
  GET /oauth2/auth?
    client_id=test-game-client
    &respcode
    &scope=openid profile
    &redirect_uri=http://localhost:8080/callback
    &state=random_state

  渡すもの:
  - client_id: OAuth2クライアントID
  - response_type: code (Authorization Code Flow)
  - scope: openid profile
  - redirect_uri: 認証後のコールバックURL
  - state: CSRF対策用のランダム文字列

  検証:
  - Hydraがクライアント登録を確認
  - redirect_uriが登録されたURIと一致するか

  返却:
  - ログインプロバイダーへのリダイレクト(login_challenge付き)

画像の説明

2 Hydra → Backend (Login Provider)

バックエンドサーバーが「認証」(ウォレットの所持者であることの確認)を行うための処理を開始


  バックエンド/login?login_challenge=xxx

  検証するもの:
  // Hydraから login_challenge を取得
  loginChallenge := r.URL.Query().Get("login_challenge")

  // Hydra Admin APIでログインリクエストの詳細を取得
  loginReq, err := hydraClient.GetLoginRequest(loginChallenge)

  Hydra Admin APIに問い合わせ:
  GET Hydra Admin API/admin/oauth2/auth/requests/login?login_challenge=xxx

  返すもの:
  - 既にログイン済みの場合(loginReq.Skip == true): Hydraへログイン承認
  - 未ログインの場合: セッションを作成してフロントエンドにリダイレクト

3 Backend → Frontend (OAuth Login Page)

ユーザーにログイン処理を促す。


  URL: フロントエンド/oauth-login?sessiyyy

  渡すもの:
  // セッションを作成
  sess, err := sessionStore.Create(loginChallenge)

  // フロントエンドにリダイレクト
  redirectURL := fmt.Sprintf("%s/oauth-login?sessi%s", frontendURL, sess.ID)
  http.Redirect(w, r, redirectURL, http.StatusFound)

  セッション内容:
  - session_id: 一時的なセッションID
  - login_challenge: Hydraのログインチャレンジ(内部保持)

4 Frontend → Backend (Auth Init)



  URL: http://localhost:3000/api/auth/init

  リクエスト: frontend/src/components/OAuthLoginPage.tsx:45
  POST /api/auth/init
  Content-Type: application/json

  {
    "session_id": "yyy",
    "address": "0x1234...",
    "wallet_name": "..."
  }

  渡すもの:
  - session_id: セッションID
  - address: ウォレットアドレス
  - wallet_name: ウォレット名(MetaMask等)

  検証
  // セッションが有効か確認

5 Backend:ウォレット署名検証の準備



  リクエスト: backend/internal/mchplus/client.go:54
  GET /api/authorize?
    respcode
    &scope=openid profile
    &client_id=xxx
    &wallet_name=MetaMask
    &state=session_id
    &redirect_uri=http://localhost:3000/callback
    &address=0x1234...
    &lang=ja

  渡すもの:
  - client_id: MCH Plus クライアントID
  - address: ウォレットアドレス
  - wallet_name: ウォレット名
  - state: セッションID(検証用)
  - redirect_uri: ダミーのコールバックURI

  返すもの:
  {
    "message": "I am signing my one-time nonce: abc123..."
  }

  このメッセージをユーザーがウォレットで署名します。

6 Frontend: ウォレット署名



  処理: 
	
	frontend/src/components/OAuthLoginPage.tsx:64
  // wagmiとethers v5を使って署名
  const client = await getConnectorClient(config)
  const ethersProvider = new providers.Web3Provider(client)
  const signer = ethersProvider.getSigner()
  const signature = await signer.signMessage(message)

  署名対象:
  - 5から取得したメッセージ

  返すもの:
  - 署名(hex文字列)

認証画面

7 Frontend → Backend (Auth Verify)



  URL: バックエンド/api/auth/verify

  POST /api/auth/verify
  Content-Type: application/json

  {
    "session_id": "yyy",
    "address": "0x1234...",
    "signature": "0xabcd...",
    "network": "..."
  }

  渡す
  - session_id: セッションID
  - address: ウォレットアドレス
  - signature: ウォレットで生成した署名
  - network: ネットワーク名(home-verse, oasys-mainnet, mainnet)

  検証
  // セッション検証
  sess, err := sessionStore.Get(req.SessionID)

8Backend → Verify Signature



  wallet検証

  リクエスト
  POST /api/login
  Content-Type: application/json

  {
    "address": "0x1234...",
    "client_id": "xxx",
    "signature": "0xabcd...",
    "network": "home-verse",
    "redirect_uri": "http://backend:3000/callback",
    "lang": "ja",
    "state": "session_id"
  }

  検証
  - wallet検証サーバーが署名を検証
  - アドレスとメッセージの整合性を確認
  - 署名が本当にそのウォレットからのものか検証

 返却
  {
    "code": "auth_code_12345",
    "state": "session_id",
    "redirect_uri": "backend/callback"
  }

9 Backend → wallet検証サーバー (Token Exchange)



  POST /api/token
  Content-Type: application/x-www-form-urlencoded

  grant_type=authorization_code
  &code=auth_code_12345
  &redirect_uri=http://xxxx:xxxx/callback
  &client_id=xxx
  &client_secret=yyy

  渡すもの:
  - grant_type: authorization_code
  - code: MCH Plusから取得した認可コード
  - client_id: MCH Plus クライアントID
  - client_secret: MCH Plus クライアントシークレット(重要)

  検証するもの:
  - 認可コードが有効か
  - client_idとclient_secretが正しいか
  - redirect_uriが一致するか

  返すもの:
  {
    "access_token": "access_token_xxx",
    "token_type": "Bearer",
    "expires_in": 604800,
    "refresh_token": "mch_refresh_token_yyy",
    "id_token": "id_token_zzz"
  }

10 Backend: Token Mapping Store に保存

ここはβ版のため、仮の処理を行っていて、正式版ではゲームサーバー側にhydraの検証の責務を負ってもらう予定。



  // トークンマッピングストアに保存
  tokenMapStore.Set(req.Address, &tokenmap.AuthToken{
      AccessToken:  tokenResp.AccessToken,
      RefreshToken: tokenResp.RefreshToken,
      IDToken:      tokenResp.IDToken,
      ExpiresAt:    time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second),
  })

  保存内容: backend/internal/tokenmap/store.go:67
  - キー: ウォレットアドレス(0x1234...)
  - 値: Auth トークン(access_token, refresh_token, id_token, ExpiresAt)

  これにより、Hydra subject(ウォレットアドレス)↔ wallet検証 トークンのマッピングが確立されます。

11 Backend → Hydra (Accept Login)



  /admin/oauth2/auth/requests/login/accept

  PUT /admin/oauth2/auth/requests/login/accept?login_challenge=xxx

  {
    "subject": "0x1234...",
    "remember": true,
    "remember_for": 3600
  }

  渡すもの:
  - subject: ウォレットアドレス(ユーザーの一意識別子)
  - remember: ログイン状態を記憶するか
  - remember_for: 記憶する期間(秒)

  返すもの:
  {
    "redirect_to": "http://hydra/oauth2/auth?..."
  }

  このURLにリダイレクトすることで、Hydraの同意画面(Consent)に進みます。

  Frontend への返却
  {
    "redirect_to": "http://hydraサーバー/oauth2/auth?..."
  }

12: Hydra → Client (Authorization Code発行)

画像の説明



  URL: http://3rdパーティーURL/callback?code=hydra_auth_code_123&state=random_state

  渡すもの:
  - code: Hydra発行の認可コード
  - state: 元のリクエストで渡したstate(CSRF検証用)

  クライアント側の検証:
  - stateが一致するか確認

13 Client → Hydra (Access Token取得)



  URL: http://hydraサーバー/oauth2/token

  リクエスト:
  POST /oauth2/token
  Content-Type: application/x-www-form-urlencoded

  grant_type=authorization_code
  &code=hydra_auth_code_123
  &redirect_uri=http://3rdパーティーURL/callback
  &client_id=test-game-client
  &client_secret=test-secret

  渡すもの:
  - grant_type: authorization_code
  - code: Hydraの認可コード
  - redirect_uri: コールバックURI(一致必須)
  - client_id: クライアントID
  - client_secret: クライアントシークレット

  検証するもの:
  - 認可コードが有効か
  - クライアント認証情報が正しいか
  - redirect_uriが一致するか

  返すもの:
  {
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "...",
    "scope": "openid profile"
  }

これでクライアントはHydraのJWTトークンを取得しました。

まとめ

Ory HydraというヘッドレスのOauth2プロパイダーを使うことで迅速にAPIを提供する環境を構築することが出来ました。

ブレヒロでは今後、提供したAPIを利用したハッカソンの開催なども予定されており、是非ご参加をお願いします。

最新記事をフォロー

RSS Feed