15. Autenticacion

En esta seccion veremos como autenticar paticiones a nuestra API,para saber exactamente de que usurio particular viene cada peticion.

Lo que haremos:

  • Presentaremos los enfoques posibles para la autenticación de API que podríamos usar, y discutiremos sus respectivas ventajas y desventajas.
  • Implementaremos un patrón de autenticación basado en tokens con estado, que permite a los clientes intercambiar sus credenciales de usuario por un token de autenticación con límite de tiempo que identifica quiénes son.

15.1 Opciones de autenticación

Antes de comenzar a escribir cualquier código, hablemos sobre cómo vamos a autenticar las solicitudes a nuestra API y averiguar de qué usuario proviene una solicitud.

Elegir un enfoque de alto nivel para la autenticación de API puede ser complicado; existen muchas opciones diferentes y no siempre es claro de inmediato cuál es la más adecuada para tu proyecto. Por lo tanto, en este capítulo discutiremos algunos de los enfoques más comunes a un nivel alto, analizaremos sus ventajas y desventajas relativas, y concluiremos con algunas pautas generales sobre cuándo es apropiado usarlos.

Específicamente, los cinco enfoques que compararemos son:

  • Basic authentication
  • Stateful token authentication
  • Stateless token authentication
  • API key authentication
  • OAuth 2.0 / OpenID Connect

Importante: Para todos los métodos de autenticación que describimos en este capítulo, se asume que tu API solo se comunica con clientes a través de HTTPS.

15.1 HTTP basic authentication

Quizás la forma más simple de determinar quién está realizando una solicitud a tu API sea utilizando la autenticación básica de HTTP.

Con este método, el cliente incluye un encabezado de Autorización con cada solicitud. que contiene sus credenciales. Las credenciales deben tener el formato nombre de usuario:contraseña y codificado en base 64. Entonces, por ejemplo, para autenticarse como __alice@example.com:pa55word__ el El cliente enviaría el siguiente encabezado:

Authorization: Basic YWxpY2VAZXhhbXBsZS5jb206cGE1NXdvcmQ=

En su API, puede extraer las credenciales de este encabezado utilizando el método Go’s Request.BasicAuth() y verificar que sean correctas antes de continuar con el procesamiento. la solicitud.

Una gran ventaja de la autenticación básica de HTTP es lo simple que es para los clientes. Pueden enviar el mismo encabezado con cada solicitud, y la autenticación básica de HTTP es compatible de manera predeterminada con la mayoría de los lenguajes de programación, navegadores web y herramientas como curl y wget. A menudo es útil en escenarios donde tu API no tiene cuentas de usuario “reales”, pero deseas una forma rápida y sencilla de restringir el acceso a ella o protegerla de miradas indiscretas.

Para las API con cuentas de usuario “reales” y, en particular, contraseñas hash, no es tan adecuada. Comparar la contraseña proporcionada por un cliente con una contraseña hash (lenta) es una operación deliberadamente costosa, y al usar la autenticación básica de HTTP, necesitas hacer esa verificación para cada solicitud. Esto creará mucho trabajo adicional para tu servidor de API y agregará una latencia significativa a las respuestas.

Pero incluso entonces, la autenticación básica aún puede ser una buena opción si el tráfico a tu API es muy bajo y la velocidad de respuesta no es importante para ti.

15.1 Token authentication

La idea de alto nivel detrás de la autenticación de token (también conocida a veces como autenticación de bearer toknen) funciona de la siguiente manera:

  1. El cliente envía una solicitud a tu API que contiene sus credenciales (generalmente nombre de usuario o dirección de correo electrónico, y contraseña).

  2. La API verifica que las credenciales sean correctas, genera un token de portador que representa al usuario y lo envía de vuelta al usuario. El token expira después de un período de tiempo establecido, después del cual el usuario deberá volver a enviar sus credenciales para obtener un nuevo token.

  3. Para solicitudes posteriores a la API, el cliente incluye el token en un encabezado de Autorización de esta manera:

    Authorization: Bearer <token>
  4. Cuando tu API recibe esta solicitud, verifica que el token no haya caducado y examina el valor del token para determinar quién es el usuario.

Para APIs donde las contraseñas de usuario están hasheadas (como la nuestra), este enfoque es mejor que la autenticación básica porque significa que la comprobación lenta de contraseñas solo debe hacerse periódicamente, ya sea al crear un token por primera vez o después de que un token haya caducado.

La desventaja es que la gestión de tokens puede ser complicada para los clientes; necesitarán implementar la lógica necesaria para almacenar en caché los tokens, monitorear y gestionar la expiración de los tokens, y generar periódicamente nuevos tokens.

Podemos desglosar aún más la autenticación de tokens en dos subtipos: autenticación de token con estado(stateful) y autenticación de token sin estado(stateless). Son bastante diferentes en cuanto a sus pros y contras, así que discutamos cada uno por separado.

15.1 Stateful token authentication

En un enfoque de token con estado, el valor del token es una cadena aleatoria criptográficamente segura de alta entropía. Este token, o un hash rápido del mismo, se almacena en el servidor en una base de datos, junto con el ID de usuario y un tiempo de caducidad para el token.

Cuando el cliente envía de vuelta el token en solicitudes posteriores, tu aplicación puede buscar el token en la base de datos, verificar que no haya caducado y recuperar el ID de usuario correspondiente para averiguar de quién proviene la solicitud. La gran ventaja de esto es que tu API mantiene el control sobre los tokens: es sencillo revocar los tokens de manera individual o por usuario eliminándolos de la base de datos o marcándolos como caducados.

Conceptualmente, también es simple y robusto: la seguridad la proporciona el hecho de que el token sea ‘impredecible’, por eso es importante usar un valor aleatorio criptográficamente seguro de alta entropía para el token.

Entonces, ¿cuáles son los inconvenientes? Más allá de la complejidad para los clientes que es inherente a la autenticación por token en general, es difícil encontrar mucho que criticar sobre este enfoque. Quizás el hecho de que requiera una búsqueda en la base de datos sea negativo, pero en la mayoría de los casos necesitarás hacer una búsqueda en la base de datos para verificar el estado de activación del usuario o recuperar información adicional sobre ellos de todos modos.

15.1 Stateless token authentication

En contraste, los tokens sin estado codifican el ID de usuario y el tiempo de expiración en el propio token. El token está firmado criptográficamente para evitar manipulaciones y, en algunos casos, cifrado para evitar que el contenido sea leído.

Existen varias tecnologías diferentes que puedes utilizar para crear tokens sin estado. Codificar la información en un JWT (JSON Web Token) es probablemente el enfoque más conocido, pero PASETO, Branca y nacl/secretbox también son alternativas viables. Aunque los detalles de implementación de estas tecnologías son diferentes, los pros y los contras generales en términos de autenticación son similares.

El principal argumento a favor de utilizar tokens sin estado para la autenticación es que el trabajo de codificar y decodificar el token puede realizarse en la memoria, y toda la información necesaria para identificar al usuario está contenida dentro del propio token. No es necesario realizar una búsqueda en la base de datos para averiguar de quién proviene una solicitud.

La principal desventaja de los tokens sin estado es que no pueden ser revocados fácilmente una vez emitidos. En caso de emergencia, podrías revocar efectivamente todos los tokens cambiando el secreto utilizado para firmar tus tokens (forzando a todos los usuarios a volver a autenticarse). Otra solución temporal es mantener una lista de bloqueo de tokens revocados en una base de datos, aunque esto contradice el aspecto ‘sin estado’ de tener tokens sin estado.

Nota: En general, se debe evitar almacenar información adicional en un token sin estado, como el estado de activación o permisos de un usuario, y utilizarlo como base para verificaciones de autorización. Durante la vida útil del token, la información codificada en él podría volverse obsoleta y desincronizarse con los datos reales en tu sistema. Dependiendo de datos obsoletos para las verificaciones de autorización puede conducir fácilmente a comportamientos inesperados para los usuarios y a diversos problemas de seguridad.

Finalmente, con los JWT en particular, el hecho de que sean altamente configurables significa que hay muchas cosas que se pueden hacer mal. Los artículos sobre vulnerabilidades críticas en las bibliotecas de Tokens Web JSON y las Mejores Prácticas de Seguridad en JWT proporcionan una buena introducción al tipo de cosas de las que debes tener cuidado aquí.

Debido a estas desventajas, los tokens sin estado, y los JWT en particular, generalmente no son la mejor opción para gestionar la autenticación en la mayoría de las aplicaciones de API. Sin embargo, pueden ser muy útiles en un escenario donde se necesita autenticación delegada, es decir, cuando la aplicación que crea el token de autenticación es diferente a la que lo consume, y esas aplicaciones no comparten ningún estado (lo que significa que usar tokens con estado no es una opción). Por ejemplo, si estás construyendo un sistema que tiene una arquitectura de estilo microservicios en segundo plano, entonces un token sin estado creado por un servicio de “autenticación” puede pasar posteriormente a otros servicios para identificar al usuario.

15.1 API-key authentication

La idea detrás de la autenticación mediante clave de API es que un usuario tiene una ‘clave’ secreta no caducada asociada con su cuenta. Esta clave debe ser una cadena aleatoria criptográficamente segura de alta entropía, y un hash rápido de la clave (SHA256 o SHA512) debe almacenarse junto con el ID de usuario correspondiente en tu base de datos.

El usuario luego pasa su clave con cada solicitud a tu API en un encabezado como este: Authorization: Key <clave>. Al recibirlo, tu API puede regenerar el hash rápido de la clave y usarlo para buscar el ID de usuario correspondiente en tu base de datos.

Conceptualmente, esto no está muy lejos del enfoque de token con estado: la principal diferencia es que las claves son claves permanentes, en lugar de tokens temporales. Por un lado, esto es conveniente para el cliente, ya que pueden usar la misma clave para cada solicitud y no necesitan escribir código para gestionar tokens o caducidad. Por otro lado, el usuario ahora tiene dos secretos de larga duración para gestionar, lo que potencialmente podría comprometer su cuenta: su contraseña y su clave de API.

El soporte para claves de API también agrega complejidad adicional a tu aplicación de API: necesitarás una forma para que los usuarios regeneren su clave de API si la pierden o si la clave se ve comprometida, y también puedes desear admitir múltiples claves de API para el mismo usuario, para que puedan usar diferentes claves para diferentes propósitos.

También es importante tener en cuenta que las claves de API mismas solo deben comunicarse a los usuarios a través de un canal seguro, y debes tratarlas con el mismo nivel de cuidado que tratarías la contraseña de un usuario.

15.1 OAuth 2.0 / OpenID Connect

Otra opción es aprovechar OAuth 2.0 para la autenticación. Con este enfoque, la información sobre tus usuarios (y sus contraseñas) se almacena en un proveedor de identidad de terceros como Google o Facebook en lugar de en tu propia aplicación.

Lo primero que hay que mencionar aquí es que OAuth 2.0 no es un protocolo de autenticación y realmente no deberías usarlo para autenticar usuarios. El sitio web oauth.net tiene un excelente artículo explicando esto, y recomiendo encarecidamente leerlo. Si deseas implementar verificaciones de autenticación contra un proveedor de identidad de terceros, deberías utilizar OpenID Connect (que se construye directamente sobre OAuth 2.0).

Hay una visión general completa de OpenID Connect aquí, pero a un nivel muy alto, funciona así: Cuando deseas autenticar una solicitud, rediriges al usuario a un formulario de ‘autenticación y consentimiento’ alojado por el proveedor de identidad. Si el usuario da su consentimiento, entonces el proveedor de identidad envía a tu API un código de autorización. Tu API luego envía el código de autorización a otro punto final proporcionado por el proveedor de identidad. Ellos verifican el código de autorización, y si es válido, te enviarán una respuesta JSON que contiene un token de identificación. Este token de identificación es en sí mismo un JWT. Necesitas validar y decodificar este JWT para obtener la información real del usuario, que incluye cosas como su dirección de correo electrónico, nombre, fecha de nacimiento, zona horaria, etc.

Ahora que sabes quién es el usuario, puedes implementar un patrón de token de autenticación con estado o sin estado para que no tengas que pasar por todo el proceso para cada solicitud posterior.

Al igual que con todas las demás opciones que hemos analizado, hay ventajas y desventajas en el uso de OpenID Connect. La gran ventaja es que no necesitas almacenar información de usuario o contraseñas de forma persistente. La gran desventaja es que es bastante complejo, aunque hay algunos paquetes auxiliares como coreos/go-oidc que hacen un buen trabajo enmascarando esa complejidad y proporcionando una interfaz simple para el flujo de trabajo de OpenID Connect al que puedes conectar.

También es importante señalar que el uso de OpenID Connect requiere que todos tus usuarios tengan una cuenta con el proveedor de identidad, y el paso de ‘autenticación y consentimiento’ requiere interacción humana a través de un navegador web, lo cual probablemente está bien si tu API es el backend para un sitio web, pero no es ideal si es una API “independiente” con otros programas informáticos como clientes.

15.1 Que metodo utilizaremos?

Es difícil dar orientación general sobre qué enfoque de autenticación es mejor usar para tu API. Como ocurre con la mayoría de las cosas en programación, diferentes herramientas son apropiadas para diferentes tareas. Pero como reglas generales simples y aproximadas:

  • Si tu API no tiene cuentas de usuario “reales” con hashes de contraseñas lentos, entonces la autenticación básica de HTTP puede ser una opción buena y a menudo pasada por alto.
  • Si no deseas almacenar contraseñas de usuario tú mismo, todos tus usuarios tienen cuentas con un proveedor de identidad de terceros que admite OpenID Connect, y tu API es el backend de un sitio web… entonces utiliza OpenID Connect.
  • Si necesitas autenticación delegada, como cuando tu API tiene una arquitectura de microservicios con diferentes servicios para realizar la autenticación y realizar otras tareas, entonces utiliza tokens de autenticación sin estado.
  • De lo contrario, utiliza claves de API o tokens de autenticación con estado. En general:
    • Los tokens de autenticación con estado son adecuados para APIs que actúan como el backend de un sitio web o aplicación de página única, ya que hay un momento natural cuando el usuario inicia sesión donde pueden ser intercambiados por credenciales de usuario.
    • En contraste, las claves de API pueden ser mejores para APIs más “de propósito general” porque son permanentes y más simples para que los desarrolladores las utilicen en sus aplicaciones y scripts.

En el resto de este post, vamos a implementar la autenticación utilizando el patrón de token de autenticación con estado. En nuestro caso, ya hemos construido gran parte de la lógica necesaria para esto como parte de nuestro trabajo con tokens de activación.

Nota: Aunque realmente no recomiendo usar JWTs a menos que necesites alguna forma de autenticación delegada, soy consciente de que tienen una gran presencia en la comunidad de desarrolladores y a menudo se utilizan más ampliamente que eso.

15.2 Generando token de autenticacion

En este capítulo nos centraremos en desarrollar el código para un nuevo punto final POST/v1/tokens/authentication, el cual permitirá a un cliente intercambiar sus credenciales (dirección de correo electrónico y contraseña) por un token de autenticación persistente.

Nota: Para mayor concisión, en lugar de repetir las palabras “token de autenticación persistente” a lo largo del resto de este post, a partir de ahora simplemente nos referiremos a esto como el token de autenticación del usuario.

A alto nivel, el proceso para intercambiar las credenciales de un usuario por un token de autenticación funcionará de la siguiente manera:

  1. El cliente envía una solicitud JSON a un nuevo punto final POST/v1/tokens/authentication que contiene sus credenciales (correo electrónico y contraseña).
  2. Buscamos el registro del usuario basado en el correo electrónico y verificamos si la contraseña proporcionada es la correcta para el usuario. Si no lo es, enviamos una respuesta de error.
  3. Si la contraseña es correcta, utilizamos nuestro método app.models.Tokens.New() para generar un token con un tiempo de expiración de 24 horas y el alcance autenticación.
  4. Enviamos este token de autenticación de vuelta al cliente en el cuerpo de una respuesta JSON.

Comencemos en nuestro archivo interno/de datos/tokens.go.

Necesitamos actualizar este archivo para definir un nuevo alcance de “autenticación” y agregar algunas etiquetas de estructura para personalizar cómo aparece la estructura Token cuando se codifica a JSON. Sería algo así:

package data

const (
    ScopeActivation     = "activation"
    ScopeAuthentication = "authentication" // Include a new authentication scope.
)

// Add struct tags to control how the struct appears when encoded to JSON.
type Token struct {
    Plaintext string `json:"token"`
    Hash      []byte `json:"-"`
    UserID    []byte `json:"-"`
    Expiry    time.Time `json:"expiry"`
    Scope     string `json:"-"`
}

Estas nuevas etiquetas de estructura significan que solo los campos Plaintext y Expiry se incluirán al codificar una estructura Token; todos los demás campos se omitirán.

También renombramos el campo Plaintext a “token”, simplemente porque es un nombre más significativo para los clientes que ‘plaintext’.

En conjunto, esto significa que cuando codificamos una estructura Token a JSON, el resultado se verá similar a esto:

{
    "token": "some_token_value",
    "expiry": "2024-01-26T12:00:00Z"
}

15.2 Contruyendo el endpoit

Ahora vamos a entrar en el meollo de este capítulo y configurar todo el código para el nuevo endpoint POST /v1/tokens/authentication. Para cuando terminemos, nuestras rutas de API se verán así:

MethodURL PatternHandlerAction
GET/v1/healthcheckhealthcheckHandlerShow application information
GET/v1/movieslistMoviesHandlerShow the details of all movies
POST/v1/moviescreateMovieHandlerCreate a new movie
GET/v1/movies/:idshowMovieHandlerShow the details of a specific movie
PATCH/v1/movies/:idupdateMovieHandlerUpdate the details of a specific movie
DELETE/v1/movies/:iddeleteMovieHandlerDelete a specific movie
POST/v1/usersregisterUserHandlerRegister a new user
PUT/v1/users/activatedactivateUserHandlerActivate a specific user
POST/v1/tokens/authenticationcreateAuthenticationTokenHandlerGenerate a new authentication token

Si estás siguiendo, adelante y crea un nuevo archivo cmd/api/tokens.go:

$ touch cmd/api/tokens.go

Y en este nuevo archivo agregaremos el código para el createAuthenticationTokenHandler.

Básicamente, queremos que este controlador intercambie la dirección de correo electrónico y la contraseña del usuario por un token de autenticación, así:

package main

import (
    "errors"
    "net/http"
    "time"

    "greenlight.alexedwards.net/internal/data"
    "greenlight.alexedwards.net/internal/validator"
)

func (app *application) createAuthenticationTokenHandler(w http.ResponseWriter, r *http.Request) {
    // Parse the email and password from the request body.
    var input struct {
        Email    string `json:"email"`
        Password string `json:"password"`
    }
    err := app.readJSON(w, r, &input)
    if err != nil {
        app.badRequestResponse(w, r, err)
        return
    }

    // Validate the email and password provided by the client.
    v := validator.New()
    data.ValidateEmail(v, input.Email)
    data.ValidatePasswordPlaintext(v, input.Password)
    if !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }

    // Lookup the user record based on the email address. If no matching user was
    // found, then we call the app.invalidCredentialsResponse() helper to send a 401
    // Unauthorized response to the client (we will create this helper in a moment).
    user, err := app.models.Users.GetByEmail(input.Email)
    if err != nil {
        switch {
        case errors.Is(err, data.ErrRecordNotFound):
            app.invalidCredentialsResponse(w, r)
        default:
            app.serverErrorResponse(w, r, err)
        }
        return
    }

    // Check if the provided password matches the actual password for the user.
    match, err := user.Password.Matches(input.Password)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    // If the passwords don't match, then we call the app.invalidCredentialsResponse()
    // helper again and return.
    if !match {
        app.invalidCredentialsResponse(w, r)
        return
    }

    // Otherwise, if the password is correct, we generate a new token with a 24-hour
    // expiry time and the scope 'authentication'.
    token, err := app.models.Tokens.New(user.ID, 24*time.Hour, data.ScopeAuthentication)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    // Encode the token to JSON and send it in the response along with a 201 Created
    // status code.
    err = app.writeJSON(w, http.StatusCreated, envelope{"authentication_token": token}, nil)
    if err != nil {
        app.serverErrorResponse(w, r, err)
    }
}

Vamos a crear rápidamente el ayudante invalidCredentialsResponse() en nuestro archivo cmd/api/errors.go también:

package main

...

func (app *application) invalidCredentialsResponse(w http.ResponseWriter, r *http.Request) {
    message := "credenciales de autenticación inválidas"
    app.errorResponse(w, r, http.StatusUnauthorized, message)
}

Por último, necesitamos incluir el endpoint POST /v1/tokens/authentication en nuestras rutas de la aplicación. Sería así:

package main

...

func (app *application) routes() http.Handler {
    router := httprouter.New()
    router.NotFound = http.HandlerFunc(app.notFoundResponse)
    router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)
    
    router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)
    router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler)
    router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
    router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)
    router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler)
    router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler)
    router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler)
    router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler)
    
    // Add the route for the POST /v1/tokens/authentication endpoint.
    router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler)
    
    return app.recoverPanic(app.rateLimit(router))
}

Con todo eso completado, ahora deberíamos poder generar un token de autenticación.

Adelante y haz una solicitud al nuevo endpoint POST /v1/tokens/authentication con una dirección de correo electrónico y contraseña válidas para uno de los usuarios que hayas creado previamente.

Deberías recibir una respuesta 201 Created y un cuerpo JSON que contenga un token de autenticación, similar a esto:

$ BODY='{"email": "alice@example.com", "password": "pa55word"}'
$ curl -i -d "$BODY" localhost:4000/v1/tokens/authentication
HTTP/1.1 201 Created
Content-Type: application/json
Date: Fri, 16 Apr 2021 09:03:36 GMT
Content-Length: 125

{
  "authentication_token": {
    "token": "IEYZQUBEMPPAKPOAWTPV6YJ6RM",
    "expiry": "2021-04-17T11:03:36.767078518+02:00"
  }
}

Por el contrario, si intentas hacer una solicitud con una dirección de correo electrónico bien formada pero desconocida, o una contraseña incorrecta, deberías recibir una respuesta de error. Por ejemplo:

$ BODY='{"email": "alice@example.com", "password": "wrong pa55word"}'
$ curl -i -d "$BODY" localhost:4000/v1/tokens/authentication
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Date: Fri, 16 Apr 2021 09:54:01 GMT
Content-Length: 51

{
  "error": "credenciales de autenticación inválidas"
}

Antes de continuar, echemos un vistazo rápido a la tabla de tokens en nuestra base de datos PostgreSQL para verificar que se haya creado el token de autenticación.

$ psql $GREENLIGHT_DB_DSN
Password for user greenlight:
psql (15.4 (Ubuntu 15.4-1.pgdg22.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
greenlight=> SELECT * FROM tokens WHERE scope = 'authentication';
\gx
4390d2ff4af7346dd4238ffccb8a5b18e8c3af9aa8cf57852895ad0f8ee2c50d | 1 | 2021-04-17 11:03:37+02 | authentication

Eso se ve bien. Podemos ver que el token está asociado con el usuario con ID 1 (que si has estado siguiendo será el usuario alice@example.com) y tiene el alcance correcto y el tiempo de caducidad.

15.2 Informacion adicional
15.2 The Authorization healthcheckHandler

Ocasionalmente podrías encontrarte con otras APIs o tutoriales donde los tokens de autenticación son enviados de vuelta al cliente en un encabezado de Autorización, en lugar de en el cuerpo de la respuesta como lo estamos haciendo en este capítulo.

Puedes hacer eso, y en la mayoría de los casos probablemente funcionará bien. Pero es importante ser consciente de que estás cometiendo una violación intencional de las especificaciones de HTTP: Autorización es un encabezado de solicitud, no un encabezado de respuesta.

15.3 Peticione de autenticacion

Ahora que nuestros clientes tienen una forma de intercambiar sus credenciales por un token de autenticación, veamos cómo podemos usar ese token para autenticarlos, de modo que sepamos exactamente de qué usuario proviene una solicitud.

Básicamente, una vez que un cliente tiene un token de autenticación, esperaremos que lo incluya en todas las solicitudes subsiguientes en un encabezado de Autorización, de la siguiente manera:

Authorization: Bearer IEYZQUBEMPPAKPOAWTPV6YJ6RM

Cuando recibamos estas solicitudes, utilizaremos un nuevo método de middleware llamado authenticate() para ejecutar la siguiente lógica:

  • Si el token de autenticación no es válido, entonces enviaremos al cliente una respuesta 401 No autorizado y un mensaje de error para informarles que su token está mal formado o es inválido.
  • Si el token de autenticación es válido, buscaremos los detalles del usuario y añadiremos sus detalles al contexto de la solicitud.
  • Si no se proporcionó ningún encabezado de Autorización en absoluto, entonces añadiremos los detalles para un usuario anónimo al contexto de la solicitud en su lugar.

15.3 Creando usuario anonimo

Comencemos con el último punto, y primero definamos un usuario anónimo en nuestro archivo internal/data/users.go, así:

package data

import "time"

// Declare a new AnonymousUser variable.
var AnonymousUser = &User{}

// User represents a user.
type User struct {
    ID        int64     `json:"id"`
    CreatedAt time.Time `json:"created_at"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Password  string    `json:"-"`
    Activated bool      `json:"activated"`
    Version   int       `json:"-"`
}

// IsAnonymous checks if a User instance is the AnonymousUser.
func (u *User) IsAnonymous() bool {
    return u == AnonymousUser
}

Entonces, aquí hemos creado una nueva variable AnonymousUser, que contiene un puntero a una estructura User representando a un usuario no activado sin ID, nombre, correo electrónico o contraseña.

También hemos implementado un método IsAnonymous() en la estructura User, de modo que siempre que tengamos una instancia de User, podemos verificar fácilmente si es la instancia AnonymousUser o no.

Por ejemplo:

data.AnonymousUser.IsAnonymous() // → Returns true
otherUser := &amp;data.User{}
otherUser.IsAnonymous()
// → Returns false

15.3 Lectura y escritura en el contexto de la solicitud

El otro paso de configuración, antes de entrar en la creación del middleware authenticate() en sí, se relaciona con almacenar los detalles del usuario en el contexto de la solicitud.

Hemos discutido en detalle qué es el contexto de la solicitud y cómo usarlo en el libro “Let’s Go”. Si alguna parte de esto te resulta desconocida, recomiendo volver a leer esa sección del libro antes de continuar.

Pero como recordatorio rápido:

  • Cada http.Request que procesa nuestra aplicación tiene incorporado un context.Context, que podemos utilizar para almacenar pares clave/valor con datos arbitrarios durante la duración de la solicitud. En este caso, queremos almacenar una estructura User que contiene la información del usuario actual.

  • Cualquier valor almacenado en el contexto de la solicitud tiene el tipo any. Esto significa que después de recuperar un valor del contexto de la solicitud, debes afirmarlo nuevamente a su tipo original antes de usarlo.

  • Es una buena práctica utilizar tu propio tipo personalizado para las claves del contexto de la solicitud. Esto ayuda a prevenir colisiones de nombres entre tu código y cualquier paquete de terceros que también esté utilizando el contexto de la solicitud para almacenar información.

Para ayudar con esto, creemos un nuevo archivo cmd/api/context.go que contenga algunos métodos auxiliares para leer/escribir la estructura User hacia y desde el contexto de la solicitud.

Si estás siguiendo el proceso, adelante y crea el nuevo archivo: cmd/api/context.go.

$ touch cmd/api/context.go

Agragamos lo siguiente

package main

import (
    "context"
    "net/http"
    "github.com/nahueldev23/greenlight/internal/data"
)

// Define a custom contextKey type, with the underlying type string.
type contextKey string

// Convert the string "user" to a contextKey type and assign it to the userContextKey
// constant. We'll use this constant as the key for getting and setting user information
// in the request context.
const userContextKey = contextKey("user")

// The contextSetUser() method returns a new copy of the request with the provided
// User struct added to the context. Note that we use our userContextKey constant as the
// key.
func (app *application) contextSetUser(r *http.Request, user *data.User) *http.Request {
    ctx := context.WithValue(r.Context(), userContextKey, user)
    return r.WithContext(ctx)
}

// The contextGetUser() retrieves the User struct from the request context. The only
// time that we'll use this helper is when we logically expect there to be User struct
// value in the context, and if it doesn't exist it will firmly be an 'unexpected' error.
// As we discussed earlier in the book, it's OK to panic in those circumstances.
func (app *application) contextGetUser(r *http.Request) *data.User {
    user, ok := r.Context().Value(userContextKey).(*data.User)
    if !ok {
        panic("missing user value in request context")
    }
    return user
}

15.3 Creando middleware de autenticacion

Ahora que tenemos esas cosas en su lugar, estamos listos para comenzar a trabajar en nuestro middleware authenticate() en sí. Abre tu archivo cmd/api/middleware.go y agrega el siguiente código:

package main

import (
    "errors"
    "fmt"
    "net"
    "net/http"
    "strings"
    "sync"
    "time"

    "greenlight.alexedwards.net/internal/data"
    "greenlight.alexedwards.net/internal/validator"
    "golang.org/x/time/rate"
)

//...

func (app *application) authenticate(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Add the "Vary: Authorization" header to the response. This indicates to any
        // caches that the response may vary based on the value of the Authorization
        // header in the request.
        w.Header().Add("Vary", "Authorization")

        // Retrieve the value of the Authorization header from the request. This will
        // return the empty string "" if there is no such header found.
        authorizationHeader := r.Header.Get("Authorization")

        // If there is no Authorization header found, use the contextSetUser() helper
        // that we just made to add the AnonymousUser to the request context. Then we
        // call the next handler in the chain and return without executing any of the
        // code below.
        if authorizationHeader == "" {
            r = app.contextSetUser(r, data.AnonymousUser)
            next.ServeHTTP(w, r)
            return
        }

        // Otherwise, we expect the value of the Authorization header to be in the format
        // "Bearer <token>". We try to split this into its constituent parts, and if the
        // header isn't in the expected format we return a 401 Unauthorized response
        // using the invalidAuthenticationTokenResponse() helper (which we will create
        // in a moment).
        headerParts := strings.Split(authorizationHeader, " ")
        if len(headerParts) != 2 || headerParts[0] != "Bearer" {
            app.invalidAuthenticationTokenResponse(w, r)
            return
        }

        // Extract the actual authentication token from the header parts.
        token := headerParts[1]

        // Validate the token to make sure it is in a sensible format.
        v := validator.New()

        // If the token isn't valid, use the invalidAuthenticationTokenResponse()
        // helper to send a response, rather than the failedValidationResponse() helper
        // that we'd normally use.
        if data.ValidateTokenPlaintext(v, token); !v.Valid() {
            app.invalidAuthenticationTokenResponse(w, r)
            return
        }

        // Retrieve the details of the user associated with the authentication token,
        // again calling the invalidAuthenticationTokenResponse() helper if no
        // matching record was found. IMPORTANT: Notice that we are using
        // ScopeAuthentication as the first parameter here.
        user, err := app.models.Users.GetForToken(data.ScopeAuthentication, token)
        if err != nil {
            switch {
            case errors.Is(err, data.ErrRecordNotFound):
                app.invalidAuthenticationTokenResponse(w, r)
            default:
                app.serverErrorResponse(w, r, err)
            }
            return
        }

        // Call the contextSetUser() helper to add the user information to the request
        // context.
        r = app.contextSetUser(r, user)

        // Call the next handler in the chain.
        next.ServeHTTP(w, r)
    })
}

Hay bastante código allí, así que para ayudar a aclarar las cosas, permita que reitere rápidamente las acciones que este middleware está realizando:

  1. Si se proporciona un token de autenticación válido en el encabezado de Autorización, entonces una estructura de usuario (User struct) que contiene los detalles de usuario correspondientes se almacenará en el contexto de la solicitud.

  2. Si no se proporciona ningún encabezado de Autorización en absoluto, nuestra estructura AnonymousUser se almacenará en el contexto de la solicitud.

  3. Si se proporciona el encabezado de Autorización, pero está mal formado o contiene un valor inválido, el cliente recibirá una respuesta 401 No autorizado utilizando la función invalidAuthenticationTokenResponse() para enviar la respuesta.

Hablando de eso, vamos a nuestro archivo cmd/api/errors.go y creemos ese ayudante de la siguiente manera:

package main

import (
    "net/http"
)

func (app *application) invalidAuthenticationTokenResponse(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "Unauthorized", http.StatusUnauthorized)
}

Nota: Estamos incluyendo un encabezado WWW-Authenticate: Bearer aquí para ayudar a informar o recordar al cliente que esperamos que se autentiquen utilizando un token de portador (bearer token).

Finalmente, necesitamos agregar el middleware authenticate() a nuestra cadena de manejadores. Queremos usar este middleware en todas las solicitudes, después de nuestro middleware de recuperación de pánico y limitador de velocidad, pero antes de nuestro enrutador.

Adelante y actualiza el archivo cmd/api/routes.go en consecuencia:

package main

import (
    "net/http"

    "github.com/justinas/alice"
)

func (app *application) routes() http.Handler {
    // Create a new Alice chain containing the middleware functions.
    standardMiddleware := alice.New(app.recoverPanic, app.rateLimit, app.authenticate)

    // Convert the notFoundResponse function to a http.Handler using the http.HandlerFunc
    // adapter, then add it as a catch-all route.
    mux := http.NewServeMux()
    mux.HandleFunc("/", app.home)
    mux.HandleFunc("/snippet", app.showSnippet)
    mux.HandleFunc("/snippet/create", app.createSnippet)
    mux.HandleFunc("/user/signup", app.signupUser)
    mux.HandleFunc("/user/login", app.loginUser)

    // Return the 'standardMiddleware' chain followed by the servemux.
    return standardMiddleware.Then(mux)
}

15.3 Demostracion

Vamos a probar esto primero haciendo una solicitud sin encabezado de Autorización. Entre bastidores, nuestro middleware authenticate() agregará el AnonymousUser al contexto de la solicitud y la solicitud debería completarse correctamente. Así:

$ curl localhost:4000/v1/healthcheck
{
"status": "available",
"system_info": {
"environment": "development",
"version": "1.0.0"
}
}

Luego, intentemos lo mismo pero con un token de autenticación válido en el encabezado de Autorización. Esta vez, los detalles relevantes del usuario deberían agregarse al contexto de la solicitud, y nuevamente deberíamos obtener una respuesta exitosa. Por ejemplo:

$ curl -d '{"email": "alice@example.com", "password": "pa55word"}' localhost:4000/v1/tokens/authentication
{
    "authentication_token": {
        "token": "FXCZM44TVLC6ML2NXTOW5OHFUE",
        "expiry": "2021-04-17T12:20:30.02833444+02:00"
    }
}
$ curl -H "Authorization: Bearer FXCZM44TVLC6ML2NXTOW5OHFUE" localhost:4000/v1/healthcheck
{
    "status": "available",
    "system_info": {
        "environment": "development",
        "version": "1.0.0"
    }
}

Pista: Si obtienes una respuesta de error aquí, asegúrate de estar utilizando el token de autenticación correcto de la primera solicitud en la segunda solicitud.

En contraste, también puedes intentar enviar algunas solicitudes con un token de autenticación inválido o un encabezado de Autorización mal formado. En estos casos, deberías obtener una respuesta 401 No autorizado, así:

$ curl -i -H "Authorization: Bearer XXXXXXXXXXXXXXXXXXXXXXXXXX" localhost:4000/v1/healthcheck
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Www-Authenticate: Bearer
Date: Fri, 16 Apr 2021 10:23:06 GMT
Content-Length: 56

{
    "error": "invalid or missing authentication token"
}
$ curl -i -H "Authorization: INVALID" localhost:4000/v1/healthcheck
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Www-Authenticate: Bearer
Date: Fri, 16 Apr 2021 10:23:26 GMT
Content-Length: 56

{
    "error": "invalid or missing authentication token"
}

Post Relacionados