Interação humana em funções duráveis - Exemplo de verificação por telefone

Este exemplo demonstra como criar uma orquestração de funções duráveis que envolve a interação humana. Sempre que uma pessoa real está envolvida em um processo automatizado, o processo deve ser capaz de enviar notificações para a pessoa e receber respostas de forma assíncrona. Deve igualmente prever a possibilidade de a pessoa não estar disponível. (Esta última parte é onde os tempos limite se tornam importantes.)

Este exemplo implementa um sistema de verificação por telefone baseado em SMS. Esses tipos de fluxos são frequentemente usados ao verificar o número de telefone de um cliente ou para autenticação multifator (MFA). É um exemplo poderoso porque toda a implementação é feita usando algumas pequenas funções. Nenhum armazenamento de dados externo, como um banco de dados, é necessário.

Nota

A versão 4 do modelo de programação Node.js para o Azure Functions está disponível em geral. O novo modelo v4 foi projetado para ter uma experiência mais flexível e intuitiva para desenvolvedores de JavaScript e TypeScript. Saiba mais sobre as diferenças entre v3 e v4 no guia de migração.

Nos trechos de código a seguir, JavaScript (PM4) indica o modelo de programação V4, a nova experiência.

Pré-requisitos

Descrição geral do cenário

A verificação por telefone é usada para verificar se os usuários finais do seu aplicativo não são spammers e se eles são quem dizem ser. A autenticação multifator é um caso de uso comum para proteger contas de usuários contra hackers. O desafio de implementar sua própria verificação por telefone é que ela requer uma interação stateful com um ser humano. Um usuário final normalmente recebe algum código (por exemplo, um número de 4 dígitos) e deve responder em um período razoável de tempo.

O Azure Functions comum é sem monitoração de estado (assim como muitos outros pontos de extremidade de nuvem em outras plataformas), portanto, esses tipos de interações envolvem o gerenciamento explícito do estado externamente em um banco de dados ou em algum outro armazenamento persistente. Além disso, a interação deve ser dividida em múltiplas funções que podem ser coordenadas em conjunto. Por exemplo, você precisa de pelo menos uma função para decidir sobre um código, persisti-lo em algum lugar e enviá-lo para o telefone do usuário. Além disso, você precisa de pelo menos uma outra função para receber uma resposta do usuário e, de alguma forma, mapeá-la de volta para a chamada de função original, a fim de fazer a validação do código. Um tempo limite também é um aspeto importante para garantir a segurança. Pode tornar-se bastante complexo rapidamente.

A complexidade desse cenário é muito reduzida quando você usa funções duráveis. Como você verá neste exemplo, uma função orchestrator pode gerenciar a interação stateful facilmente e sem envolver nenhum armazenamento de dados externo. Como as funções do orquestrador são duráveis, esses fluxos interativos também são altamente confiáveis.

Configurando a integração do Twilio

Este exemplo envolve o uso do serviço Twilio para enviar mensagens SMS para um telefone celular. O Azure Functions já tem suporte para o Twilio por meio da associação do Twilio, e o exemplo usa esse recurso.

A primeira coisa que você precisa é de uma conta Twilio. Você pode criar um gratuitamente em https://www.twilio.com/try-twilio. Depois de ter uma conta, adicione as três configurações de aplicativo a seguir ao seu aplicativo de função.

Nome de definição de aplicação Descrição do valor
TwilioAccountSid O SID da sua conta Twilio
TwilioAuthToken O token de autenticação para sua conta Twilio
TwilioPhoneNumber O número de telefone associado à sua conta Twilio. Isso é usado para enviar mensagens SMS.

As funções

Este artigo descreve as seguintes funções no aplicativo de exemplo:

  • E4_SmsPhoneVerification: Uma função orquestradora que executa o processo de verificação do telefone, incluindo o gerenciamento de tempos limite e tentativas.
  • E4_SendSmsChallenge: Uma função de atividade que envia um código via mensagem de texto.

Nota

A HttpStart função no aplicativo de exemplo e o início rápido atuam como cliente Orchestration que aciona a função orchestrator.

E4_SmsPhoneVerification função orquestradora

[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

Pode não ser óbvio no início, mas este orquestrador não viola a restrição de orquestração determinista. É determinístico porque a CurrentUtcDateTime propriedade é usada para calcular o tempo de expiração do temporizador e retorna o mesmo valor em cada repetição neste ponto do código do orquestrador. Esse comportamento é importante para garantir que o mesmo winner resulte de cada chamada repetida para Task.WhenAny.

Uma vez iniciada, esta função orquestradora faz o seguinte:

  1. Obtém um número de telefone para o qual enviará a notificação por SMS.
  2. Chama E4_SendSmsChallenge para enviar uma mensagem SMS para o usuário e retorna o código de desafio de 4 dígitos esperado.
  3. Cria um temporizador durável que dispara 90 segundos a partir da hora atual.
  4. Em paralelo com o temporizador, aguarda um evento SmsChallengeResponse do usuário.

O usuário recebe uma mensagem SMS com um código de quatro dígitos. Eles têm 90 segundos para enviar esse mesmo código de quatro dígitos de volta para a instância da função orchestrator para concluir o processo de verificação. Se eles enviarem o código errado, eles receberão mais três tentativas para acertar (dentro da mesma janela de 90 segundos).

Aviso

É importante cancelar os temporizadores se já não precisar que eles expirem, como no exemplo acima, quando uma resposta de desafio é aceite.

E4_SendSmsChallenge função de atividade

A função E4_SendSmsChallenge usa a ligação Twilio para enviar a mensagem SMS com o código de quatro dígitos para o usuário 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

Você deve primeiro instalar o Microsoft.Azure.WebJobs.Extensions.Twilio pacote Nuget para Functions para executar o código de exemplo. Não instale também o pacote nuget principal do Twilio porque isso pode causar problemas de versionamento que resultam em erros de compilação.

Executar o exemplo

Usando as funções acionadas por HTTP incluídas no exemplo, você pode iniciar a orquestração enviando a seguinte solicitação 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}"}

A função orquestrador recebe o número de telefone fornecido e envia-lhe imediatamente uma mensagem SMS com um código de verificação de 4 dígitos gerado aleatoriamente — por exemplo, 2168. Em seguida, a função aguarda 90 segundos por uma resposta.

Para responder com o código, você pode usar RaiseEventAsync (.NET) ou raiseEvent (JavaScript/TypeScript) dentro de outra função ou invocar o webhook HTTP POST sendEventPostUri referenciado na resposta 202 acima, substituindo {eventName} pelo nome do 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

Se você enviar isso antes que o temporizador expire, a orquestração será concluída e o output campo será definido como true, indicando uma verificação bem-sucedida.

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

Se você deixar o temporizador expirar, ou se você inserir o código errado quatro vezes, você pode consultar o status e ver uma saída de false função de orquestração, indicando que a verificação do telefone falhou.

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

Próximos passos

Este exemplo demonstrou algumas das capacidades avançadas das Funções Duráveis, nomeadamente WaitForExternalEvent e CreateTimer APIs. Você viu como eles podem ser combinados com Task.WaitAny (C#)/context.df.Task.any (JavaScript/TypeScript)/context.task_any (Python) para implementar um sistema de tempo limite confiável, que muitas vezes é útil para interagir com pessoas reais. Você pode aprender mais sobre como usar funções duráveis lendo uma série de artigos que oferecem uma cobertura aprofundada de tópicos específicos.