Schreiben eines TokenProvider-Elements mit einer Azure-Funktion

Hinweis

Diese Vorschauversion wird ohne Vereinbarung zum Servicelevel bereitgestellt und ist nicht für Produktionsworkloads vorgesehen. Manche Features werden möglicherweise nicht unterstützt oder sind nur eingeschränkt verwendbar.

Im Fluid Framework sind TokenProvider für das Erstellen und Signieren von Token verantwortlich, die vom @fluidframework/azure-client verwendet werden, um Anforderungen an den Azure Fluid Relay-Dienst zu senden. Das Fluid Framework stellt einen einfachen, unsicheren TokenProvider für Entwicklungszwecke bereit, der passend als InsecureTokenProvider bezeichnet wird. Jeder Fluid-Dienst muss einen benutzerdefinierten TokenProvider basierend auf den Authentifizierungs- und Sicherheitsaspekten des jeweiligen Diensts implementieren.

Jeder erstellten Azure Fluid Relay-Ressource werden eine Mandanten-ID und ein eigener eindeutiger geheimer Mandantenschlüssel zugewiesen. Der geheime Schlüssel ist ein freigegebener geheimer Schlüssel. Ihre App/Ihr Dienst und der Azure Fluid Relay-Dienst kennen ihn. Tokenanbieter müssen den geheimen Schlüssel kennen, um Anforderungen zu signieren, aber der geheime Schlüssel darf nicht im Clientcode enthalten sein.

Implementieren einer Azure-Funktion zum Signieren von Token

Eine Möglichkeit zum Erstellen eines sicheren Tokenanbieters besteht darin, einen HTTPS-Endpunkt sowie eine TokenProvider-Implementierung zu erstellen, die authentifizierte HTTPS-Anforderungen zum Abrufen von Token an diesen Endpunkt sendet. Dieser Pfad ermöglicht es Ihnen, den geheimen Mandantenschlüssel an einem sicheren Speicherort zu speichern – beispielsweise in Azure Key Vault.

Die vollständige Lösung umfasst zwei Komponenten:

  1. Einen HTTPS-Endpunkt, der Anforderungen akzeptiert und Azure Fluid Relay-Token zurückgibt
  2. Eine ITokenProvider-Implementierung, die eine URL eines Endpunkts akzeptiert und dann Anforderungen zum Abrufen von Token an diesen Endpunkt sendet

Erstellen eines Endpunkts für Ihre TokenProvider-Instanz mithilfe von Azure Functions

Mit Azure Functions können Sie schnell einen solchen HTTPS-Endpunkt erstellen.

In diesem Beispiel wird veranschaulicht, wie Sie eine eigene HTTPTrigger-Azure-Funktion erstellen, die das Token abruft, indem Sie Ihren Mandantenschlüssel übergeben.

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;

Die Funktion generateToken aus dem Paket @fluidframework/azure-service-utils generiert ein Token für den angegebenen Benutzer, das mit dem geheimen Schlüssel des Mandanten signiert wird. Mit dieser Methode kann das Token an den Client zurückgegeben werden, ohne das Geheimnis preiszugeben. Stattdessen wird das Token serverseitig unter Verwendung des Geheimnisses generiert, um bereichsbezogenen Zugriff auf das angegebene Dokument zu gewähren. Im ITokenProvider-Beispiel weiter unten werden HTTP-Anforderungen an diese Azure-Funktion gesendet, um die Token abzurufen.

Bereitstellen der Azure-Funktions-App

Azure Functions kann auf verschiedene Arten bereitgestellt werden. Weitere Informationen zum Bereitstellen von Azure Functions finden Sie in der Azure Functions-Dokumentation im Abschnitt Bereitstellen.

Implementieren des Tokenanbieters (TokenProvider)

Tokenanbieter können auf verschiedene Arten implementiert werden. Sie müssen jedoch zwei separate API-Aufrufe implementieren: fetchOrdererToken und fetchStorageToken. Diese APIs sind für das Abrufen von Token für den Fluid-Orderer und die Speicherdienste zuständig. Beide Funktionen geben TokenResponse-Objekte zurück, die den Tokenwert darstellen. Die Fluid Framework-Runtime ruft diese beiden APIs nach Bedarf auf, um Token abzurufen. Beachten Sie, dass, obwohl Ihr Anwendungscode nur einen Dienstendpunkt verwendet, um die Verbindung mit dem Azure Fluid Relay-Dienst herzustellen, der Azure-Client intern in Verbindung mit dem Dienst diesen einen Endpunkt in ein Orderer- und Speicherendpunktpaar übersetzt. Diese beiden Endpunkte werden von diesem Zeitpunkt an für die Sitzung verwendet. Deshalb müssen Sie zwei separate Funktionen für das Abrufen von Token implementieren, jeweils eine für jedes Token.

Um sicherzustellen, dass der geheime Mandantenschlüssel sicher aufbewahrt wird, wird er an einem sicheren Back-End-Speicherort gespeichert und ist nur innerhalb der Azure-Funktion zugänglich. Zum Abrufen von Token müssen Sie eine Anforderung vom Typ GET oder POST an Ihre bereitgestellte Azure-Funktion senden und die Mandanten-ID (tenantID), die Dokument-ID (documentId) und die Benutzer-ID/den Benutzernamen (userID/userName) angeben. Die Azure-Funktion ist für die Zuordnung zwischen der Mandanten-ID und einem Mandantenschlüsselgeheimnis zuständig, um das Token entsprechend zu generieren und zu signieren.

Die folgende Beispielimplementierung behandelt die Durchführung dieser Anforderungen an Ihre Azure-Funktion. Sie verwendet die axios-Bibliothek, um HTTP-Anforderungen zu stellen. Sie können andere Bibliotheken oder Ansätze verwenden, um eine HTTP-Anforderung über Servercode durchzuführen. Diese spezifische Implementierung wird auch als Export aus dem @fluidframework/azure-client-Paket für Sie bereitgestellt.

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;
    }
}

Effizienz und Fehlerbehandlung hinzufügen

Die AzureFunctionTokenProvider ist eine einfache Implementierung von TokenProvider, die bei der Implementierung Ihres eigenen benutzerdefinierten Tokenanbieters als Ausgangspunkt behandelt werden sollte. Für die Implementierung eines produktionsbereiten Tokenanbieters sollten Sie verschiedene Fehlerszenarien berücksichtigen, die der Tokenanbieter verarbeiten muss. Die Implementierung kann z. B. keine Netzwerktrennsituationen verarbeiten, AzureFunctionTokenProvider da es das Token nicht clientseitig zwischenspeichert.

Wenn der Container die Verbindung trennt, versucht der Verbindungs-Manager, ein neues Token aus dem TokenProvider abzurufen, bevor die Verbindung mit dem Container erneut hergestellt wird. Während die Verbindung mit dem Netzwerk getrennt ist, schlägt die API GET-Anforderung von fetchOrdererToken fehl und löst einen Fehler aus, welcher besagt, dass es nicht erneut versucht werden kann. Dies führt wiederum dazu, dass der Container verworfen wird und nicht erneut verbunden werden kann, auch wenn eine Netzwerkverbindung erneut hergestellt wird.

Eine mögliche Lösung für dieses Trennungsproblem besteht darin, gültige Token in Window.localStorage zwischenzuspeichern. Beim Zwischenspeichern von Token ruft der Container ein gültiges gespeichertes Token ab, anstatt eine API-Get-Anforderung zu erstellen, während das Netzwerk getrennt ist. Beachten Sie, dass ein lokal gespeichertes Token nach einem bestimmten Zeitraum ablaufen könnte und Sie dennoch eine API-Anforderung zum Abrufen eines neuen gültigen Tokens stellen müssen. In diesem Fall wäre eine zusätzliche Fehlerbehandlungs- und Wiederholungslogik erforderlich, um zu verhindern, dass der Container nach einem einzelnen fehlgeschlagenen Versuch verworfen wird.

Wie Sie sich für die Implementierung dieser Verbesserungen entscheiden, liegt ganz bei Ihnen und den Anforderungen Ihrer Anwendung. Beachten Sie, dass Sie mit der localStorage-Tokenlösung auch Leistungsverbesserungen in Ihrer Anwendung sehen werden, da Sie eine Netzwerkanforderung bei jedem getContainer-Aufruf entfernen.

Das Caching von Tokens mit etwas ähnlichem wie localStorage kann zu Sicherheitsauswirkungen führen, und es liegt an Ihrem Ermessen, wenn Sie entscheiden, welche Lösung für Ihre Anwendung geeignet ist. Unabhängig davon, ob Sie das Caching von Tokens implementieren, sollten Sie Fehlerbehandlungs- und Wiederholungslogik in fetchOrdererToken und fetchStorageToken hinzufügen, damit der Container nicht nach einem einzelnen fehlgeschlagenen Aufruf verworfen wird. Erwägen Sie beispielsweise das Umschließen des Aufrufs von getToken in einem try-Block mit einem catch-Block, der es erneut versucht und einen Fehler nur nach einer bestimmten Anzahl von Wiederholungen wirft.

Siehe auch