Procedure: Een TokenProvider schrijven met een Azure-functie

Notitie

Deze preview-versie wordt aangeboden zonder Service Level Agreement en wordt niet aanbevolen voor productieworkloads. Misschien worden bepaalde functies niet ondersteund of zijn de mogelijkheden ervan beperkt.

In de Vloeiend Framework zijn TokenProviders verantwoordelijk voor het maken en ondertekenen van tokens die worden gebruikt voor het @fluidframework/azure-client indienen van aanvragen bij de Azure Fluid Relay-service. De Vloeiend Framework biedt een eenvoudige, onveilige TokenProvider voor ontwikkelingsdoeleinden, met de naam InsecureTokenProvider. Elke Fluid-service moet een aangepaste TokenProvider implementeren op basis van de verificatie- en beveiligingsoverwegingen van de specifieke service.

Aan elke Azure Fluid Relay-resource die u maakt, wordt een tenant-id en een eigen unieke tenantgeheimsleutel toegewezen. De geheime sleutel is een gedeeld geheim. Uw app/service kent deze en de Azure Fluid Relay-service kent deze. TokenProviders moet de geheime sleutel kennen om aanvragen te ondertekenen, maar de geheime sleutel kan niet worden opgenomen in de clientcode.

Een Azure-functie implementeren om tokens te ondertekenen

Een optie voor het bouwen van een beveiligde tokenprovider is het maken van een HTTPS-eindpunt en het maken van een TokenProvider-implementatie waarmee geverifieerde HTTPS-aanvragen naar dat eindpunt worden uitgevoerd om tokens op te halen. Met dit pad kunt u de geheime sleutel van de tenant opslaan op een veilige locatie, zoals Azure Key Vault.

De volledige oplossing heeft twee onderdelen:

  1. Een HTTPS-eindpunt dat aanvragen accepteert en Azure Fluid Relay-tokens retourneert.
  2. Een ITokenProvider-implementatie die een URL naar een eindpunt accepteert en vervolgens aanvragen naar dat eindpunt verzendt om tokens op te halen.

Een eindpunt voor uw TokenProvider maken met behulp van Azure Functions

Het gebruik van Azure Functions is een snelle manier om een dergelijk HTTPS-eindpunt te maken.

In dit voorbeeld ziet u hoe u uw eigen HTTPTrigger Azure-functie maakt waarmee het token wordt opgehaald door uw tenantsleutel door te geven.

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;

De generateToken functie, gevonden in het @fluidframework/azure-service-utils pakket, genereert een token voor de opgegeven gebruiker die is ondertekend met behulp van de geheime sleutel van de tenant. Met deze methode kan het token worden geretourneerd naar de client zonder het geheim weer te geven. In plaats daarvan wordt het token gegenereerd aan de serverzijde met behulp van het geheim om scoped toegang tot het opgegeven document te bieden. In het onderstaande voorbeeld van ITokenProvider worden HTTP-aanvragen naar deze Azure-functie verzonden om de tokens op te halen.

De Azure-functie implementeren

Azure Functions kan op verschillende manieren worden geïmplementeerd. Zie de sectie Implementeren van de Documentatie voor Azure Functions voor meer informatie over het implementeren van Azure Functions.

TokenProvider implementeren

TokenProviders kan op veel manieren worden geïmplementeerd, maar moet twee afzonderlijke API-aanroepen implementeren: fetchOrdererToken en fetchStorageToken. Deze API's zijn respectievelijk verantwoordelijk voor het ophalen van tokens voor de Fluid orderer en opslagservices. Beide functies retourneren TokenResponse objecten die de tokenwaarde vertegenwoordigen. De Vloeiend Framework runtime roept deze twee API's aan, indien nodig om tokens op te halen. Hoewel uw toepassingscode slechts één service-eindpunt gebruikt om verbinding te maken met de Azure Fluid Relay-service, vertaalt de Azure-client intern in combinatie met de service dat ene eindpunt naar een orderer en opslageindpuntpaar. Deze twee eindpunten worden vanaf dat punt gebruikt voor die sessie. Daarom moet u de twee afzonderlijke functies implementeren voor het ophalen van tokens, één voor elk.

Om ervoor te zorgen dat de tenantgeheimsleutel veilig blijft, wordt deze opgeslagen op een beveiligde back-endlocatie en is deze alleen toegankelijk vanuit de Azure-functie. Als u tokens wilt ophalen, moet u een GET of POST aanvraag indienen bij uw geïmplementeerde Azure-functie, waarbij u de tenantID en documentId, en userID/userName. De Azure-functie is verantwoordelijk voor de toewijzing tussen de tenant-id en een tenantsleutelgeheim om het token op de juiste wijze te genereren en te ondertekenen.

In de onderstaande voorbeeld-implementatie worden deze aanvragen naar uw Azure-functie verwerkt. De axios-bibliotheek wordt gebruikt om HTTP-aanvragen te maken. U kunt andere bibliotheken of benaderingen gebruiken om een HTTP-aanvraag uit servercode te maken. Deze specifieke implementatie is ook beschikbaar als export vanuit het @fluidframework/azure-client pakket.

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

Efficiëntie en foutafhandeling toevoegen

Dit AzureFunctionTokenProvider is een eenvoudige implementatie die als uitgangspunt moet worden beschouwd bij het implementeren van TokenProvider uw eigen aangepaste tokenprovider. Voor de implementatie van een tokenprovider die gereed is voor productie, moet u rekening houden met verschillende foutscenario's die de tokenprovider moet verwerken. De implementatie kan bijvoorbeeld niet omgaan met situaties waarin de AzureFunctionTokenProvider netwerkverbinding is verbroken, omdat het token niet in de cache op de client wordt opgeslagen.

Wanneer de verbinding met de container wordt verbroken, probeert de verbindingsbeheerder een nieuw token op te halen van de TokenProvider voordat de verbinding met de container opnieuw wordt gemaakt. Hoewel de verbinding met het netwerk is verbroken, mislukt de API-aanvraag die is fetchOrdererToken ingediend en treedt er een fout op die niet opnieuw kan worden geprobeerd. Dit leidt er op zijn beurt toe dat de container wordt verwijderd en niet opnieuw verbinding kan maken, zelfs niet als er een netwerkverbinding opnieuw tot stand is gebracht.

Een mogelijke oplossing voor dit probleem met de verbinding verbreken is het opslaan van geldige tokens in Window.localStorage. Met tokencaching haalt de container een geldig opgeslagen token op in plaats van een API-aanvraag te doen terwijl de verbinding met het netwerk is verbroken. Houd er rekening mee dat een lokaal opgeslagen token na een bepaalde periode kan verlopen en u nog steeds een API-aanvraag moet indienen om een nieuw geldig token op te halen. In dit geval is er extra foutafhandeling en logica voor opnieuw proberen vereist om te voorkomen dat de container wordt verwijderd na één mislukte poging.

Hoe u ervoor kiest om deze verbeteringen te implementeren, is volledig aan u en de vereisten van uw toepassing. Houd er rekening mee dat u met de localStorage tokenoplossing ook prestatieverbeteringen in uw toepassing ziet, omdat u een netwerkaanvraag voor elke getContainer aanroep verwijdert.

Tokencaching met iets zoals localStorage kan gevolgen hebben voor de beveiliging en het is aan uw discretie bij het bepalen welke oplossing geschikt is voor uw toepassing. Ongeacht of u tokencaching implementeert, moet u foutafhandeling en logica voor opnieuw proberen fetchOrdererToken toevoegen, zodat fetchStorageToken de container niet wordt verwijderd na één mislukte aanroep. Denk bijvoorbeeld aan het verpakken van de aanroep van getToken een try blok met een blok dat opnieuw catch probeert en een fout genereert pas na een opgegeven aantal nieuwe pogingen.

Zie ook