Como: Escrever um TokenProvider com uma função do Azure

Nota

Esta versão de pré-visualização é fornecida sem um contrato de nível de serviço e não é recomendada para cargas de trabalho de produção. Algumas funcionalidades poderão não ser suportadas ou poderão ter capacidades limitadas.

No Fluid Framework, os TokenProviders são responsáveis por criar e assinar tokens que o @fluidframework/azure-client usa para fazer solicitações ao serviço Azure Fluid Relay. O Fluid Framework fornece um TokenProvider simples e inseguro para fins de desenvolvimento, apropriadamente chamado InsecureTokenProvider. Cada serviço Fluid deve implementar um TokenProvider personalizado com base nas considerações de autenticação e segurança do serviço específico.

Cada recurso do Azure Fluid Relay criado recebe uma ID de locatário e sua própria chave secreta de locatário exclusiva. A chave secreta é um segredo partilhado. Seu aplicativo/serviço sabe disso, e o serviço Azure Fluid Relay sabe disso. TokenProviders deve saber a chave secreta para assinar solicitações, mas a chave secreta não pode ser incluída no código do cliente.

Implementar uma função do Azure para assinar tokens

Uma opção para criar um provedor de token seguro é criar um ponto de extremidade HTTPS e criar uma implementação TokenProvider que faça solicitações HTTPS autenticadas para esse ponto de extremidade para recuperar tokens. Esse caminho permite armazenar a chave secreta do locatário em um local seguro, como o Cofre da Chave do Azure.

A solução completa tem duas partes:

  1. Um ponto de extremidade HTTPS que aceita solicitações e retorna tokens do Azure Fluid Relay.
  2. Uma implementação ITokenProvider que aceita uma URL para um ponto de extremidade e, em seguida, faz solicitações a esse ponto de extremidade para recuperar tokens.

Criar um ponto de extremidade para seu TokenProvider usando o Azure Functions

Usar o Azure Functions é uma maneira rápida de criar esse ponto de extremidade HTTPS.

Este exemplo demonstra como criar sua própria Função do Azure HTTPTrigger que busca o token passando sua chave de locatário.

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;

A generateToken função, encontrada no @fluidframework/azure-service-utils pacote, gera um token para determinado usuário que é assinado usando a chave secreta do locatário. Esse método permite que o token seja retornado ao cliente sem expor o segredo. Em vez disso, o token é gerado no lado do servidor usando o segredo para fornecer acesso com escopo ao documento determinado. O exemplo ITokenProvider abaixo faz solicitações HTTP para esta Função do Azure para recuperar os tokens.

Implantar a função do Azure

O Azure Functions pode ser implantado de várias maneiras. Para obter mais informações, consulte a seção Implantar da documentação do Azure Functions para obter mais informações sobre como implantar o Azure Functions.

Implementar o TokenProvider

TokenProviders pode ser implementado de várias maneiras, mas deve implementar duas chamadas de API separadas: fetchOrdererToken e fetchStorageToken. Essas APIs são responsáveis por buscar tokens para os serviços de encomendador e armazenamento do Fluid, respectivamente. Ambas as funções retornam TokenResponse objetos que representam o valor do token. O tempo de execução do Fluid Framework chama essas duas APIs conforme necessário para recuperar tokens. Observe que, embora o código do aplicativo esteja usando apenas um ponto de extremidade de serviço para estabelecer conectividade com o serviço Azure Fluid Relay, o azure-client internamente em conjunto com o serviço converte esse ponto de extremidade em um par de ponto de extremidade de pedido e armazenamento. Esses dois pontos de extremidade são usados a partir desse ponto para essa sessão, e é por isso que você precisa implementar as duas funções separadas para buscar tokens, uma para cada um.

Para garantir que a chave secreta do locatário seja mantida segura, ela é armazenada em um local de back-end seguro e só é acessível de dentro da Função do Azure. Para recuperar tokens, você precisa fazer uma GET ou solicitação para sua Função do Azure implantada, fornecendo o tenantID e documentId, euserNameuserID/ .POST A Função do Azure é responsável pelo mapeamento entre a ID do locatário e um segredo de chave de locatário para gerar e assinar adequadamente o token.

O exemplo de implementação abaixo lida com a realização dessas solicitações para sua Função do Azure. Ele usa a biblioteca axios para fazer solicitações HTTP. Você pode usar outras bibliotecas ou abordagens para fazer uma solicitação HTTP do código do servidor. Essa implementação específica também é fornecida para você como uma exportação do @fluidframework/azure-client pacote.

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

Adicione eficiência e tratamento de erros

A AzureFunctionTokenProvider é uma implementação simples da qual deve ser tratada como um ponto de partida ao implementar seu próprio provedor de TokenProvider token personalizado. Para a implementação de um provedor de token pronto para produção, você deve considerar vários cenários de falha que o provedor de token precisa lidar. Por exemplo, a AzureFunctionTokenProvider implementação falha ao lidar com situações de desconexão de rede porque não armazena em cache o token no lado do cliente.

Quando o contêiner se desconecta, o gerenciador de conexões tenta obter um novo token do TokenProvider antes de se reconectar ao contêiner. Enquanto a rede estiver desconectada, a solicitação de obtenção de API feita falhará fetchOrdererToken e lançará um erro não retentável. Isso, por sua vez, faz com que o contêiner seja descartado e não seja capaz de se reconectar, mesmo que uma conexão de rede seja restabelecida.

Uma possível solução para esse problema de desconexão é armazenar tokens válidos em cache em Window.localStorage. Com o cache de tokens, o contêiner recuperará um token armazenado válido em vez de fazer uma solicitação de API get enquanto a rede estiver desconectada. Observe que um token armazenado localmente pode expirar após um determinado período de tempo e você ainda precisará fazer uma solicitação de API para obter um novo token válido. Nesse caso, seria necessário um tratamento de erro adicional e uma lógica de repetição para impedir que o contêiner fosse descartado após uma única tentativa com falha.

A forma como você escolhe implementar essas melhorias depende completamente de você e dos requisitos do seu aplicativo. Observe que, com a solução de localStorage token, você também verá melhorias de desempenho em seu aplicativo porque está removendo uma solicitação de rede em cada getContainer chamada.

O cache de tokens com algo parecido localStorage pode vir com implicações de segurança, e fica a seu critério ao decidir qual solução é apropriada para seu aplicativo. Independentemente de implementar ou não o cache de tokens, você deve adicionar a lógica de tratamento de erros e repetição para fetchOrdererToken fetchStorageToken que o contêiner não seja descartado após uma única chamada com falha. Considere, por exemplo, envolver a chamada em getToken um try bloco com um catch bloco que tenta novamente e lança um erro somente após um número especificado de tentativas.

Consulte também