https://d226lax1qjow5r.cloudfront.net/blog/blogposts/build-a-2fa-server-with-kotlin-and-ktor/kotlin_ktor_2fa_1200x600_white.png

Construir un servidor 2FA con Kotlin y Ktor

Publicado el January 20, 2021

Tiempo de lectura: 6 minutos

En este tutorial, usted escribirá un servidor que proporciona una API para Autenticación de dos factores (2FA). Esta API permitirá a los clientes de escritorio, clientes móviles y clientes web utilizar la autenticación de dos factores.

Para crear la aplicación, se utilizará el módulo Kotlin y Ktorun framework asíncrono para crear microservicios y aplicaciones web.

El código fuente completo está disponible en GitHub.

Requisitos previos

Para seguir este tutorial, necesitarás:

  • IntelliJ IDEA IDE instalado (de pago o gratuito, edición comunitaria).

  • Ktor para IntelliJ IDEA. Este plugin le permite crear un proyecto Ktor utilizando un nuevo asistente de proyecto. Abrir IntelliJ IDEAy vaya a Preferenciasy luego a Pluginse instale Ktor del mercado.

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.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

Crear un proyecto Ktor

  • Abrir IntelliJ IDEAy vaya a Archivo > Nuevo > Proyecto.

  • En el Nuevo proyecto seleccione el archivo Ktor en la parte izquierda y pulse el botón Siguiente Siguiente.

  • En la siguiente pantalla, deje los valores por defecto y pulse la tecla Siguiente Siguiente.

  • En la pantalla final, introduzca ktor-2fa-server como nombre de la aplicación y pulse el botón Finalizar .

Ha creado un proyecto de aplicación Ktor.

Primer punto final

Abra el src/Application.kt y añada un nuevo routing para verificar que la aplicación funciona:

fun Application.module(testing: Boolean = false) {
    routing {
        get("/") {
            call.respondText("2FA app is working", ContentType.Text.Html)
        }
    }
}

En este tutorial, todo el código de la aplicación Ktor se almacenará en el archivo Application.kt archivo.

Haga clic en la flecha verde situada junto a la función main para ejecutar la aplicación (esto creará una nueva configuración de ejecución en el IDE):

Run app

Navegue hasta http://localhost:8080/ en su navegador para comprobar si la aplicación funciona correctamente: debería aparecer "2FA app is working":

App is working

Modo de desarrollo

Activar el modo de desarrollo permite que la aplicación Ktor muestre información de depuración más detallada en el IDE, como la pila de llamadas. Ayudará con el desarrollo y el diagnóstico de problemas.

Abra el resources/application.conf y añada development = true:

ktor {
    development = true

    ...

Añadir dependencias

SDK Java de Vonage

El lenguaje Kotlin proporciona interoperabilidad con Javalo que te permite llamar a código Java desde código Kotlin para que puedas utilizar Vonage Java SDK para el proyecto Kotlin/Ktor.

Abra el archivo build.gradle y añada la siguiente dependencia:

dependencies {

    ...

    implementation 'com.vonage:client:6.1.0'
}

Serialización

Utilizarás JSON como formato de datos para comunicarte con los clientes. Serializarás objetos Kotlin usando serialización Kotlin.

Abra el archivo build.gradle y añada las siguientes dependencias:

dependencies {

    ...
    
    implementation "io.ktor:ktor-serialization:$ktor_version"
    implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1'
}

La librería de serialización de Kotlin utiliza preprocesamiento (en tiempo de compilación), por lo que tienes que añadir el plugin de org.jetbrains.kotlin.plugin.serialization plugin de Gradle. En el momento de escribir este artículo, Ktor está usando usando la antigua forma de aplicar los plugins de Gradlepor lo que tenemos que sustituirla por la nueva configuración.

Abra el archivo build.gradle y elimine los plugins:

apply plugin: 'kotlin'
apply plugin: 'application'

Quitar el mainClassName:

mainClassName = "io.ktor.server.netty.EngineMain"

Quitar el classpath:

dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}

Añade plugins usando la nueva sintaxis de Gradle, justo debajo de buildscript bloque:

buildscript {
    // ...
}

plugins {
    id "java"
    id "org.jetbrains.kotlin.jvm" version "$kotlin_version"
    id "org.jetbrains.kotlin.plugin.serialization" version "$kotlin_version"
}

Después de todas las modificaciones, el archivo build.gradle debería tener este aspecto:

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

plugins {
    id "java"
    id "org.jetbrains.kotlin.jvm" version "$kotlin_version"
    id "org.jetbrains.kotlin.plugin.serialization" version "$kotlin_version"
}

group 'com.example'
version '0.0.1'

sourceSets {
    main.kotlin.srcDirs = main.java.srcDirs = ['src']
    test.kotlin.srcDirs = test.java.srcDirs = ['test']
    main.resources.srcDirs = ['resources']
    test.resources.srcDirs = ['testresources']
}

repositories {
    mavenLocal()
    jcenter()
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation "io.ktor:ktor-server-netty:$ktor_version"
    implementation "ch.qos.logback:logback-classic:$logback_version"
    testImplementation "io.ktor:ktor-server-tests:$ktor_version"

    implementation 'com.vonage:client:6.1.0'
    implementation "io.ktor:ktor-serialization:$ktor_version"
    implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1'
}

En kotlin_version y ktor_version se definen dentro del archivo gradle.properties archivo.

Para habilitar serializatin, el convertidor JSON tiene que estar habilitado para la aplicación Ktor. Abra el archivo Application.kt y añada un bloque install dentro del bloque Application.module función:

fun Application.module(testing: Boolean = false) {

    install(ContentNegotiation) {
        json()
    }
    
    // ...
}

El IDE marcará con el color rojo todas las clases y extensiones que tengan import missing. Pase el ratón sobre el nombre de la clase o método, espere a que aparezca una ventana y seleccione import... para añadir la importación de la clase y corregir el error.

Crear una aplicación de Vonage

Una aplicación de Vonage proporcionará capacidades 2FA para la API. Crea una aplicación de Vonage en el tablero. Haz clic en el botón Crear una nueva aplicación ingresa un nombre y haz clic en el botón Generar nueva aplicación .

Ir a configuración y anota API key y API secret.

Inicializar cliente de Vonage

Añada la propiedad client dentro de la función Application.module para inicializar un cliente de Vonage:

fun Application.module(testing: Boolean = false) {

    val client: VonageClient = VonageClient.builder()
        .apiKey("API_KEY")
        .apiSecret("API_SECRET")
        .build()

    install(ContentNegotiation) {
        json()
    }

    // ...
}

Sustituya API_KEY y API_SECRET utilizando los valores del cuadro de mandos.

NOTA: en producción API_KEY y API_SECRET deben recuperarse de variables de entorno.

Funcionalidad API

Construirá dos puntos finales de API:

  • verifyNumber - el cliente accederá primero a este punto final para iniciar el proceso de verificación procesando el número de teléfono que debe verificarse.

  • verifyCode - tras recibir el código (por SMS o llamada de voz), el cliente enviará el código y la aplicación realizará una comprobación 2FA para determinar si el cliente está verificado.

Creación del punto final de la API verifyNumber

Definir un nuevo manejador de ruta, get("/verifyNumber")dentro del bloque routing de la función Application.module función:

fun Application.module(testing: Boolean = false) {

    // ...

    routing {
        get("/") {
            call.respondText("2FA app is working", ContentType.Text.Html)
        }
        get("/verifyNumber") {
            // ...
        }
    }
}

El código dentro del get("/verifyNumber") manejador de ruta se ejecutará cuando el cliente haga una llamada a la http://localhost:8080/verifyNumber URL.

El punto final verifyNumber endpoint contendrá la siguiente lógica:

  • recuperar phoneNumber de la cadena de consulta (http://localhost:8080/verifyNumber?phoneNumber=1234)

  • iniciar la verificación 2FA con el SDK de Vonage

  • devuelve requestId como JSON (en una aplicación de producción, normalmente almacenaría el ID en el lado del servidor)

Añada la siguiente lógica al get("/verifyNumber") controlador de ruta:

get("/verifyNumber") {
    val phoneNumber = call.parameters["phoneNumber"]
    require(!phoneNumber.isNullOrBlank()) { "phoneNumber is missing" }

    val ongoingVerify = client.verifyClient.verify(phoneNumber, "VONAGE")

    val response = VerifyNumberResponse(ongoingVerify.requestId)
    call.respond(response)
}

Define una VerifyNumberResponse que se serializará a JSON y se devolverá al cliente de la API. Añada el siguiente código al final de Application.kt del archivo:

@Serializable
data class VerifyNumberResponse(val requestId: String)

Kotlin permite definir múltiples miembros de nivel superior (clases, propiedades, etc.) dentro de un único archivo.

Debido a un error en el plugin de Kotlin, es necesario añadir la sentencia import para la anotación Serializable manualmente. Añade el siguiente código en la parte superior del archivo, justo debajo de la última sentencia import:

import kotlinx.serialization.Serializable

En lugar de usar la verificación incorporada de Vonage, podrías generar el código tú mismo y enviar un SMS usando Vonage Java SDK. Sin embargo, el mecanismo de verificación de Vonage ofrece una manera sencilla de utilizar flujos de trabajopor ejemplo: el flujo de trabajo predeterminado realizará una llamada telefónica y leerá el código al usuario si el cliente no proporcionó el código SMS dentro de un período específico.

Crear el punto final de la API verifyCode

Definir un nuevo manejador de ruta, get("/verifyCode")dentro del bloque routing de la función Application.module función:

fun Application.module(testing: Boolean = false) {

    // ...

    routing {
        // ...
        get("/verifyCode") {
            // ...
        }
    }
}

El punto final verifyCode endpoint contendrá la siguiente lógica:

  • recuperar code de la cadena de consulta (code se entregará al usuario después de pulsar el botón verifyNumber punto final)

  • recuperar un parámetro de verificación requestId de la cadena de consulta (valor recuperado de verifyNumber endpoint)

  • Verifica el código usando Vonage SDK

  • devolver al cliente el estado de verificación

Añada la siguiente lógica al get("/verifyCode") controlador de ruta:

get("/verifyCode") {
    val code = call.parameters["code"]
    val requestId = call.parameters["requestId"]

    val checkResponse = client.verifyClient.check(requestId, code)
    println(checkResponse.status)

    val status = if(checkResponse.status == VerifyStatus.OK) {
        "OK"
    } else {
        "ERROR: ${checkResponse.status}"
    }

    val response = VerifyCodeResponse(status)
    call.respond(response)
}

Define una VerifyCodeResponse que se serializará a JSON y se devolverá al cliente de la API. Añada el siguiente código al final de Application.kt del archivo:

@Serializable
data class VerifyCodeResponse(val status: String)

Después de todas las modificaciones, Application.kt el archivo debería verse así:

package com.example

import com.vonage.client.VonageClient
import com.vonage.client.verify.VerifyStatus
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.serialization.*
import kotlinx.serialization.Serializable

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {

    val client: VonageClient = VonageClient.builder()
        .apiKey("API_KEY")
        .apiSecret("API_KEY")
        .build()

    install(ContentNegotiation) {
        json()
    }

    routing {
        get("/") {
            call.respondText("2FA app is working", ContentType.Text.Html)
        }
        get("/verifyNumber") {
            val phoneNumber = call.parameters["phoneNumber"]
            require(!phoneNumber.isNullOrBlank()) { "phoneNumber is missing" }

            val ongoingVerify = client.verifyClient.verify(phoneNumber, "VONAGE")
            val response = VerifyNumberResponse(ongoingVerify.requestId)
            call.respond(response)
        }
        get("/verifyCode") {
            val code = call.parameters["code"]
            val requestId = call.parameters["requestId"]

            val checkResponse = client.verifyClient.check(requestId, code)
            println(checkResponse.status)

            val status = if(checkResponse.status == VerifyStatus.OK) {
                "OK"
            } else {
                "ERROR: ${checkResponse.status}"
            }

            val response = VerifyCodeResponse(status)
            call.respond(response)
        }
    }
}

@Serializable
data class VerifyNumberResponse(val requestId: String)

@Serializable
data class VerifyCodeResponse(val status: String)

Utilizar la API

La implementación de la API está completa, así que vamos a probarla.

Cualquier cliente puede utilizar la API, incluidos los clientes de escritorio y móviles, pero usted realizará pruebas sencillas utilizando un navegador web.

Inicie la aplicación Ktor.

Sustituya PHONE_NUMBER por un número de teléfono real y abra la siguiente URL en el navegador:

http://localhost:8080/verifyNumber?phoneNumber=PHONE_NUMBER

Los números de teléfono de Vonage están en E.164 '+' y '-' no son válidos. Asegúrate de especificar el código de tu país al ingresar tu número, por ejemplo, US: 14155550100 y Reino Unido: 447700900001

Como usuario de pruebasólo podrás enviar SMS y realizar llamadas de voz al número con el que te registraste y a otros 4 números de prueba de tu elección (puedes recargar tu cuenta de Vonage para eliminar esta restricción).

Deberías recibir un SMS con un código y ver una respuesta similar:

{"requestId":"9ac76db7971b4ea4a49f2e061432c6fe"}

Redacte una segunda solicitud. Sustituya REQUEST_ID por el valor devuelto por el servidor (en el ejemplo anterior, es 9ac76db7971b4ea4a49f2e061432c6fe) y sustituya CODE por el código de verificación recibido:

http://localhost:8080/verifyCode?requestId=REQUEST_ID&code=CODE

Si el número de teléfono del cliente está verificado, debería ver la siguiente respuesta:

{"status":"OK"}

Estás usando un flujo de trabajo de verificación predeterminado de Vonage (/verify/verify-v1/guides/workflows-and-events), por lo que si no ingresas el código dentro de los 125 segundos, recibirás la llamada de voz que lee el código.

Lecturas complementarias

Puede encontrar el código que se muestra en este tutorial en la página de Github.

A continuación encontrarás otros tutoriales que hemos escrito sobre el uso de nuestros servicios con Go:

Si tienes alguna pregunta, consejo o idea que quieras compartir con la comunidad, no dudes en entrar en nuestro espacio de trabajo espacio de trabajo Slack de la comunidad. Me encantaría saber de alguien que haya implementado este tutorial y cómo funciona su proyecto.

Compartir:

https://a.storyblok.com/f/270183/384x384/8ae5af43bb/igor-wojda.png
Igor WojdaAntiguos alumnos de Vonage