Compartilhar via


Como gravar um TokenProvider com uma função do Azure

Observação

Essa versão prévia é fornecida sem contrato de nível de serviço e não é recomendada para cargas de trabalho de produção. Alguns recursos podem não ter suporte ou podem ter restrição de recursos.

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

Cada recurso do Azure Fluid Relay que você cria recebe uma ID de locatário e a própria chave secreta exclusiva do locatário. A chave secreta é um segredo compartilhado. Tanto seu aplicativo/serviço quanto o serviço do Azure Fluid Relay o conhecem. TokenProviders precisam conhecer 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 compilar um provedor de token seguro é criar um ponto de extremidade HTTPS e criar uma implementação TokenProvider que faz solicitações HTTPS autenticadas para esse ponto de extremidade para recuperar tokens. Esse caminho permite que você armazene a chave secreta do locatário em um local seguro, como Azure Key Vault.

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 de ITokenProvider que aceita uma URL para um ponto de extremidade e faz solicitações para esse ponto de extremidade para recuperar tokens.

Criar um ponto de extremidade para o 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 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 função generateToken, encontrada no pacote @fluidframework/azure-service-utils, gera um token para o 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 fornecido. O exemplo de ITokenProvider abaixo faz solicitações HTTP para essa função do Azure para recuperar tokens.

Implantar a Azure Function

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

Implementar o TokenProvider

Os TokenProviders podem ser implementados de várias maneiras, mas devem implementar duas chamadas à API separadas: fetchOrdererToken e fetchStorageToken. Essas APIs são responsáveis por buscar tokens para o Fluid solicitado e os serviços de armazenamento, respectivamente. As duas funções retornam objetos TokenResponse que representam o valor do token. O runtime 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 do Azure Fluid Relay, o cliente do Azure internamente em conjunto com o serviço converte esse ponto de extremidade em um par de pontos de extremidade de armazenamento e ordenador. Esses dois pontos de extremidade são usados desse ponto em diante para essa sessão e é por isso que você precisa implementar as duas funções separadas para buscar tokens, uma para cada.

Para garantir que a chave secreta do locatário seja mantida em segurança, ela é armazenada em um local de back-end seguro e só pode ser acessada de dentro da Função do Azure. Para recuperar tokens, você precisa fazer uma solicitação GET ou POST para seu Azure Functions implantado, fornecendo o tenantID, documentId e userID/userName. O Azure Functions é responsável pelo mapeamento entre a ID do locatário e um segredo de chave do locatário para gerar e assinar adequadamente o token.

A implementação de exemplo a seguir lida com a realização dessas solicitações para sua Função do Azure. Ela 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 pacote @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;
    }
}

Adicionar eficiência e tratamento de erros

A AzureFunctionTokenProvider é uma implementação simples da TokenProvider que deve ser tratada como um ponto de partida ao implementar seu próprio provedor de 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 implementação AzureFunctionTokenProvider 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 junto ao TokenProvider antes de se reconectar ao contêiner. Embora a rede esteja desconectada, a solicitação de obtenção da API feita no fetchOrdererToken falhará e gerará um erro sem repetição. Isso, por sua vez, faz com que o contêiner seja descartado e não possa se reconectar mesmo que uma conexão de rede seja restabelecida.

Uma possível solução para esse problema de desconexão é armazenar em cache tokens válidos no Window.localStorage. Com o cache de token, o contêiner recuperará um token armazenado válido em vez de fazer uma solicitação de obtenção da API 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, a lógica adicional de tratamento de erros e repetição será necessária para impedir que o contêiner seja descartado após uma tentativa com falha.

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

O cache de tokens com algo como localStorage pode vir com implicações de segurança e cabe a você decidir qual solução é apropriada para seu aplicativo. Não importa se você vai ou não implementar o cache de token, mas é necessário adicionar a lógica de tratamento de erros e repetição no fetchOrdererToken e fetchStorageToken para que o contêiner não seja descartado após uma chamada com falha. Considere, por exemplo, encapsular a chamada de getTokenem um bloco try com um bloco catch que repete a tentativa e gera um erro somente após um número especificado de tentativas.

Confira também