方法: Azure 関数を使用して TokenProvider を記述する

Note

このプレビュー バージョンはサービス レベル アグリーメントなしで提供されています。運用環境のワークロードに使用することはお勧めできません。 特定の機能はサポート対象ではなく、機能が制限されることがあります。

Fluid Framework で、トークン プロバイダーは、@fluidframework/azure-client で Azure Fluid Relay サービスへの要求を行う際に使用されるトークンの作成と署名を担当します。 Fluid Framework には、InsecureTokenProvider という名前の単純で安全でない開発目的の TokenProvider が用意されています。 特定のサービスの認証とセキュリティに関する考慮事項に基づいたカスタム TokenProvider が各 Fluid サービスで実装される必要があります。

作成する各 Azure Fluid Relay リソースには、テナント ID と独自の一意のテナントの秘密鍵が割り当てられます。 シークレット キーは共有シークレットです。 アプリまたはサービスでそれが認識されてから、Azure Fluid Relay サービスでそれが認識されます。 要求に署名するためのシークレット キーが TokenProviders で認識されている必要がありますが、シークレット キーをクライアント コードに含めることはできません。

Azure 関数を実装してトークンに署名する

セキュリティで保護されたトークン プロバイダーを構築するための 1 つのオプションは、HTTPS エンドポイントを作成し、そのエンドポイントに対して認証された HTTPS 要求を送信してトークンを取得する TokenProvider 実装を作成することです。 このパスにより、Azure Key Vault などの安全な場所に "テナントの秘密鍵" を保存できます。

完全なソリューションには、次の2つの要素があります。

  1. 要求を受け入れ、Azure Fluid Relay トークンを返す HTTPS エンドポイント。
  2. エンドポイントへの URL を受け取り、そのエンドポイントへの要求を作成してトークンを取得する ITokenProvider 実装。

Azure Functions を使用して、TokenProvider のエンドポイントを作成する

Azure Functions の使用は、このような HTTPS エンドポイントをすばやく作成する方法です。

この例では、テナント キーを渡すことによってトークンをフェッチする独自の HTTPTrigger Azure 関数を作成する方法を示します。

import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { ScopeType } from "@fluidframework/azure-client";
import { generateToken } from "@fluidframework/azure-service-utils";

// NOTE: retrieve the key from a secure location.
const key = "myTenantKey";

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    // tenantId, documentId, userId and userName are required parameters
    const tenantId = (req.query.tenantId || (req.body && req.body.tenantId)) as string;
    const documentId = (req.query.documentId || (req.body && req.body.documentId)) as string | undefined;
    const userId = (req.query.userId || (req.body && req.body.userId)) as string;
    const userName = (req.query.userName || (req.body && req.body.userName)) as string;
    const scopes = (req.query.scopes || (req.body && req.body.scopes)) as ScopeType[];

    if (!tenantId) {
        context.res = {
            status: 400,
            body: "No tenantId provided in query params",
        };
        return;
    }

    if (!key) {
        context.res = {
            status: 404,
            body: `No key found for the provided tenantId: ${tenantId}`,
        };
        return;
    }

    let user = { name: userName, id: userId };

    // Will generate the token and returned by an ITokenProvider implementation to use with the AzureClient.
    const token = generateToken(
        tenantId,
        documentId,
        key,
        scopes ?? [ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
        user
    );

    context.res = {
        status: 200,
        body: token
    };
};

export default httpTrigger;

@fluidframework/azure-service-utils に含まれる generateToken 関数は、テナントの秘密鍵を使用して署名された特定のユーザーのトークンを生成します。 この方法により、秘密を公開せずにトークンをクライアントに返すことができます。 代わりに、シークレットを使用してトークンがサーバー側で生成され、指定されたドキュメントへのスコープ指定されたアクセスが提供されます。 下に示す ITokenProvider の例では、トークンを取得するために、この Azure 関数に対する HTTP 要求を作成しています。

Azure 関数のデプロイ

Azure Functions はいくつかの方法で展開できます。 Azure Functions の展開の詳細については、Azure Functions ドキュメントの「デプロイ」セクションを参照してください。

TokenProvider を実装する

TokenProviders はさまざまな方法で実装できますが、fetchOrdererTokenfetchStorageToken という 2 つの個別の API 呼び出しを実装する必要があります。 これらの API は、Fluid Orderer とストレージの各サービスのトークンをそれぞれフェッチする役割を担います。 どちらの関数からも、トークン値を表す TokenResponseオブジェクトが返されます。 Fluid Framework ランタイムは、必要に応じてこれら 2 つの API を呼び出してトークンを取得します。 アプリケーション コードでは Azure Fluid Relay サービスとの接続を確立するために 1 つのサービス エンドポイントのみを使用していますが、azure-client は内部的にそのサービスと連動して、その 1 つのエンドポイントを Orderer とストレージのエンドポイントのペアに変換します。 これらの 2 つのエンドポイントは、そのセッションに対してその時点から使用されるため、トークンをフェッチするために、2 つの個別の関数 (それぞれに 1 つずつ) を実装する必要があります。

テナント シークレット キーは、安全な状態を確保するために、セキュリティで保護されたバックエンドの場所に格納され、Azure 関数内からのみアクセスできます。 トークンを取得するには、tenantIDdocumentId、および userID/userName を指定して、デプロイされた Azure 関数に GET または POST 要求を行う必要があります。 Azure 関数でテナント ID とテナント キー シークレットとの間のマッピングを行い、トークンを適切に生成して署名します。

次の実装例は、Azure 関数に対するこれらの要求の実行を処理します。 ここでは、HTTP 要求を行うために axios ライブラリが使用されます。 他のライブラリまたは方法を使用して サーバー コードから HTTP 要求を行うことができます。 この特定の実装は、@fluidframework/azure-client パッケージからのエクスポートとしても提供されます。

import { ITokenProvider, ITokenResponse } from "@fluidframework/routerlicious-driver";
import axios from "axios";
import { AzureMember } from "./interfaces";

/**
 * Token Provider implementation for connecting to an Azure Function endpoint for
 * Azure Fluid Relay token resolution.
 */
export class AzureFunctionTokenProvider implements ITokenProvider {
    /**
     * Creates a new instance using configuration parameters.
     * @param azFunctionUrl - URL to Azure Function endpoint
     * @param user - User object
     */
    constructor(
        private readonly azFunctionUrl: string,
        private readonly user?: Pick<AzureMember, "userId" | "userName" | "additionalDetails">,
    ) { }

    public async fetchOrdererToken(tenantId: string, documentId?: string): Promise<ITokenResponse> {
        return {
            jwt: await this.getToken(tenantId, documentId),
        };
    }

    public async fetchStorageToken(tenantId: string, documentId: string): Promise<ITokenResponse> {
        return {
            jwt: await this.getToken(tenantId, documentId),
        };
    }

    private async getToken(tenantId: string, documentId: string | undefined): Promise<string> {
        const response = await axios.get(this.azFunctionUrl, {
            params: {
                tenantId,
                documentId,
                userId: this.user?.userId,
                userName: this.user?.userName,
                additionalDetails: this.user?.additionalDetails,
            },
        });
        return response.data as string;
    }
}

効率とエラー処理を追加する

AzureFunctionTokenProviderTokenProvider の単純な実装であり、独自のカスタム トークン プロバイダーを実装する際の開始点として扱う必要があります。 運用環境に対応したトークン プロバイダーを実装するには、トークン プロバイダーが処理する必要があるさまざまな障害シナリオを考慮する必要があります。 たとえば、AzureFunctionTokenProvider 実装は、クライアント側でトークンをキャッシュしないため、ネットワーク切断の状況を処理できません。

コンテナーが切断されると、接続マネージャーは、コンテナーに再接続する前に TokenProvider から新しいトークンを取得しようとします。 ネットワークが切断されている間、fetchOrdererToken で行われた API GET 要求は失敗し、再試行できないエラーがスローされます。 これにより、コンテナーが破棄され、ネットワーク接続が再確立された場合でも再接続できなくなります。

この切断の問題に対する潜在的な解決策は、有効なトークンを Window.localStorage にキャッシュすることです。 トークン キャッシュを使用すると、そのコンテナーは、ネットワークが切断されている間に API GET 要求を行う代わりに、保存されている有効なトークンを取得します。 ローカルに保存されたトークンは一定期間後に期限切れになる可能性があり、新しい有効なトークンを取得するには API 要求を行う必要があることに注意してください。 この場合、1 回試行が失敗した後にコンテナーが破棄されないようにするために、追加のエラー処理と再試行ロジックが必要になります。

これらの機能強化を実装する方法は、ユーザーとそのアプリケーションの要件に完全に依存します。 localStorage トークン ソリューションを使用すると、getContainer 呼び出しごとにネットワーク要求を削除するため、アプリケーションのパフォーマンスも向上することに注意してください。

localStorage などを使用したトークン キャッシュにはセキュリティへの影響が伴う可能性があるため、アプリケーションに適したソリューションを決定するのはユーザーの裁量に任されています。 トークン キャッシュを実装するかどうかにかかわらず、呼び出しが 1 回失敗した後にコンテナーが破棄されないように、fetchOrdererTokenfetchStorageToken にエラー処理と再試行ロジックを追加する必要があります。 たとえば、指定された回数の再試行後にのみ再試行してエラーをスローする catch ブロックを使用して、getToken の呼び出しを try ブロックでラップすることを検討してください。

関連項目