Procedimiento: Escritura de un TokenProvider con una función de Azure

Nota:

Esta versión preliminar se ofrece sin Acuerdo de Nivel de Servicio y no se recomienda para cargas de trabajo de producción. Es posible que algunas características no sean compatibles o que tengan sus funcionalidades limitadas.

En Fluid Framework, los proveedores de tokens son responsables de crear y firmar tokens que usa @fluidframework/azure-client para realizar solicitudes al servicio Azure Fluid Relay. Fluid Framework proporciona un proveedor de tokens sencillo y poco seguro con fines de desarrollo, llamado acertadamente insecureTokenProvider. Cada servicio de Fluid debe implementar un proveedor de tokens personalizado basado en las consideraciones particulares de seguridad y autenticación del servicio.

A cada recurso de Azure Fluid Relay que cree se le asigna un id. de inquilino y su propia clave secreta de inquilino única. La clave secreta es un secreto compartido. La aplicación o el servicio lo sabe, y el servicio Azure Fluid Relay también. Los proveedores de tokens deben conocer la clave secreta para firmar las solicitudes, pero no se puede incluir en el código de cliente.

Implementación de una función de Azure para firmar los tokens

Una opción para crear un proveedor de tokens seguro es crear un punto de conexión HTTPS y una implementación de TokenProvider que realiza solicitudes HTTPS autenticadas a ese punto de conexión para recuperar tokens. Esta ruta de acceso le permite almacenar la clave secreta de inquilino en una ubicación segura, como Azure Key Vault.

La solución completa tiene dos partes:

  1. Un punto de conexión HTTPS que acepta solicitudes y devuelve tokens de Azure Fluid Relay.
  2. Una implementación de ITokenProvider que acepta una dirección URL a un punto de conexión y, a continuación, realiza solicitudes a ese punto de conexión para recuperar tokens.

Cree un punto de conexión para el TokenProvider mediante Azure Functions

Usar Azure Functions es una manera rápida de crear este tipo de punto de conexión HTTPS.

En este ejemplo se muestra cómo crear su propia función de Azure HTTPTrigger, que captura el token pasando la clave de inquilino.

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 función generateToken, que se encuentra en el paquete @fluidframework/azure-service-utils, genera un token para el usuario en cuestión que se firma con la clave secreta del inquilino. Este método permite que el token se devuelva al cliente sin exponer el secreto. En su lugar, el token se genera en el servidor con el secreto a fin de proporcionar acceso limitado al documento especificado. El ITokenProvider de ejemplo que se muestra a continuación realiza solicitudes HTTP a esta función de Azure a fin de recuperar los tokens.

Implementación de la función de Azure

Es posible implementar Azure Functions de varias maneras. Para obtener más información, consulte la sección de la documentación de Azure Functions que trata sobre la implementación a fin de obtener más información sobre cómo implementar Azure Functions.

Implementación de TokenProvider

Un TokenProvider se puede implementar de varias maneras, pero es necesario implementar dos llamadas API independientes: fetchOrdererToken y fetchStorageToken. Estas API son responsables de capturar tokens para los servicios de orden y almacenamiento de Fluid, respectivamente. Ambas funciones devuelven objetos TokenResponse que representan el valor del token. El runtime de Fluid Framework llama a estas dos API según sea necesario para recuperar tokens. Tenga en cuenta que, aunque el código de la aplicación usa solo un punto de conexión de servicio para establecer la conectividad con el servicio Azure Fluid Relay, azure-client internamente junto con el servicio traduce ese punto de conexión a un par de puntos de conexión de almacenamiento y solicitante. Esos dos puntos de conexión se usan desde ese momento para esa sesión, por lo que es necesario implementar las dos funciones independientes para capturar tokens, una para cada uno.

Para tener la seguridad de que la clave secreta del inquilino se mantiene segura, se almacena en una ubicación de back-end protegida y solo es accesible desde la función de Azure. Para recuperar tokens, debe realizar una solicitud GET o POST a la función de Azure implementada y proporcionar los valores de tenantID y documentId, y userID/userName. La función de Azure es responsable de la asignación entre el identificador de inquilino y un secreto de clave de inquilino para generar y firmar correctamente el token.

La implementación de ejemplo siguiente controla la realización de estas solicitudes a Azure Function. Usa la biblioteca axios para realizar solicitudes HTTP. Puede usar otras bibliotecas o enfoques para hacer una solicitud HTTP desde el código del servidor. Esta implementación específica también se proporciona como exportación desde el paquete @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;
    }
}

Adición de eficiencia y control de errores

AzureFunctionTokenProvider es una implementación sencilla de TokenProvider que se debe tratar como punto inicial al implementar su propio proveedor de tokens personalizado. Para la implementación de un proveedor de tokens listo para producción, debe tener en cuenta varios escenarios de error que el proveedor de tokens necesita controlar. Por ejemplo, la implementación AzureFunctionTokenProvider no puede controlar situaciones de desconexión de red porque no almacena en caché el token en el lado cliente.

Cuando el contenedor se desconecta, el administrador de conexiones intenta obtener un nuevo token del TokenProvider antes de volver a conectarse al contenedor. Mientras la red está desconectada, se producirá un error en la solicitud de obtención de la API en fetchOrdererToken y se producirá un error que no se puede reintentar. Esto, a su vez, conduce a que el contenedor se elimine y no pueda volver a conectarse aunque se vuelva a establecer una conexión de red.

Una posible solución para este problema de desconexión es almacenar en caché tokens válidos en Window.localStorage. Con el almacenamiento en caché de tokens, el contenedor recuperará un token almacenado válido en lugar de realizar una solicitud de obtención de API mientras la red está desconectada. Tenga en cuenta que un token almacenado localmente podría expirar después de un período de tiempo determinado y todavía tendría que realizar una solicitud de API para obtener un nuevo token válido. En este caso, se requeriría un control de errores adicional y una lógica de reintento para evitar que el contenedor se eliminara después de un único intento de error.

La forma en que decida aplicar estas mejoras depende totalmente de usted y los requisitos de su aplicación. Tenga en cuenta que, con la solución de token localStorage, también verá mejoras de rendimiento en la aplicación porque va a quitar una solicitud de red en cada llamada getContainer.

El almacenamiento en caché de tokens con algo como localStorage puede tener implicaciones de seguridad y depende de su criterio al decidir qué solución es adecuada para la aplicación. Independientemente de si implementa o no el almacenamiento en caché de tokens, debe agregar lógica de control de errores y reintento en fetchOrdererToken y fetchStorageToken para que el contenedor no se elimine después de una sola llamada con error. Considere, por ejemplo, encapsular la llamada de getToken en un bloque try con un bloque catch que reintenta y produce un error solo después de un número especificado de reintentos.

Consulte también