Las interacciones humanas en Durable Functions: comprobación telefónica de ejemplo

Este ejemplo muestra cómo compilar una orquestación de Durable Functions con interacción humana. Cada vez que una persona real participa en un proceso automatizado,este debe ser capaz de enviar notificaciones a la persona y de recibir respuestas de forma asincrónica. También debe permitir la posibilidad de que la persona no esté disponible. (Esta última parte es donde los tiempos de espera son relevantes).

En este ejemplo se implementa un sistema de comprobación telefónica por SMS. A menudo se usan estos tipos de flujos al comprobar el número de teléfono de un cliente o para la autenticación multifactor (MFA). Es un ejemplo eficaz, porque toda la implementación se realiza con un par de pequeñas funciones. No se necesita almacén de datos externo, como una base de datos.

Nota:

La versión 4 del modelo de programación de Node.js para Azure Functions está disponible de forma general. El nuevo modelo v4 está diseñado para que los desarrolladores de JavaScript y TypeScript tengan una experiencia más flexible e intuitiva. Obtenga más información sobre las diferencias entre v3 y v4 en la guía de migración.

En los siguientes fragmentos de código, JavaScript (PM4) denota el modelo de programación V4, la nueva experiencia.

Prerrequisitos

Información general de escenario

La comprobación telefónica sirve para verificar la identidad de los usuarios finales de la aplicación y que no son responsables de correo basura. La autenticación multifactor es un caso de uso común para proteger las cuentas de usuario de los piratas informáticos. La dificultad de implementar su propia comprobación telefónica consiste en que requiere una interacción con estado con una persona. Normalmente, al usuario final se le proporciona código (por ejemplo, un número de 4 dígitos) y debe responder en un intervalo de tiempo razonable.

Las instancias normales de Azure Functions no tienen estado (igual que muchos otros puntos de conexión en la nube de otras plataformas), por lo que estos tipos de interacciones implican explícitamente la administración externa del estado en una base de datos u otro almacén persistente. Además, la interacción debe dividirse en varias funciones que se puedan coordinar entre sí. Por ejemplo, necesita al menos una función para determinar un código, almacenarlo en alguna parte y enviarlo al teléfono del usuario. Además, necesita al menos un otra función para recibir una respuesta del usuario y asignarla de algún modo a la llamada de función original para la validación del código. El tiempo de espera también es un aspecto importante para garantizar la seguridad. Este factor puede complicarse con bastante rapidez.

Con Durable Functions se reduce enormemente la complejidad de este escenario. Como verá en este ejemplo, una función de orquestador puede administrar la interacción con estado muy fácilmente y sin que intervengan almacenes de datos externos. Dado que las funciones de orquestador son durables, estos flujos interactivos también son muy confiables.

Configuración de la integración de Twilio

En este ejemplo se usa el servicio Twilio para enviar mensajes SMS a un teléfono móvil. Azure Functions ya compatible con Twilio a través del enlace de Twilio, el ejemplo utiliza esa característica.

Lo primero que necesita es una cuenta de Twilio. Puede crear una gratis en https://www.twilio.com/try-twilio. Cuando tenga una cuenta, agregue los tres valores de configuración de la aplicación siguientes a la aplicación de función.

Nombre del valor de configuración de la aplicación Descripción del valor
TwilioAccountSid Identificador de seguridad de la cuenta de Twilio
TwilioAuthToken Token de autenticación de la cuenta de Twilio
TwilioPhoneNumber Número de teléfono asociado a la cuenta de Twilio, que se utiliza para enviar mensajes SMS.

Funciones

En este artículo se explican las funciones siguientes en la aplicación de ejemplo:

  • E4_SmsPhoneVerification: función de orquestador que realiza el proceso de comprobación telefónica, incluida la administración de tiempos de espera y reintentos.
  • E4_SendSmsChallenge: función de actividad que envía un código mediante un mensaje de texto.

Nota

La función HttpStart de la aplicación de ejemplo y el inicio rápido actúa como cliente de orquestación que desencadena la función de orquestador.

Función de orquestador E4_SmsPhoneVerification

[FunctionName("E4_SmsPhoneVerification")]
public static async Task<bool> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    string phoneNumber = context.GetInput<string>();
    if (string.IsNullOrEmpty(phoneNumber))
    {
        throw new ArgumentNullException(
            nameof(phoneNumber),
            "A phone number input is required.");
    }

    int challengeCode = await context.CallActivityAsync<int>(
        "E4_SendSmsChallenge",
        phoneNumber);

    using (var timeoutCts = new CancellationTokenSource())
    {
        // The user has 90 seconds to respond with the code they received in the SMS message.
        DateTime expiration = context.CurrentUtcDateTime.AddSeconds(90);
        Task timeoutTask = context.CreateTimer(expiration, timeoutCts.Token);

        bool authorized = false;
        for (int retryCount = 0; retryCount <= 3; retryCount++)
        {
            Task<int> challengeResponseTask =
                context.WaitForExternalEvent<int>("SmsChallengeResponse");

            Task winner = await Task.WhenAny(challengeResponseTask, timeoutTask);
            if (winner == challengeResponseTask)
            {
                // We got back a response! Compare it to the challenge code.
                if (challengeResponseTask.Result == challengeCode)
                {
                    authorized = true;
                    break;
                }
            }
            else
            {
                // Timeout expired
                break;
            }
        }

        if (!timeoutTask.IsCompleted)
        {
            // All pending timers must be complete or canceled before the function exits.
            timeoutCts.Cancel();
        }

        return authorized;
    }
}

Nota

Es posible que no sea obvio al principio, pero este orquestador no infringe la restricción de orquestación determinista. Es determinista porque la propiedad CurrentUtcDateTime se usa para calcular la fecha de expiración del temporizador y devuelve el mismo valor en cada reproducción en ese momento en el código del orquestador. Este comportamiento es importante para garantizar que winner es igual en todas las llamadas a Task.WhenAny repetidas.

Una vez iniciada, la función de orquestador hace lo siguiente:

  1. Obtiene un número de teléfono al que enviar la notificación mediante SMS.
  2. Llama a E4_SendSmsChallenge para enviar un mensaje SMS al usuario y devuelve el código de desafío de 4 dígitos esperado.
  3. Crea un temporizador durable que se desencadena 90 segundos a partir de la hora actual.
  4. Además del temporizador, espera un evento SmsChallengeResponse del usuario.

El usuario recibe un mensaje SMS con un código de cuatro dígitos. Tiene 90 segundos para enviar ese mismo código de cuatro dígitos a la instancia de la función de orquestador y completar el proceso de comprobación. Si envía un código incorrecto, tiene otros tres intentos para enviar el correcto (dentro de esos mismos 90 segundos).

Advertencia

Es importante cancelar los temporizadores si ya no se necesita que expiren, como en el ejemplo anterior, cuando se acepta una respuesta de desafío.

Función de actividad E4_SendSmsChallenge

La función E4_SendSmsChallenge usa el enlace de Twilio para enviar el mensaje SMS con el código de cuatro dígitos al usuario final.

[FunctionName("E4_SendSmsChallenge")]
public static int SendSmsChallenge(
    [ActivityTrigger] string phoneNumber,
    ILogger log,
    [TwilioSms(AccountSidSetting = "TwilioAccountSid", AuthTokenSetting = "TwilioAuthToken", From = "%TwilioPhoneNumber%")]
        out CreateMessageOptions message)
{
    // Get a random number generator with a random seed (not time-based)
    var rand = new Random(Guid.NewGuid().GetHashCode());
    int challengeCode = rand.Next(10000);

    log.LogInformation($"Sending verification code {challengeCode} to {phoneNumber}.");

    message = new CreateMessageOptions(new PhoneNumber(phoneNumber));
    message.Body = $"Your verification code is {challengeCode:0000}";

    return challengeCode;
}

Nota

Primero debe instalar el paquete Nuget Microsoft.Azure.WebJobs.Extensions.Twilio para Functions a fin de ejecutar el código de ejemplo. No instale también el paquete Nuget de Twilio principal porque puede provocar problemas de control de versiones que produzcan errores de compilación.

Ejecución del ejemplo

Con las funciones desencadenadas mediante HTTP del ejemplo, puede iniciar la orquestación con el envío de la siguiente solicitud HTTP POST:

POST http://{host}/orchestrators/E4_SmsPhoneVerification
Content-Length: 14
Content-Type: application/json

"+1425XXXXXXX"
HTTP/1.1 202 Accepted
Content-Length: 695
Content-Type: application/json; charset=utf-8
Location: http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}

{"id":"741c65651d4c40cea29acdd5bb47baf1","statusQueryGetUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}","sendEventPostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}","terminatePostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/terminate?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}"}

La función de orquestador recibe el número de teléfono proporcionado e inmediatamente le envía un mensaje SMS con un código de verificación de 4 dígitos generado aleatoriamente; por ejemplo, 2168. A continuación, la función espera respuesta durante 90 segundos.

Para responder con el código, puede usar RaiseEventAsync (.NET) o raiseEvent (JavaScript/TypeScript) dentro de otra función o invocar el webhook HTTP POST sendEventUrl al que se hace referencia en la respuesta 202 anterior y sustituir {eventName} por el nombre del evento, SmsChallengeResponse:

POST http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/raiseEvent/SmsChallengeResponse?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
Content-Length: 4
Content-Type: application/json

2168

Si se envía antes de que expire el temporizador, se completa la orquestación y el campo output se establece en true, lo cual indica que la comprobación es correcta.

GET http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
HTTP/1.1 200 OK
Content-Length: 144
Content-Type: application/json; charset=utf-8

{"runtimeStatus":"Completed","input":"+1425XXXXXXX","output":true,"createdTime":"2017-06-29T19:10:49Z","lastUpdatedTime":"2017-06-29T19:12:23Z"}

Si permite que el temporizador expire o si escribe el código incorrecto cuatro veces, puede consultar el estado y verá una salida de función de orquestación false, que indica un error en la comprobación telefónica.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 145

{"runtimeStatus":"Completed","input":"+1425XXXXXXX","output":false,"createdTime":"2017-06-29T19:20:49Z","lastUpdatedTime":"2017-06-29T19:22:23Z"}

Pasos siguientes

En este ejemplo, se han demostrado algunas de las funcionalidades avanzadas de Durable Functions, en particular las API WaitForExternalEvent y CreateTimer. Hemos visto cómo estas se combinan con Task.WaitAny (C#)/context.df.Task.any (JavaScript/TypeScript)/context.task_any (Python) para implementar un sistema confiable de tiempo de espera, que a menudo resulta útil para interactuar con personas reales. Puede obtener más información acerca de cómo utilizar Durable Functions leyendo una serie de artículos que ofrecen información detallada acerca de temas específicos.