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:
- Um ponto de extremidade HTTPS que aceita solicitações e retorna tokens do Azure Fluid Relay.
- 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 getToken
em um bloco try
com um bloco catch
que repete a tentativa e gera um erro somente após um número especificado de tentativas.