Game Server Services(GS2)のTypeScript SDKをWebフロントエンドでできるだけ安全に使う

Game Server Services(GS2)のTypeScript SDKをWebフロントエンドでできるだけ安全に使う

Clock Icon2024.10.06

こんにちは、ゲームソリューション部の入井です。

Game Server Services(以下、GS2)ではTypeScript用のSDKが用意されており、Webフロントエンド開発に取り入れることで、GS2が提供する各種ゲームサーバーサイドの機能にWebサイトからアクセスできるようになります。

これにより、Webサイトに景品付きくじ引きやポイント交換のような遊び要素を実装したり、教育系Webサービスにミッションとその報酬のようなゲーミフィケーション的要素を取り入れたりといったことが比較的簡単に実現できるようになります。

ただ、他のゲームクライアント等での開発時と同じようにSDKを利用しようとすると、Webフロントエンドという環境特有の事情から、セキュリティ的に問題が発生しやすくなります。

今回の記事では、できるだけ安全にWebフロントエンドでGS2 SDKを使用する方法について書いていきます。

環境

Webフロントエンド向けにGS2のTypeScript SDKを使用する上での注意点と解決策

注意点

GS2 SDKでは、最初に専用のClientオブジェクトを初期化する必要があります。
初期化の際は、GS2から各ユーザー向けに発行されるClientIdやClient Secretといったクレデンシャル情報を渡します。これにより、Clientオブジェクトは初期化処理時にクレデンシャル情報を使ってGS2の専用APIから認証用トークンを取得し、以後Clientから各マイクロサービスにこのトークンを使用してアクセスできるようになる、という仕組みです。

公式ドキュメントのTypeScript初期化コード例

const session = new Gs2Core.Gs2RestSession(
        "ap-northeast-1",
        new BasicGs2Credential(
            "your client id",
            "your client secret"
        )
    );
    await session.connect();

この認証トークン取得のためのAPIアクセスの内容は、ブラウザから実行する関係上ある程度の知識を持つユーザーからは簡単に見ることができます。つまり、どのようなClientIdやClientSecretを使っているかを知られてしまいます

GS2では発行するClientId毎に細かくセキュリティポリシーを設定できるので、一般ユーザー用の最低限の権限ポリシーを設定していれば、ClientIdやClientSecretを知られていても、いきなり管理者権限を乗っ取られることはありません。また、各ユーザーのデータにアクセスするには、APIのクレデンシャル情報だけでなく、別途ユーザー毎の認証も済ませておく必要があるため、他のユーザーのデータを見られたり、書き換えられたりする可能性も低いでしょう。

それでも、特に有効期限設定のないクレデンシャル情報が露出したままでは、管理側の想定していない形でのAPIアクセスを受ける危険性が高いので隠す必要があります。

解決策

今回はトークン発行処理を専用のLambda関数で実行する仕組みを作ってみました。ClientSecretはLambdaの環境変数としてAWS上に保管しておくので、ブラウザ側から値を確認することはできません。

Webフロントエンドは最初にこのLambda関数をコールしてトークンを受取後、そのトークンを使ってSDK経由でGS2とやりとりをする形です。

実装

実際に私の環境でこの方法を試した際のコードを書いていきます。

トークン発行用Lambda関数(Python)

Webフロントエンド側に合わせてこちらもNode.jsにするという考えもありましたが、Typescript対応に手間がかかるため今回はPythonで書きました。

API Gateway等は経由せず、関数URL機能を使ってAPIとして公開しました。
clientSecretはLambdaの環境変数に登録したものを使っていますが、実運用時はSecrets Manager等を使った方が良いでしょう。

import json
import os
from gs2 import core

def lambda_handler(event, context):
    body = json.loads(event['body'])

    client_id = body.get('clientId')
    region = body.get('region')

    session = core.Gs2RestSession(
        core.BasicGs2Credential(
            client_id,
            os.environ['clientSecret']
        ),
        region
    )
    session.connect()

    response_data = {
        'projectToken': session.project_token
    }

    return {
        'statusCode': 200,
        'body': json.dumps(response_data)
    }

ClientIdとリージョン情報を元にトークンを発行するだけのシンプルな内容です。
今回は検証用のため機能を最小限にしていますが、実運用時は必要に応じて機能を盛り込んだ方が良いでしょう。

例えば、GS2は各ユーザーのアカウント作成もSDK経由で行うフローになっていますが、制限無くアカウントを作成できる権限が入ったトークンをブラウザ側へ返すのはあまり良くないので、新規ユーザーの場合のアカウント作成もこのLambda上で行っておく、という実装等が考えられます。

Webフロントエンド

Webフロントエンド側では、以下のコードのようにLambda関数からトークンを取得後、そのトークンを使ってGS2クライアントを初期化し、最後に認証が通ったことの確認としてGS2-AccountのNamespace情報をブラウザに表示しています。

import React, { useEffect, useState } from "react";
import "./App.css";
import axios, { AxiosRequestConfig } from "axios";
import { Gs2AccountRestClient } from "gs2/account";
import { DescribeNamespacesRequest } from "gs2/account/request";
import { Gs2RestSession, ProjectTokenGs2Credential } from "gs2/core/model";

function App() {
  const [namespaces, setNamespaces] = useState<(string | null)[]>([]);

  useEffect(() => {
    const initClient = async () => {
      const clientId = process.env.REACT_APP_GS2_CLIENT_ID as string;
      const region = "ap-northeast-1";

      const config: AxiosRequestConfig = {
        url: process.env.REACT_APP_GET_GS2_TOKEN_URL,
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        data: { clientId, region },
      };
      const result = await axios.request(config);
      const session = new Gs2RestSession(
        new ProjectTokenGs2Credential(clientId, result.data.projectToken),
        region,
      );
      await session.connect();

      const gs2Account = new Gs2AccountRestClient(session);
      const namespacesResult = await gs2Account.describeNamespaces(
        new DescribeNamespacesRequest().withLimit(10),
      );

      const fetchedNamespaces = namespacesResult.getItems();
      if (fetchedNamespaces) {
        setNamespaces(
          fetchedNamespaces.map((namespace) => namespace.getName()),
        );
      }
    };
    initClient().catch((err) => console.log(err));
  }, []);

  return (
    <div>
      <h1>Namespaces</h1>
      <ul>
        {namespaces.map((namespace, index) => (
          <li key={index}>{namespace}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Gs2RestSessionの生成時に渡しているProjectTokenGs2Credentialは、その名の通りトークンをクレデンシャル情報として扱うオブジェクトです。チュートリアル等で使われているBasicGs2Credentialは、クライアント初期化時にトークン発行のためのAPIリクエストが実行されますが、ProjectTokenGs2Credentialを使った場合そのAPIリクエストはスキップされ引数に渡したトークンがそのままクライアントで使われるようになっています。

実行結果

以下の画像のようにブラウザ上にGS2-AccountのNamespace名がリスト表示されました。

スクリーンショット 2024-09-30 221135

GS2のマネジメントコンソールと同じ内容が表示されているため、ClientSecretをブラウザ側に見せずにAPIリクエストに必要なトークンを発行できたことが確認できました。

スクリーンショット 2024-09-30 215820

今回の方法の注意点

今回はトークン発行部分の処理のみクラウド上で行う仕組みを作りましたが、これではClientIDやトークンの値はまだブラウザから確認できる状態です。更に安全性を高めるためにClientIdやトークンも秘匿したいのであれば、WebフロントエンドとGS2の間にProxyサーバーを置き、APIリクエスト時は常にProxyを通る構成にする必要があるでしょう。

ただ、その場合はGS2の『ゲームサーバーの構築や運用をしなくても良い』という利点が少し薄まってしまいます。

どういった構成を作るかについては、プロジェクトの内容や状況等から考える必要があると思われます。

まとめ

この記事では以下のような内容について書きました。

  • Webフロントエンドでできるだけ安全にGS2 SDKを使用するため、Lambda関数でトークン発行を行う方法を紹介しました。
  • これにより、ClientSecretの露出を防ぎつつ、GS2の機能をWebフロントエンドから使用することができます。
  • この方法ではClientIdやトークンは依然ブラウザから確認可能なため、プロジェクトの要件に応じて追加のセキュリティ対策を検討する必要があります。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.