Guide pratique : Écrire un TokenProvider avec une fonction Azure

Remarque

Cette préversion est fournie sans contrat de niveau de service et n’est pas recommandée pour les charges de travail de production. Certaines fonctionnalités peuvent être limitées ou non prises en charge.

Dans l’infrastructure Fluid, les fournisseurs de jetons sont chargés de créer et signer des jetons que @fluidframework/azure-client utilise pour adresser des demandes au service Relais Azure Fluid. L’infrastructure Fluid inclut un fournisseur de jetons simple non sécurisé à des fins de développement, judicieusement nommé InsecureTokenProvider. Chaque service Fluid doit implémenter un fournisseur de jetons personnalisé en fonction des considérations relatives à l’authentification et à la sécurité du service spécifique.

Chaque ressource Relais Azure Fluid que vous créez se voit attribuer un ID de locataire et sa propre clé secrète de locataire unique. La clé secrète est un secret partagé. Votre application/service la connaît, et le service Relais Azure Fluid la connaît également. TokenProviders doivent connaître la clé secrète pour signer des demandes, mais cette clé ne peut pas être incluse dans le code client.

Implémentation d’une fonction Azure pour signer des jetons

L’une des options possibles pour générer un fournisseur de jetons sécurisé consiste à créer un point de terminaison HTTPS et une implémentation de TokenProvider qui adresse des demandes HTTPS authentifiées à ce point de terminaison pour récupérer des jetons. Cela vous permet de stocker la clé secrète du locataire dans un emplacement sécurisé, comme Azure Key Vault.

La solution complète se compose de deux éléments :

  1. Un point de terminaison HTTPS qui accepte les demandes et retourne des jetons Relais Azure Fluid
  2. Une implémentation de ITokenProvider qui accepte l’URL d’un point de terminaison, puis adresse des demandes à ce point de terminaison pour récupérer des jetons

Création d’un point de terminaison pour un TokenProvider avec Azure Functions

L’utilisation d’Azure Functions représente un moyen rapide de créer un tel point de terminaison HTTPS.

Cet exemple montre comment créer votre propre fonction Azure HTTPTrigger qui récupère le jeton en passant votre clé de locataire.

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;

La fonction generateToken, qui se trouve dans le package @fluidframework/azure-service-utils, génère un jeton pour l’utilisateur connecté avec la clé secrète du locataire. Cette méthode permet de retourner le jeton au client sans exposer le secret. Le jeton est en effet généré côté serveur en utilisant le secret pour fournir un accès délimité au document en question. L’exemple d’ITokenProvider suivant adresse des requêtes HTTP à cette fonction Azure pour récupérer les jetons.

Déployer la fonction Azure

Les fonctions Azure Functions peuvent être déployées de plusieurs façons. Pour plus d’informations sur le déploiement de fonctions Azure Functions, consultez la section Déploiement de la documentation Azure Functions.

Implémentation du TokenProvider

Les TokenProvider peuvent être implémentés de plusieurs façons. Ils doivent cependant mettre en œuvre deux appels d’API distincts : fetchOrdererToken et fetchStorageToken. Ces API sont respectivement chargées d’extraire les jetons pour les services d’ordonnanceur et de stockage Fluid. Les deux fonctions retournent des TokenResponse objets représentant la valeur du jeton. Le runtime de l’Infrastructure Fluid appelle ces deux API en fonction des besoins pour récupérer les jetons. Notez que lorsque votre code d’application utilise un seul point de terminaison de service pour établir la connectivité avec le service Relais Azure Fluid, le client Azure en interne conjointement avec le service traduit ce point de terminaison en paire de points de terminaison de nœud ordonnanceur et de point de terminaison de stockage. Ces deux points de terminaison sont utilisés à partir de ce point pour cette session, c’est pourquoi vous devez implémenter les deux fonctions distinctes pour récupérer des jetons, une pour chacune.

Pour garantir la sécurité de la clé secrète du locataire, celle-ci est stockée dans un emplacement principal sécurisé et n’est accessible qu’à partir de la fonction Azure. Pour récupérer des jetons, vous devez adresser une demande GET ou POST à la fonction Azure déployée, en fournissant les valeurs tenantID, documentId et userID/userName. La fonction Azure assure le mappage entre l’ID de locataire et la clé secrète d’un locataire pour générer et signer le jeton de manière appropriée.

L’exemple d’implémentation ci-dessous gère l’exécution de ces requêtes à votre fonction Azure. Il utilise la bibliothèque d’axios pour effectuer des requêtes HTTP. Vous pouvez opter pour d’autres bibliothèques ou d’autres approches afin d’adresser une requête HTTP à partir du code serveur. Cette implémentation spécifique vous est également fournie en tant qu’exportation à partir du package @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;
    }
}

Ajouter l’efficacité et la gestion des erreurs

La AzureFunctionTokenProvider est une implémentation simple de TokenProvider qui doit être traitée comme un point de départ lors de l’implémentation de votre propre fournisseur de jetons personnalisé. Pour l’implémentation d’un fournisseur de jetons prêt pour la production, vous devez prendre en compte différents scénarios d’échec dont le fournisseur de jetons a besoin pour gérer. Par exemple, l’implémentation AzureFunctionTokenProvider ne parvient pas à gérer les situations de déconnexion réseau, car elle ne met pas en cache le jeton côté client.

Lorsque le conteneur se déconnecte, le gestionnaire de connexions tente d’obtenir un nouveau jeton à partir du TokenProvider avant de se reconnecter au conteneur. Pendant que le réseau est déconnecté, la demande d’obtention de l’API effectuée dans fetchOrdererToken échoue et génère une erreur non retentante. Cela entraîne à son tour la suppression du conteneur et l’impossibilité de se reconnecter même si une connexion réseau est rétablie.

Une solution potentielle pour ce problème de déconnexion consiste à mettre en cache des jetons valides dans Window.localStorage. Avec la mise en cache des jetons, le conteneur récupère un jeton stocké valide au lieu d’effectuer une requête d’obtention d’API pendant que le réseau est déconnecté. Notez qu’un jeton stocké localement peut expirer après une certaine période et que vous devez toujours effectuer une demande d’API pour obtenir un nouveau jeton valide. Dans ce cas, la gestion des erreurs et la logique de nouvelle tentative supplémentaires sont requises pour empêcher le conteneur de se supprimer après une seule tentative ayant échoué.

La façon dont vous choisissez d’implémenter ces améliorations est entièrement à vous et aux exigences de votre application. Notez qu’avec la solution de jeton localStorage , vous verrez également des améliorations des performances dans votre application, car vous supprimez une demande réseau sur chaque appel getContainer .

La mise en cache des jetons avec quelque chose comme localStorage peut présenter des implications en matière de sécurité, et c’est à votre discrétion quand vous décidez de la solution appropriée pour votre application. Que vous implémentez ou non la mise en cache des jetons, vous devez ajouter une logique de gestion des erreurs et de nouvelle tentative dans fetchOrdererToken et fetchStorageToken afin que le conteneur ne soit pas supprimé après un seul appel ayant échoué. Prenons l’exemple d’encapsuler l’appel de getToken dans un bloc try avec un bloc catch qui retente et lève une erreur uniquement après un nombre spécifié de nouvelles tentatives.

Voir aussi