https://d226lax1qjow5r.cloudfront.net/blog/blogposts/add-2fa-to-your-application-with-the-verify-api-and-golang-sdk/go_verify-apis_1200x600.png

Añada 2FA a su aplicación con Verify API y Golang SDK

Publicado el March 16, 2021

Tiempo de lectura: 16 minutos

Cuando has trabajado duro en la construcción de una aplicación web que ofrece un valor real a tus usuarios, puede ser realmente descorazonador ver cómo se abusa de ella. Credenciales filtradas, registros falsos... siempre hay una pequeña minoría que intenta utilizar tu plataforma para sus propios fines nefastos.

Aunque es casi imposible evitar que esto ocurra en algún nivel, puede disuadir a todos los abusadores, excepto a los más comprometidos, implantando la autenticación de doble factor (2FA).

¿Qué es la 2FA?

2FA es una capa adicional de protección que requiere que el usuario proporcione algo más que un nombre de usuario y una contraseña para utilizar su servicio. Por lo general, se trata del acceso a un dispositivo móvil que puede recibir un código de seguridad que luego introduce en su aplicación como parte del proceso de registro o inicio de sesión. Como tu número de teléfono es único, a los estafadores les resulta mucho más difícil hacerse pasar por usuarios existentes o registrar varias cuentas falsas.

Otro buen caso de uso para 2FA es la "autenticación por pasos". Esto es ideal para situaciones en las que un usuario intenta hacer algo en su aplicación que podría tener malas consecuencias si ese usuario no es quien dice ser. Por ejemplo, si tiene una aplicación bancaria y su usuario intenta añadir un nuevo beneficiario. En casos como este, 2FA proporciona cierta seguridad de que son legítimos.

La Verify API de Vonage

La página Verify API de Vonage hace que sea realmente sencillo implementar 2FA en tus aplicaciones y eso es lo que esperamos demostrar en el post de hoy. Construiremos un sitio simple para una compañía ficticia llamada Acme Inc, que requiere que los usuarios se registren usando su dispositivo móvil para agregar cierto nivel de protección contra el uso fraudulento.

Y, para hacer las cosas más emocionantes, vamos a construir esto en nuestro nuevo (beta) Go SDK. Esto debería complacer a todos los Gophers endurecidos por ahí y tal vez incluso animar a otros a tener su primera experiencia de desarrollo Go? (Alerta de spoiler: creo que te encantará Go).

¡Manos a la obra! (Si no tienes tiempo, puedes encontrar el código fuente en el código fuente en GitHub).

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Cree su proyecto

Crea un directorio para tu proyecto y dentro de él, ejecuta go mod init. Esto genera un archivo go.mod para gestionar las dependencias.

mkdir go-2fa-example cd go-2fa-example go mod init go-2fa-example

A continuación, cree un main.go archivo. Aquí es donde escribirás el código de tu aplicación:

touch main.go

Vamos a utilizar plantillas Go para construir nuestro sitio. Vamos a crear un directorio para ellas (tmpl) y también para el CSS que usaremos para dar estilo a nuestras páginas (static):

mkdir tmpl static

Configurar el SDK Vonage Go

Vamos a utilizar la Verify API para pedir a los usuarios que demuestren que son propietarios del dispositivo con el que quieren registrarse en nuestro servicio. Verify API hace que todo el proceso de autenticación de dos factores sea realmente sencillo.

No hace mucho tiempo, los desarrolladores de Go habríamos tenido que hacer todas las llamadas a la API manualmente, pero en estos tiempos más ilustrados tenemos nuestro nuevo y reluciente Vonage Go SDK para hacernos la vida más fácil, así que vamos a instalarlo y configurarlo.

Para utilizar el SDK de Go tenemos que proporcionar nuestra clave y secreto de API, que se pueden encontrar en el Panel del desarrollador. Ahora bien, no queremos poner esas credenciales directamente en nuestro código, porque cualquiera que tenga acceso a nuestra clave y secreto puede hacer llamadas a la API a nuestra costa. Así que configuraremos esos detalles en un archivo .env y usaremos el paquete github.com/joho/godotenv para leer esos valores en variables de entorno.

En primer lugar, cree el archivo .env en el directorio del proyecto:

VONAGE_API_KEY=<YOUR_API_KEY>
VONAGE_API_SECRET=<YOUR_API_SECRET>

Sustituya <YOUR_API_KEY> y <YOUR_API_SECRET> por su propia clave y secreto de API.

A continuación, ejecute go get para instalar dotenv y el SDK Vonage Go:

go get github.com/joho/godotenv go get github.com/vonage/vonage-go-sdk

En main.go escribe código para leer tus credenciales de la API de Vonage desde el archivo .env y utilizarlas para instanciar una instancia del SDK Vonage Go VerifyClient:

package main

import (
	"log"
	"os"

	"github.com/joho/godotenv"
	"github.com/vonage/vonage-go-sdk"
)

var verifyClient *vonage.VerifyClient

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
		os.Exit(1)
	}

	apiKey := os.Getenv("VONAGE_API_KEY")
	apiSecret := os.Getenv("VONAGE_API_SECRET")

	auth := vonage.CreateAuthFromKeySecret(apiKey, apiSecret)
	verifyClient = vonage.NewVerifyClient(auth)

}

Construir la interfaz de usuario

Para entender qué páginas tiene que presentar nuestra aplicación web a los usuarios, consideremos el flujo de trabajo que vamos a implementar:

The verification processThe verification process

A partir de aquí, puede ver que necesitamos las siguientes páginas:

  • La página página de inicio (/): Si el usuario está registrado, mostramos aquí su nombre registrado y su número de teléfono.

  • La página página de registro (/register): Donde el usuario puede introducir su nombre y número de teléfono para registrarse en nuestro servicio.

  • El código de verificación página de entrada (/enter-code): Donde el usuario introduce el código PIN enviado a su teléfono por la Verify API.

Crearemos estas páginas utilizando plantillas Go. Estas facilitan el paso de variables desde el código a la página y también hacen que una plantilla incluya otras como "parciales" para que no tengas que duplicar contenido común como cabeceras, menús, etc.

En primer lugar, añada el módulo html/template a la lista de importaciones en main.go. A continuación, cree las siguientes plantillas en el directorio tmpl directorio:

La plantilla de la página base

Este es el diseño base que utilizarán todas nuestras plantillas. La directiva {{template "main" .}} nos permite rellenar el cuerpo de la página con contenido de las otras plantillas:

<!-- base.layout.gohtml-->
{{define "base"}}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Go Verify Demo</title>
  <link rel="stylesheet" href="/static/style.css">
  <link rel="preconnect" href="https://fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300&family=Seymour+One&display=swap" rel="stylesheet">
</head>

<body>
  {{template "main" .}}
</body>

</html>
{{end}}

La plantilla de la página de inicio

Cuando el usuario se haya registrado correctamente en el servicio, esta página mostrará su nombre y número de teléfono:

<!-- home.page.gohtml -->
{{template "base" .}}

{{define "main"}}
  <h1>Welcome to Acme Inc {{.Name}}!</h1>
  <p>Your registered phone number is: {{.Phone}}</p>
{{end}}

Plantilla de la página de registro

Esta plantilla incluye un formulario en el que el usuario puede introducir su nombre y número de teléfono para registrarse en el servicio:

<!-- register.page.gohtml -->
{{template "base" .}}

{{define "main"}}
  <h1>Acme inc.</h1>
  <p>Please register using your mobile/cell phone number for the exciting benefits our service provides!</p>
  <form action="/verify">
    <fieldset>
      <label for="name">Your name:</label>
      <input type="text" class="ghost-input" id="name" name="name" placeholder="Jane Smith" required/>   
      <label for="phone_number">Your mobile number:</label>
      <input type="text" class="ghost-input" id="phone_number" name="phone_number" placeholder="447700900001"required/>
      <button type="submit" class="ghost-button">Register</button>
    </fieldset>
  </form>
{{end}} 

Plantilla de la página de introducción de códigos

Esta página muestra un formulario en el que el usuario introduce el código que ha recibido de la Verify API:

<!-- entercode.page.gohtml -->
{{template "base" .}}

{{define "main"}}
  <h1>Acme inc.</h1>
  <p>Please enter the code you received by SMS:</p>
  <form action="/check-code">
    <fieldset>
      <label for="pin_code">Enter PIN:</label>
      <input type="text" class="ghost-input" id="pin_code" name="pin_code" placeholder="123456" required><br><br>
    <button type="submit" class="ghost-button">Let's Go!</button>
    </fieldset>
  </form>
  
{{end}} 

Haz que parezca bonito(s)

Por último, añadamos algo de CSS para que estas páginas parezcan que hemos invertido algún esfuerzo en su creación. (No soy una persona de front-end me temo, por lo que PRs son muy bienvenidos!)

Coge el style.css archivo del repositorio de Github e inclúyalo en su directorio static directorio.

Go necesita ser capaz de servir ese contenido CSS estático desde tu sistema de archivos local, por lo que necesitas decirle a tu aplicación cómo hacerlo.

En main.goimporte primero el módulo net/http y luego modifique su función main() como sigue:

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
		os.Exit(1)
	}

	apiKey := os.Getenv("VONAGE_API_KEY")
	apiSecret := os.Getenv("VONAGE_API_SECRET")

	auth := vonage.CreateAuthFromKeySecret(apiKey, apiSecret)
	verifyClient = vonage.NewVerifyClient(auth)

	mux := http.NewServeMux()
	mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))

}

Gestionar la sesión del usuario

Tenemos que pensar en cómo vamos a saber si un usuario se ha registrado en nuestro servicio para poder iniciar sesión o redirigirlo a la página de registro.

Para ello, vamos a depender de las cookies de sesión. Hay un gran módulo que podemos utilizar para gestionar las cookies y las sesiones del sistema de ficheros en Go: el módulo módulo de sesiones Gorilla.

Para instalarlo, ejecute:

go get github.com/gorilla/sessions

Añádelo a tu lista de importaciones en main.go y crea la cookie de sesión. Ya que estás ahí, declara también una struct llamado UserData que almacenará el nombre y el número de teléfono de un usuario registrado. Pasaremos esto struct a nuestra plantilla de página de inicio para que podamos mostrar esos detalles una vez que el usuario se haya registrado correctamente:

package main

import (
	"html/template"
	"log"
	"net/http"
	"os"

	"github.com/gorilla/sessions"
	"github.com/joho/godotenv"
	"github.com/vonage/vonage-go-sdk"
)

var (
	// Key must be 16, 24 or 32 bytes long (AES-128, AES-192 or AES-256)
	key   = []byte("super-secret-key")
	store = sessions.NewCookieStore(key)
)

// UserData - Info from session that we'll use in the UI
type UserData struct {
	Name  string
	Phone string
}

...

Definir las rutas

Ahora debemos considerar qué rutas debemos definir para gestionar nuestro flujo de trabajo. Vamos a crear las siguientes rutas:

  • El punto de entrada de nuestra aplicación (/). Esto mostrará la página de inicio (si el usuario está registrado), o redirigirá a la ruta /register (si no lo está).

  • En registro (/register). Mostrará el formulario de registro que, una vez enviado, iniciará el proceso de verificación llamando a la ruta /verify ruta.

  • El sitio verificación (/verify). Realiza una solicitud de Verify a la Verify API para enviar un código al dispositivo móvil del usuario. A continuación, redirige a la ruta /enter-code ruta.

  • El introducir código (/enter-code) presenta al usuario un formulario con un cuadro de texto en el que puede introducir el código PIN que ha recibido. Cuando envían el formulario, redirigimos a la ruta /check-code ruta.

  • El código de control ruta (check-code). Compara el código introducido con el enviado por la Verify API. Si coincide, almacenamos los datos del usuario en la sesión y redirigimos a la página de inicio (/). Si no hay coincidencia, registraremos un error en la consola. (Podríamos manejar esto con más elegancia, pero lo dejaré como ejercicio para el lector).

  • En borrar ruta (/clear). Esto borra la cookie de sesión para que pueda "dar de baja" a un usuario registrado. Útil para hacer pruebas.

¡Uf! Es bastante código para escribir. Afortunadamente, todo es bastante sencillo.

En primer lugar, defina algunos manejadores para las rutas en su función main e inicie su servidor a la escucha de peticiones entrantes:

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
		os.Exit(1)
	}

	apiKey := os.Getenv("VONAGE_API_KEY")
	apiSecret := os.Getenv("VONAGE_API_SECRET")

	auth := vonage.CreateAuthFromKeySecret(apiKey, apiSecret)
	verifyClient = vonage.NewVerifyClient(auth)

	mux := http.NewServeMux()
	mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
	mux.HandleFunc("/", home)
	mux.HandleFunc("/register", register)
	mux.HandleFunc("/verify", verify)
	mux.HandleFunc("/enter-code", enterCode)
	mux.HandleFunc("/check-code", checkCode)
	mux.HandleFunc("/clear", unregister)

	log.Println("Starting server on :5000")
	err = http.ListenAndServe(":5000", mux)
	log.Fatal(err)
}

El gestor de rutas de origen

Cree el home encima de la función main función. Este manejador se dispara cada vez que un usuario visita la raíz de su aplicación web en /.

Carga la cookie de sesión y comprueba si el registered valor de sesión. Si lo está, almacena la información de la cookie en una instancia de UserData y la utiliza para mostrar la página de inicio con el nombre y el número de teléfono del usuario.

Si no, redirige a la ruta /register ruta:

func home(w http.ResponseWriter, r *http.Request) {

	session, _ := store.Get(r, "acmeinc-cookie")

	// Check if user is authenticated
	if auth, ok := session.Values["registered"].(bool); !ok || !auth {
		// Not authenticated, so user must register
		http.Redirect(w, r, "/register", 302)
	}

	userData := UserData{
		Name:  fmt.Sprintf("%v", session.Values["name"]),
		Phone: fmt.Sprintf("%v", session.Values["phoneNumber"]),
	}

	files := []string{
		"./tmpl/home.page.gohtml",
		"./tmpl/base.layout.gohtml",
	}

	/* Use the template.ParseFiles() function to read the files and store the
	templates in a template set.*/
	ts, err := template.ParseFiles(files...)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
		return
	}

	err = ts.Execute(w, userData)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
	}
}

El gestor de rutas de registro

La ruta de registro muestra un formulario para que el usuario introduzca su nombre y número de teléfono para darse de alta en el servicio.

En primer lugar, añada fmt a la lista de importaciones. A continuación, cree el register encima de la función main función:

func register(w http.ResponseWriter, r *http.Request) {
	files := []string{
		"./tmpl/register.page.gohtml",
		"./tmpl/base.layout.gohtml",
	}

	ts, err := template.ParseFiles(files...)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
		return
	}

	err = ts.Execute(w, nil)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
	}
}

El gestor de rutas Verify

Cuando el usuario ha rellenado sus datos de registro y ha pulsado el botón "Registrarse", la aplicación llama a la /verify ruta. Es aquí donde creamos la petición inicial a la Verify API para generar un código y enviarlo al teléfono del usuario.

Cuando se envía una solicitud a la Verify API, ésta devuelve un icono request_id para identificar esa solicitud concreta. Necesitarás este ID cuando compruebes el código que introduce el usuario, así que guárdalo en una variable global:

package main

import (
	...
)

var (
	...
)

// UserData: Info from session that we'll use in the UI
type UserData struct {
	...
}

var verifyClient *vonage.VerifyClient
var requestID string

Iniciar esta solicitud es muy sencillo utilizando el SDK de Go. Llamamos al método VerifyClient.Request introduciendo el número de teléfono del usuario, el texto que queremos que aparezca en el SMS de verificación y algunas opciones para personalizar el proceso de verificación.

En este ejemplo, las opciones que estamos especificando son:

  • CodeLength: Esta es la longitud del código que enviaremos al usuario. Por defecto es de cuatro dígitos, pero estamos generando un código de seis dígitos.

  • Lg: Especifica el idioma en el que vamos a enviar el SMS de verificación, de la lista de idiomas disponibles.

  • WorkflowID: Verify API realiza varios intentos de enviar un código de verificación. Cómo y cuándo se realizan esos intentos depende del flujo de trabajo elegido. Estamos utilizando el flujo de trabajo con un ID de 4que envía el código por SMS y espera dos minutos a que se verifique. Si no se verifica después de ese tiempo, envía el código de nuevo y espera otros tres minutos antes de cancelar el proceso. Otros flujos de trabajo utilizan una combinación de SMS y llamadas de voz con conversión de texto a voz para enviar el código de verificación.

También vamos a almacenar la información que nuestro usuario introdujo en nuestra cookie de sesión.

Escriba el código del verify manejador de ruta por encima de su main función:

func verify(w http.ResponseWriter, r *http.Request) {

	session, _ := store.Get(r, "acmeinc-cookie")
	// retrieve user's name and phone number from the submitted form
	userName := r.URL.Query().Get("name")
	phoneNumber := r.URL.Query().Get("phone_number")
	session.Values["name"] = userName
	session.Values["phoneNumber"] = phoneNumber
	session.Save(r, w)
	log.Println("Verifying...." + userName + " at " + phoneNumber)
	response, errResp, err := verifyClient.Request(phoneNumber, "GoTest", vonage.VerifyOpts{CodeLength: 6, Lg: "en-gb", WorkflowID: 4})

	if err != nil {
		fmt.Printf("%#v\n", err)
	} else if response.Status != "0" {
		fmt.Println("Error status " + errResp.Status + ": " + errResp.ErrorText)
	} else {
		requestID = response.RequestId
		fmt.Println("Request started: " + response.RequestId)
		// redirect to "check" page
		http.Redirect(w, r, "/enter-code", 302)
	}
}

El gestor de rutas de introducción de código

Si todo ha ido según lo previsto, el usuario recibirá un SMS con un código PIN. Nuestra ruta /enter-code ruta proporcionará un formulario para que el usuario introduzca ese código.

Añada el enterCode a su aplicación:

func enterCode(w http.ResponseWriter, r *http.Request) {
	files := []string{
		"./tmpl/entercode.page.gohtml",
		"./tmpl/base.layout.gohtml",
	}

	ts, err := template.ParseFiles(files...)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
		return
	}

	err = ts.Execute(w, nil)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
	}
}

El gestor de rutas de código de control

Cuando el usuario ha introducido el código que recibió por SMS, tenemos que hacer una petición de verificación Verify para asegurarnos de que el código que ha introducido es el mismo que le enviaron. Para ello utilizamos el método VerifyClient.Check para ello, pasando el ID de la petición que hicimos en el verify y el código que han introducido en nuestra página.

Si el código coincide, establecemos el valor de registered cookie de sesión a true y redirigimos a la página de inicio. Si no, registraremos un mensaje de error en la consola:

func checkCode(w http.ResponseWriter, r *http.Request) {
	// Retrieve the PIN code that the user entered
	session, _ := store.Get(r, "acmeinc-cookie")
	pinCode := r.URL.Query().Get("pin_code")
	response, errResp, err := verifyClient.Check(requestID, pinCode)

	if err != nil {
		fmt.Printf("%#v\n", err)
	} else if response.Status != "0" {
		fmt.Println("Error status " + errResp.Status + ": " + errResp.ErrorText)
	} else {
		fmt.Println("Request complete: " + response.RequestId)
		// Set user as authenticated and return to home page
		session.Values["registered"] = true
		session.Save(r, w)
		http.Redirect(w, r, "/", 302)
	}
}

El gestor de rutas claro

Esto será útil para las pruebas. Te permite reiniciar el usuario haciendo una petición a http://localhost:5000/clearque borra la cookie de sesión.

Cree el unregister manejador de ruta como sigue:

func unregister(w http.ResponseWriter, r *http.Request) {
	// Delete the session
	session, _ := store.Get(r, "acmeinc-cookie")
	session.Options.MaxAge = -1
	session.Save(r, w)
	http.Redirect(w, r, "/", 302)
}

Pruébelo.

¡Ya ha terminado! Ahora es el momento de probar tu aplicación.

  1. Inicie la aplicación ejecutando go run main.go

  2. Visite http://localhost:5000 en su navegador

  3. Introduzca su nombre y número de teléfono, incluyendo el código de marcación pero omitiendo los ceros a la izquierda. Por ejemplo, el número de teléfono móvil del Reino Unido 07700900001 debe introducirse como 447700900001:

    Enter your name and phone numberRegistration page

  4. Recibirás un SMS con un código de seis dígitos:

    SMS containing PIN codeThe verification code, sent by SMS

  5. Introduce ese código en la aplicación:

    Enter the code into the appEntering the verificaiton code

  6. Se le registra y se le envía a la página de inicio:

    Acceder a la página de inicio]("Acceder a la página de inicio")

¿Y ahora qué?

Esperamos que hayas visto lo fácil que es utilizar Verify API y Go SDK para habilitar la autenticación de dos factores en tus aplicaciones. De hecho, ¡todo el proceso de verificación sólo ha requerido unas pocas líneas de código! La mayor parte del trabajo consistió en crear y gestionar la interfaz de usuario.

Nota: Si no has conseguido seguir el tutorial, puedes encontrar el código fuente completo de este tutorial en GitHub.

¿Crees que esta demo podría mejorar? Estoy de acuerdo. Estas son las que se me ocurren:

  • La salida no tan airosa cuando el usuario introduce un código erróneo. En su lugar, podría redirigir al usuario a la página de introducción del código y simultáneamente comprobar el progreso de la solicitude incluso cancelar una solicitud en curso después de dos intentos fallidos.

  • Formato en el que los usuarios deben introducir sus números de teléfono. Las API de Vonage esperan que los números de teléfono estén en formato E.164pero no hay razón por la cual debas imponerlo a tus usuarios. Puedes hacer que ingresen su número local y país desde una lista desplegable y usar la Number Insight API de Vonage para convertirlo al formato correcto. Esto también proporcionaría una práctica comprobación de que el número ingresado por el usuario es legítimo.

Esperamos que esté tan entusiasmado como nosotros con nuestro nuevo Go SDK. Para saber más sobre él y sobre Verify API, consulta los siguientes recursos:

Compartir:

https://a.storyblok.com/f/270183/384x384/637d0e41eb/marklewin.png
Mark LewinAntiguos alumnos de Vonage

Antiguo redactor técnico en Vonage. Le encanta jugar con las API y documentarlas.